From: kruland2607 Date: Sun, 8 Jan 2012 02:24:44 +0000 (+0000) Subject: moving to core/ X-Git-Tag: upstream/12.03~1^2~173 X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=8654c7d5a9d56274a296500d40c7f74229cdf6f1;p=debian%2Fopenrocket moving to core/ git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@298 180e2498-e6e9-4542-8430-84ac67f01cd8 --- diff --git a/core/src/META-INF/services/net.sf.openrocket.optimization.services.OptimizableParameterService b/core/src/META-INF/services/net.sf.openrocket.optimization.services.OptimizableParameterService new file mode 100644 index 00000000..522a653f --- /dev/null +++ b/core/src/META-INF/services/net.sf.openrocket.optimization.services.OptimizableParameterService @@ -0,0 +1,2 @@ +# Default service implementation: +net.sf.openrocket.optimization.services.DefaultOptimizableParameterService diff --git a/core/src/META-INF/services/net.sf.openrocket.optimization.services.SimulationModifierService b/core/src/META-INF/services/net.sf.openrocket.optimization.services.SimulationModifierService new file mode 100644 index 00000000..7b5cda9b --- /dev/null +++ b/core/src/META-INF/services/net.sf.openrocket.optimization.services.SimulationModifierService @@ -0,0 +1,2 @@ +# Default service implementation: +net.sf.openrocket.optimization.services.DefaultSimulationModifierService diff --git a/core/src/net/sf/openrocket/aerodynamics/AbstractAerodynamicCalculator.java b/core/src/net/sf/openrocket/aerodynamics/AbstractAerodynamicCalculator.java new file mode 100644 index 00000000..a92d0ab4 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/AbstractAerodynamicCalculator.java @@ -0,0 +1,116 @@ +package net.sf.openrocket.aerodynamics; + +import java.util.Map; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; + + +/** + * An abstract aerodynamic calculator implementation, that offers basic implementation + * of some methods and methods for cache validation and purging. + * + * @author Sampo Niskanen + */ + +public abstract class AbstractAerodynamicCalculator implements AerodynamicCalculator { + private static final LogHelper log = Application.getLogger(); + + /** Number of divisions used when calculating worst CP. */ + public static final int DIVISIONS = 360; + + /** + * A WarningSet that can be used if null is passed + * to a calculation method. + */ + protected WarningSet ignoreWarningSet = new WarningSet(); + + /** The aerodynamic modification ID of the latest rocket */ + private int rocketAeroModID = -1; + private int rocketTreeModID = -1; + + + + + //////////////// Aerodynamic calculators //////////////// + + @Override + public abstract Coordinate getCP(Configuration configuration, FlightConditions conditions, + WarningSet warnings); + + @Override + public abstract Map getForceAnalysis(Configuration configuration, FlightConditions conditions, + WarningSet warnings); + + @Override + public abstract AerodynamicForces getAerodynamicForces(Configuration configuration, + FlightConditions conditions, WarningSet warnings); + + + + /* + * The worst theta angle is stored in conditions. + */ + @Override + public Coordinate getWorstCP(Configuration configuration, FlightConditions conditions, + WarningSet warnings) { + FlightConditions cond = conditions.clone(); + Coordinate worst = new Coordinate(Double.MAX_VALUE); + Coordinate cp; + double theta = 0; + + for (int i = 0; i < DIVISIONS; i++) { + cond.setTheta(2 * Math.PI * i / DIVISIONS); + cp = getCP(configuration, cond, warnings); + if (cp.x < worst.x) { + worst = cp; + theta = cond.getTheta(); + } + } + + conditions.setTheta(theta); + + return worst; + } + + + + /** + * Check the current cache consistency. This method must be called by all + * methods that may use any cached data before any other operations are + * performed. If the rocket has changed since the previous call to + * checkCache(), then {@link #voidAerodynamicCache()} is called. + *

+ * This method performs the checking based on the rocket's modification IDs, + * so that these method may be called from listeners of the rocket itself. + * + * @param configuration the configuration of the current call + */ + protected final void checkCache(Configuration configuration) { + if (rocketAeroModID != configuration.getRocket().getAerodynamicModID() || + rocketTreeModID != configuration.getRocket().getTreeModID()) { + rocketAeroModID = configuration.getRocket().getAerodynamicModID(); + rocketTreeModID = configuration.getRocket().getTreeModID(); + log.debug("Voiding the aerodynamic cache"); + voidAerodynamicCache(); + } + } + + + + /** + * Void cached aerodynamic data. This method is called whenever a change occurs in + * the rocket structure that affects the aerodynamics of the rocket and when a new + * Rocket is set. This method must be overridden to void any cached data + * necessary. The method must call super.voidAerodynamicCache() during + * its execution. + */ + protected void voidAerodynamicCache() { + // No-op + } + + +} diff --git a/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java b/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java new file mode 100644 index 00000000..0ce3efc5 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java @@ -0,0 +1,70 @@ +package net.sf.openrocket.aerodynamics; + +import java.util.Map; + +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Monitorable; + +/** + * An interface for performing aerodynamic calculations on rockets. + * + * @author Sampo Niskanen + */ +public interface AerodynamicCalculator extends Monitorable { + + /** + * Calculate the CP of the specified configuration. + * + * @param configuration the rocket configuration + * @param conditions the flight conditions + * @param warnings the set in which to place warnings, or null + * @return the CP position in absolute coordinates + */ + public Coordinate getCP(Configuration configuration, FlightConditions conditions, WarningSet warnings); + + /** + * Calculate the aerodynamic forces acting upon the rocket. + * + * @param configuration the rocket configuration. + * @param conditions the flight conditions. + * @param warnings the set in which to place warnings, or null. + * @return the aerodynamic forces acting upon the rocket. + */ + public AerodynamicForces getAerodynamicForces(Configuration configuration, + FlightConditions conditions, WarningSet warnings); + + /** + * Calculate the aerodynamic forces acting upon the rocket with a component analysis. + * + * @param configuration the rocket configuration. + * @param conditions the flight conditions. + * @param warnings the set in which to place warnings, or null. + * @return a map from the rocket components to the aerodynamic force portions that component + * exerts. The map contains an value for the base rocket, which is the total + * aerodynamic forces. + */ + public Map getForceAnalysis(Configuration configuration, + FlightConditions conditions, WarningSet warnings); + + /** + * Calculate the worst CP occurring for any lateral wind angle. The worst CP is returned and the theta angle + * that produces the worst CP is stored in the flight conditions. + * + * @param configuration the rocket configuration. + * @param conditions the flight conditions. + * @param warnings the set in which to place warnings, or null. + * @return the worst (foremost) CP position for any lateral wind angle. + */ + public Coordinate getWorstCP(Configuration configuration, FlightConditions conditions, + WarningSet warnings); + + /** + * Return a new instance of this aerodynamic calculator type. + * + * @return a new, independent instance of this aerodynamic calculator type + */ + public AerodynamicCalculator newInstance(); + +} diff --git a/core/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java b/core/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java new file mode 100644 index 00000000..2bb6d75b --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java @@ -0,0 +1,355 @@ +package net.sf.openrocket.aerodynamics; + +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Monitorable; + +public class AerodynamicForces implements Cloneable, Monitorable { + + /** + * The component this data is referring to. Used in analysis methods. + * A total value is indicated by the component being the Rocket component. + */ + private RocketComponent component = null; + + + /** CP and CNa. */ + private Coordinate cp = null; + + + /** + * Normal force coefficient derivative. At values close to zero angle of attack + * this value may be poorly defined or NaN if the calculation method does not + * compute CNa directly. + */ + private double CNa = Double.NaN; + + + /** Normal force coefficient. */ + private double CN = Double.NaN; + + /** Pitching moment coefficient, relative to the coordinate origin. */ + private double Cm = Double.NaN; + + /** Side force coefficient, Cy */ + private double Cside = Double.NaN; + + /** Yaw moment coefficient, Cn, relative to the coordinate origin. */ + private double Cyaw = Double.NaN; + + /** Roll moment coefficient, Cl, relative to the coordinate origin. */ + private double Croll = Double.NaN; + + /** Roll moment damping coefficient */ + private double CrollDamp = Double.NaN; + + /** Roll moment forcing coefficient */ + private double CrollForce = Double.NaN; + + + + /** Axial drag coefficient, CA */ + private double Caxial = Double.NaN; + + /** Total drag force coefficient, parallel to the airflow. */ + private double CD = Double.NaN; + + /** Drag coefficient due to fore pressure drag. */ + private double pressureCD = Double.NaN; + + /** Drag coefficient due to base drag. */ + private double baseCD = Double.NaN; + + /** Drag coefficient due to friction drag. */ + private double frictionCD = Double.NaN; + + + private double pitchDampingMoment = Double.NaN; + private double yawDampingMoment = Double.NaN; + + private int modID = 0; + + + public void setComponent(RocketComponent component) { + this.component = component; + modID++; + } + + public RocketComponent getComponent() { + return component; + } + + public void setCP(Coordinate cp) { + this.cp = cp; + modID++; + } + + public Coordinate getCP() { + return cp; + } + + public void setCNa(double cNa) { + CNa = cNa; + modID++; + } + + public double getCNa() { + return CNa; + } + + public void setCN(double cN) { + CN = cN; + modID++; + } + + public double getCN() { + return CN; + } + + public void setCm(double cm) { + Cm = cm; + modID++; + } + + public double getCm() { + return Cm; + } + + public void setCside(double cside) { + Cside = cside; + modID++; + } + + public double getCside() { + return Cside; + } + + public void setCyaw(double cyaw) { + Cyaw = cyaw; + modID++; + } + + public double getCyaw() { + return Cyaw; + } + + public void setCroll(double croll) { + Croll = croll; + modID++; + } + + public double getCroll() { + return Croll; + } + + public void setCrollDamp(double crollDamp) { + CrollDamp = crollDamp; + modID++; + } + + public double getCrollDamp() { + return CrollDamp; + } + + public void setCrollForce(double crollForce) { + CrollForce = crollForce; + modID++; + } + + public double getCrollForce() { + return CrollForce; + } + + public void setCaxial(double caxial) { + Caxial = caxial; + modID++; + } + + public double getCaxial() { + return Caxial; + } + + public void setCD(double cD) { + CD = cD; + modID++; + } + + public double getCD() { + return CD; + } + + public void setPressureCD(double pressureCD) { + this.pressureCD = pressureCD; + modID++; + } + + public double getPressureCD() { + return pressureCD; + } + + public void setBaseCD(double baseCD) { + this.baseCD = baseCD; + modID++; + } + + public double getBaseCD() { + return baseCD; + } + + public void setFrictionCD(double frictionCD) { + this.frictionCD = frictionCD; + modID++; + } + + public double getFrictionCD() { + return frictionCD; + } + + public void setPitchDampingMoment(double pitchDampingMoment) { + this.pitchDampingMoment = pitchDampingMoment; + modID++; + } + + public double getPitchDampingMoment() { + return pitchDampingMoment; + } + + public void setYawDampingMoment(double yawDampingMoment) { + this.yawDampingMoment = yawDampingMoment; + modID++; + } + + public double getYawDampingMoment() { + return yawDampingMoment; + } + + + + /** + * Reset all values to null/NaN. + */ + public void reset() { + setComponent(null); + + setCP(null); + setCNa(Double.NaN); + setCN(Double.NaN); + setCm(Double.NaN); + setCside(Double.NaN); + setCyaw(Double.NaN); + setCroll(Double.NaN); + setCrollDamp(Double.NaN); + setCrollForce(Double.NaN); + setCaxial(Double.NaN); + setCD(Double.NaN); + setPitchDampingMoment(Double.NaN); + setYawDampingMoment(Double.NaN); + } + + /** + * Zero all values to 0 / Coordinate.NUL. Component is left as it was. + */ + public void zero() { + // component untouched + + setCP(Coordinate.NUL); + setCNa(0); + setCN(0); + setCm(0); + setCside(0); + setCyaw(0); + setCroll(0); + setCrollDamp(0); + setCrollForce(0); + setCaxial(0); + setCD(0); + setPitchDampingMoment(0); + setYawDampingMoment(0); + } + + + @Override + public AerodynamicForces clone() { + try { + return (AerodynamicForces)super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException?!?"); + } + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof AerodynamicForces)) + return false; + AerodynamicForces other = (AerodynamicForces) obj; + + return (MathUtil.equals(this.getCNa(), other.getCNa()) && + MathUtil.equals(this.getCN(), other.getCN()) && + MathUtil.equals(this.getCm(), other.getCm()) && + MathUtil.equals(this.getCside(), other.getCside()) && + MathUtil.equals(this.getCyaw(), other.getCyaw()) && + MathUtil.equals(this.getCroll(), other.getCroll()) && + MathUtil.equals(this.getCrollDamp(), other.getCrollDamp()) && + MathUtil.equals(this.getCrollForce(), other.getCrollForce()) && + MathUtil.equals(this.getCaxial(), other.getCaxial()) && + MathUtil.equals(this.getCD(), other.getCD()) && + MathUtil.equals(this.getPressureCD(), other.getPressureCD()) && + MathUtil.equals(this.getBaseCD(), other.getBaseCD()) && + MathUtil.equals(this.getFrictionCD(), other.getFrictionCD()) && + MathUtil.equals(this.getPitchDampingMoment(), other.getPitchDampingMoment()) && + MathUtil.equals(this.getYawDampingMoment(), other.getYawDampingMoment()) && + this.getCP().equals(other.getCP())); + } + + @Override + public int hashCode() { + return (int) (1000*(this.getCD()+this.getCaxial()+this.getCNa())) + this.getCP().hashCode(); + } + + + @Override + public String toString() { + String text="AerodynamicForces["; + + if (getComponent() != null) + text += "component:" + getComponent() + ","; + if (getCP() != null) + text += "cp:" + getCP() + ","; + + if (!Double.isNaN(getCNa())) + text += "CNa:" + getCNa() + ","; + if (!Double.isNaN(getCN())) + text += "CN:" + getCN() + ","; + if (!Double.isNaN(getCm())) + text += "Cm:" + getCm() + ","; + + if (!Double.isNaN(getCside())) + text += "Cside:" + getCside() + ","; + if (!Double.isNaN(getCyaw())) + text += "Cyaw:" + getCyaw() + ","; + + if (!Double.isNaN(getCroll())) + text += "Croll:" + getCroll() + ","; + if (!Double.isNaN(getCaxial())) + text += "Caxial:" + getCaxial() + ","; + + if (!Double.isNaN(getCD())) + text += "CD:" + getCD() + ","; + + if (text.charAt(text.length()-1) == ',') + text = text.substring(0, text.length()-1); + + text += "]"; + return text; + } + + @Override + public int getModID() { + return modID; + } +} diff --git a/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java new file mode 100644 index 00000000..be139e14 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java @@ -0,0 +1,715 @@ +package net.sf.openrocket.aerodynamics; + +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import net.sf.openrocket.aerodynamics.barrowman.FinSetCalc; +import net.sf.openrocket.aerodynamics.barrowman.RocketComponentCalc; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.PolyInterpolator; +import net.sf.openrocket.util.Reflection; + +/** + * An aerodynamic calculator that uses the extended Barrowman method to + * calculate the CP of a rocket. + * + * @author Sampo Niskanen + */ +public class BarrowmanCalculator extends AbstractAerodynamicCalculator { + + private static final String BARROWMAN_PACKAGE = "net.sf.openrocket.aerodynamics.barrowman"; + private static final String BARROWMAN_SUFFIX = "Calc"; + + + private Map calcMap = null; + + private double cacheDiameter = -1; + private double cacheLength = -1; + + + + public BarrowmanCalculator() { + + } + + + @Override + public BarrowmanCalculator newInstance() { + return new BarrowmanCalculator(); + } + + + /** + * Calculate the CP according to the extended Barrowman method. + */ + @Override + public Coordinate getCP(Configuration configuration, FlightConditions conditions, + WarningSet warnings) { + checkCache(configuration); + AerodynamicForces forces = calculateNonAxialForces(configuration, conditions, null, warnings); + return forces.getCP(); + } + + + + @Override + public Map getForceAnalysis(Configuration configuration, + FlightConditions conditions, WarningSet warnings) { + checkCache(configuration); + + AerodynamicForces f; + Map map = + new LinkedHashMap(); + + // Add all components to the map + for (RocketComponent c : configuration) { + f = new AerodynamicForces(); + f.setComponent(c); + + map.put(c, f); + } + + + // Calculate non-axial force data + AerodynamicForces total = calculateNonAxialForces(configuration, conditions, map, warnings); + + + // Calculate friction data + total.setFrictionCD(calculateFrictionDrag(configuration, conditions, map, warnings)); + total.setPressureCD(calculatePressureDrag(configuration, conditions, map, warnings)); + total.setBaseCD(calculateBaseDrag(configuration, conditions, map, warnings)); + + total.setComponent(configuration.getRocket()); + map.put(total.getComponent(), total); + + + for (RocketComponent c : map.keySet()) { + f = map.get(c); + if (Double.isNaN(f.getBaseCD()) && Double.isNaN(f.getPressureCD()) && + Double.isNaN(f.getFrictionCD())) + continue; + if (Double.isNaN(f.getBaseCD())) + f.setBaseCD(0); + if (Double.isNaN(f.getPressureCD())) + f.setPressureCD(0); + if (Double.isNaN(f.getFrictionCD())) + f.setFrictionCD(0); + f.setCD(f.getBaseCD() + f.getPressureCD() + f.getFrictionCD()); + f.setCaxial(calculateAxialDrag(conditions, f.getCD())); + } + + return map; + } + + + + @Override + public AerodynamicForces getAerodynamicForces(Configuration configuration, + FlightConditions conditions, WarningSet warnings) { + checkCache(configuration); + + if (warnings == null) + warnings = ignoreWarningSet; + + // Calculate non-axial force data + AerodynamicForces total = calculateNonAxialForces(configuration, conditions, null, warnings); + + // Calculate friction data + total.setFrictionCD(calculateFrictionDrag(configuration, conditions, null, warnings)); + total.setPressureCD(calculatePressureDrag(configuration, conditions, null, warnings)); + total.setBaseCD(calculateBaseDrag(configuration, conditions, null, warnings)); + + total.setCD(total.getFrictionCD() + total.getPressureCD() + total.getBaseCD()); + + total.setCaxial(calculateAxialDrag(conditions, total.getCD())); + + // Calculate pitch and yaw damping moments + calculateDampingMoments(configuration, conditions, total); + total.setCm(total.getCm() - total.getPitchDampingMoment()); + total.setCyaw(total.getCyaw() - total.getYawDampingMoment()); + + + return total; + } + + + + + /* + * Perform the actual CP calculation. + */ + private AerodynamicForces calculateNonAxialForces(Configuration configuration, FlightConditions conditions, + Map map, WarningSet warnings) { + + checkCache(configuration); + + AerodynamicForces total = new AerodynamicForces(); + total.zero(); + + double radius = 0; // aft radius of previous component + double componentX = 0; // aft coordinate of previous component + AerodynamicForces forces = new AerodynamicForces(); + + if (warnings == null) + warnings = ignoreWarningSet; + + if (conditions.getAOA() > 17.5 * Math.PI / 180) + warnings.add(new Warning.LargeAOA(conditions.getAOA())); + + + if (calcMap == null) + buildCalcMap(configuration); + + for (RocketComponent component : configuration) { + + // Skip non-aerodynamic components + if (!component.isAerodynamic()) + continue; + + // Check for discontinuities + if (component instanceof SymmetricComponent) { + SymmetricComponent sym = (SymmetricComponent) component; + // TODO:LOW: Ignores other cluster components (not clusterable) + double x = component.toAbsolute(Coordinate.NUL)[0].x; + + // Check for lengthwise discontinuity + if (x > componentX + 0.0001) { + if (!MathUtil.equals(radius, 0)) { + warnings.add(Warning.DISCONTINUITY); + radius = 0; + } + } + componentX = component.toAbsolute(new Coordinate(component.getLength()))[0].x; + + // Check for radius discontinuity + if (!MathUtil.equals(sym.getForeRadius(), radius)) { + warnings.add(Warning.DISCONTINUITY); + // TODO: MEDIUM: Apply correction to values to cp and to map + } + radius = sym.getAftRadius(); + } + + // Call calculation method + forces.zero(); + calcMap.get(component).calculateNonaxialForces(conditions, forces, warnings); + forces.setCP(component.toAbsolute(forces.getCP())[0]); + forces.setCm(forces.getCN() * forces.getCP().x / conditions.getRefLength()); + + if (map != null) { + AerodynamicForces f = map.get(component); + + f.setCP(forces.getCP()); + f.setCNa(forces.getCNa()); + f.setCN(forces.getCN()); + f.setCm(forces.getCm()); + f.setCside(forces.getCside()); + f.setCyaw(forces.getCyaw()); + f.setCroll(forces.getCroll()); + f.setCrollDamp(forces.getCrollDamp()); + f.setCrollForce(forces.getCrollForce()); + } + + total.setCP(total.getCP().average(forces.getCP())); + total.setCNa(total.getCNa() + forces.getCNa()); + total.setCN(total.getCN() + forces.getCN()); + total.setCm(total.getCm() + forces.getCm()); + total.setCside(total.getCside() + forces.getCside()); + total.setCyaw(total.getCyaw() + forces.getCyaw()); + total.setCroll(total.getCroll() + forces.getCroll()); + total.setCrollDamp(total.getCrollDamp() + forces.getCrollDamp()); + total.setCrollForce(total.getCrollForce() + forces.getCrollForce()); + } + + return total; + } + + + + + //////////////// DRAG CALCULATIONS //////////////// + + + private double calculateFrictionDrag(Configuration configuration, FlightConditions conditions, + Map map, WarningSet set) { + double c1 = 1.0, c2 = 1.0; + + double mach = conditions.getMach(); + double Re; + double Cf; + + if (calcMap == null) + buildCalcMap(configuration); + + Re = conditions.getVelocity() * configuration.getLength() / + conditions.getAtmosphericConditions().getKinematicViscosity(); + + // Calculate the skin friction coefficient (assume non-roughness limited) + if (configuration.getRocket().isPerfectFinish()) { + + // Assume partial laminar layer. Roughness-limitation is checked later. + if (Re < 1e4) { + // Too low, constant + Cf = 1.33e-2; + } else if (Re < 5.39e5) { + // Fully laminar + Cf = 1.328 / MathUtil.safeSqrt(Re); + } else { + // Transitional + Cf = 1.0 / pow2(1.50 * Math.log(Re) - 5.6) - 1700 / Re; + } + + // Compressibility correction + + if (mach < 1.1) { + // Below Re=1e6 no correction + if (Re > 1e6) { + if (Re < 3e6) { + c1 = 1 - 0.1 * pow2(mach) * (Re - 1e6) / 2e6; // transition to turbulent + } else { + c1 = 1 - 0.1 * pow2(mach); + } + } + } + if (mach > 0.9) { + if (Re > 1e6) { + if (Re < 3e6) { + c2 = 1 + (1.0 / Math.pow(1 + 0.045 * pow2(mach), 0.25) - 1) * (Re - 1e6) / 2e6; + } else { + c2 = 1.0 / Math.pow(1 + 0.045 * pow2(mach), 0.25); + } + } + } + + // Applying continuously around Mach 1 + if (mach < 0.9) { + Cf *= c1; + } else if (mach < 1.1) { + Cf *= (c2 * (mach - 0.9) / 0.2 + c1 * (1.1 - mach) / 0.2); + } else { + Cf *= c2; + } + + + } else { + + // Assume fully turbulent. Roughness-limitation is checked later. + if (Re < 1e4) { + // Too low, constant + Cf = 1.48e-2; + } else { + // Turbulent + Cf = 1.0 / pow2(1.50 * Math.log(Re) - 5.6); + } + + // Compressibility correction + + if (mach < 1.1) { + c1 = 1 - 0.1 * pow2(mach); + } + if (mach > 0.9) { + c2 = 1 / Math.pow(1 + 0.15 * pow2(mach), 0.58); + } + // Applying continuously around Mach 1 + if (mach < 0.9) { + Cf *= c1; + } else if (mach < 1.1) { + Cf *= c2 * (mach - 0.9) / 0.2 + c1 * (1.1 - mach) / 0.2; + } else { + Cf *= c2; + } + + } + + // Roughness-limited value correction term + double roughnessCorrection; + if (mach < 0.9) { + roughnessCorrection = 1 - 0.1 * pow2(mach); + } else if (mach > 1.1) { + roughnessCorrection = 1 / (1 + 0.18 * pow2(mach)); + } else { + c1 = 1 - 0.1 * pow2(0.9); + c2 = 1.0 / (1 + 0.18 * pow2(1.1)); + roughnessCorrection = c2 * (mach - 0.9) / 0.2 + c1 * (1.1 - mach) / 0.2; + } + + + + /* + * Calculate the friction drag coefficient. + * + * The body wetted area is summed up and finally corrected with the rocket + * fineness ratio (calculated in the same iteration). The fins are corrected + * for thickness as we go on. + */ + + double finFriction = 0; + double bodyFriction = 0; + double maxR = 0, len = 0; + + double[] roughnessLimited = new double[Finish.values().length]; + Arrays.fill(roughnessLimited, Double.NaN); + + for (RocketComponent c : configuration) { + + // Consider only SymmetricComponents and FinSets: + if (!(c instanceof SymmetricComponent) && + !(c instanceof FinSet)) + continue; + + // Calculate the roughness-limited friction coefficient + Finish finish = ((ExternalComponent) c).getFinish(); + if (Double.isNaN(roughnessLimited[finish.ordinal()])) { + roughnessLimited[finish.ordinal()] = + 0.032 * Math.pow(finish.getRoughnessSize() / configuration.getLength(), 0.2) * + roughnessCorrection; + } + + /* + * Actual Cf is maximum of Cf and the roughness-limited value. + * For perfect finish require additionally that Re > 1e6 + */ + double componentCf; + if (configuration.getRocket().isPerfectFinish()) { + + // For perfect finish require Re > 1e6 + if ((Re > 1.0e6) && (roughnessLimited[finish.ordinal()] > Cf)) { + componentCf = roughnessLimited[finish.ordinal()]; + } else { + componentCf = Cf; + } + + } else { + + // For fully turbulent use simple max + componentCf = Math.max(Cf, roughnessLimited[finish.ordinal()]); + + } + + + + // Calculate the friction drag: + if (c instanceof SymmetricComponent) { + + SymmetricComponent s = (SymmetricComponent) c; + + bodyFriction += componentCf * s.getComponentWetArea(); + + if (map != null) { + // Corrected later + map.get(c).setFrictionCD(componentCf * s.getComponentWetArea() + / conditions.getRefArea()); + } + + double r = Math.max(s.getForeRadius(), s.getAftRadius()); + if (r > maxR) + maxR = r; + len += c.getLength(); + + } else if (c instanceof FinSet) { + + FinSet f = (FinSet) c; + double mac = ((FinSetCalc) calcMap.get(c)).getMACLength(); + double cd = componentCf * (1 + 2 * f.getThickness() / mac) * + 2 * f.getFinCount() * f.getFinArea(); + finFriction += cd; + + if (map != null) { + map.get(c).setFrictionCD(cd / conditions.getRefArea()); + } + + } + + } + // fB may be POSITIVE_INFINITY, but that's ok for us + double fB = (len + 0.0001) / maxR; + double correction = (1 + 1.0 / (2 * fB)); + + // Correct body data in map + if (map != null) { + for (RocketComponent c : map.keySet()) { + if (c instanceof SymmetricComponent) { + map.get(c).setFrictionCD(map.get(c).getFrictionCD() * correction); + } + } + } + + return (finFriction + correction * bodyFriction) / conditions.getRefArea(); + } + + + + private double calculatePressureDrag(Configuration configuration, FlightConditions conditions, + Map map, WarningSet warnings) { + + double stagnation, base, total; + double radius = 0; + + if (calcMap == null) + buildCalcMap(configuration); + + stagnation = calculateStagnationCD(conditions.getMach()); + base = calculateBaseCD(conditions.getMach()); + + total = 0; + for (RocketComponent c : configuration) { + if (!c.isAerodynamic()) + continue; + + // Pressure fore drag + double cd = calcMap.get(c).calculatePressureDragForce(conditions, stagnation, base, + warnings); + total += cd; + + if (map != null) { + map.get(c).setPressureCD(cd); + } + + + // Stagnation drag + if (c instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent) c; + + if (radius < s.getForeRadius()) { + double area = Math.PI * (pow2(s.getForeRadius()) - pow2(radius)); + cd = stagnation * area / conditions.getRefArea(); + total += cd; + if (map != null) { + map.get(c).setPressureCD(map.get(c).getPressureCD() + cd); + } + } + + radius = s.getAftRadius(); + } + } + + return total; + } + + + private double calculateBaseDrag(Configuration configuration, FlightConditions conditions, + Map map, WarningSet warnings) { + + double base, total; + double radius = 0; + RocketComponent prevComponent = null; + + if (calcMap == null) + buildCalcMap(configuration); + + base = calculateBaseCD(conditions.getMach()); + total = 0; + + for (RocketComponent c : configuration) { + if (!(c instanceof SymmetricComponent)) + continue; + + SymmetricComponent s = (SymmetricComponent) c; + + if (radius > s.getForeRadius()) { + double area = Math.PI * (pow2(radius) - pow2(s.getForeRadius())); + double cd = base * area / conditions.getRefArea(); + total += cd; + if (map != null) { + map.get(prevComponent).setBaseCD(cd); + } + } + + radius = s.getAftRadius(); + prevComponent = c; + } + + if (radius > 0) { + double area = Math.PI * pow2(radius); + double cd = base * area / conditions.getRefArea(); + total += cd; + if (map != null) { + map.get(prevComponent).setBaseCD(cd); + } + } + + return total; + } + + + + public static double calculateStagnationCD(double m) { + double pressure; + if (m <= 1) { + pressure = 1 + pow2(m) / 4 + pow2(pow2(m)) / 40; + } else { + pressure = 1.84 - 0.76 / pow2(m) + 0.166 / pow2(pow2(m)) + 0.035 / pow2(m * m * m); + } + return 0.85 * pressure; + } + + + public static double calculateBaseCD(double m) { + if (m <= 1) { + return 0.12 + 0.13 * m * m; + } else { + return 0.25 / m; + } + } + + + + private static final double[] axialDragPoly1, axialDragPoly2; + static { + PolyInterpolator interpolator; + interpolator = new PolyInterpolator( + new double[] { 0, 17 * Math.PI / 180 }, + new double[] { 0, 17 * Math.PI / 180 } + ); + axialDragPoly1 = interpolator.interpolator(1, 1.3, 0, 0); + + interpolator = new PolyInterpolator( + new double[] { 17 * Math.PI / 180, Math.PI / 2 }, + new double[] { 17 * Math.PI / 180, Math.PI / 2 }, + new double[] { Math.PI / 2 } + ); + axialDragPoly2 = interpolator.interpolator(1.3, 0, 0, 0, 0); + } + + + /** + * Calculate the axial drag from the total drag coefficient. + * + * @param conditions + * @param cd + * @return + */ + private double calculateAxialDrag(FlightConditions conditions, double cd) { + double aoa = MathUtil.clamp(conditions.getAOA(), 0, Math.PI); + double mul; + + // double sinaoa = conditions.getSinAOA(); + // return cd * (1 + Math.min(sinaoa, 0.25)); + + + if (aoa > Math.PI / 2) + aoa = Math.PI - aoa; + if (aoa < 17 * Math.PI / 180) + mul = PolyInterpolator.eval(aoa, axialDragPoly1); + else + mul = PolyInterpolator.eval(aoa, axialDragPoly2); + + if (conditions.getAOA() < Math.PI / 2) + return mul * cd; + else + return -mul * cd; + } + + + private void calculateDampingMoments(Configuration configuration, FlightConditions conditions, + AerodynamicForces total) { + + // Calculate pitch and yaw damping moments + double mul = getDampingMultiplier(configuration, conditions, + conditions.getPitchCenter().x); + double pitch = conditions.getPitchRate(); + double yaw = conditions.getYawRate(); + double vel = conditions.getVelocity(); + + vel = MathUtil.max(vel, 1); + + mul *= 3; // TODO: Higher damping yields much more realistic apogee turn + + total.setPitchDampingMoment(mul * MathUtil.sign(pitch) * pow2(pitch / vel)); + total.setYawDampingMoment(mul * MathUtil.sign(yaw) * pow2(yaw / vel)); + } + + // TODO: MEDIUM: Are the rotation etc. being added correctly? sin/cos theta? + + + private double getDampingMultiplier(Configuration configuration, FlightConditions conditions, + double cgx) { + if (cacheDiameter < 0) { + double area = 0; + cacheLength = 0; + cacheDiameter = 0; + + for (RocketComponent c : configuration) { + if (c instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent) c; + area += s.getComponentPlanformArea(); + cacheLength += s.getLength(); + } + } + if (cacheLength > 0) + cacheDiameter = area / cacheLength; + } + + double mul; + + // Body + mul = 0.275 * cacheDiameter / (conditions.getRefArea() * conditions.getRefLength()); + mul *= (MathUtil.pow4(cgx) + MathUtil.pow4(cacheLength - cgx)); + + // Fins + // TODO: LOW: This could be optimized a lot... + for (RocketComponent c : configuration) { + if (c instanceof FinSet) { + FinSet f = (FinSet) c; + mul += 0.6 * Math.min(f.getFinCount(), 4) * f.getFinArea() * + MathUtil.pow3(Math.abs(f.toAbsolute(new Coordinate( + ((FinSetCalc) calcMap.get(f)).getMidchordPos()))[0].x + - cgx)) / + (conditions.getRefArea() * conditions.getRefLength()); + } + } + + return mul; + } + + + + //////// The calculator map + + @Override + protected void voidAerodynamicCache() { + super.voidAerodynamicCache(); + + calcMap = null; + cacheDiameter = -1; + cacheLength = -1; + } + + + private void buildCalcMap(Configuration configuration) { + Iterator iterator; + + calcMap = new HashMap(); + + iterator = configuration.getRocket().iterator(); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + + if (!c.isAerodynamic()) + continue; + + calcMap.put(c, (RocketComponentCalc) Reflection.construct(BARROWMAN_PACKAGE, + c, BARROWMAN_SUFFIX, c)); + } + } + + + @Override + public int getModID() { + // Only cached data is stored, return constant mod ID + return 0; + } + + +} diff --git a/core/src/net/sf/openrocket/aerodynamics/FlightConditions.java b/core/src/net/sf/openrocket/aerodynamics/FlightConditions.java new file mode 100644 index 00000000..0b079e68 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/FlightConditions.java @@ -0,0 +1,467 @@ +package net.sf.openrocket.aerodynamics; + +import java.util.ArrayList; +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; + +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.StateChangeListener; +import net.sf.openrocket.util.UniqueID; + +/** + * A class defining the momentary flight conditions of a rocket, including + * the angle of attack, lateral wind angle, atmospheric conditions etc. + * + * @author Sampo Niskanen + */ +public class FlightConditions implements Cloneable, ChangeSource, Monitorable { + + private List listenerList = new ArrayList(); + private EventObject event = new EventObject(this); + + + /** Reference length used in calculations. */ + private double refLength = 1.0; + + /** Reference area used in calculations. */ + private double refArea = Math.PI * 0.25; + + + /** Angle of attack. */ + private double aoa = 0; + + /** Sine of the angle of attack. */ + private double sinAOA = 0; + + /** + * The fraction sin(aoa) / aoa. At an AOA of zero this value + * must be one. This value may be used in many cases to avoid checking for + * division by zero. + */ + private double sincAOA = 1.0; + + /** Lateral wind direction. */ + private double theta = 0; + + /** Current Mach speed. */ + private double mach = 0.3; + + /** + * Sqrt(1 - M^2) for M<1 + * Sqrt(M^2 - 1) for M>1 + */ + private double beta = MathUtil.safeSqrt(1 - mach * mach); + + + /** Current roll rate. */ + private double rollRate = 0; + + private double pitchRate = 0; + private double yawRate = 0; + + private Coordinate pitchCenter = Coordinate.NUL; + + + private AtmosphericConditions atmosphericConditions = new AtmosphericConditions(); + + + private int modID; + private int modIDadd = 0; + + + /** + * Sole constructor. The reference length is initialized to the reference length + * of the Configuration, and the reference area accordingly. + * If config is null, then the reference length is set + * to 1 meter. + * + * @param config the configuration of which the reference length is taken. + */ + public FlightConditions(Configuration config) { + if (config != null) + setRefLength(config.getReferenceLength()); + this.modID = UniqueID.next(); + } + + + /** + * Set the reference length from the given configuration. + * @param config the configuration from which to get the reference length. + */ + public void setReference(Configuration config) { + setRefLength(config.getReferenceLength()); + } + + + /** + * Set the reference length and area. + */ + public void setRefLength(double length) { + refLength = length; + + refArea = Math.PI * MathUtil.pow2(length / 2); + fireChangeEvent(); + } + + /** + * Return the reference length. + */ + public double getRefLength() { + return refLength; + } + + /** + * Set the reference area and length. + */ + public void setRefArea(double area) { + refArea = area; + refLength = MathUtil.safeSqrt(area / Math.PI) * 2; + fireChangeEvent(); + } + + /** + * Return the reference area. + */ + public double getRefArea() { + return refArea; + } + + + /** + * Sets the angle of attack. It calculates values also for the methods + * {@link #getSinAOA()} and {@link #getSincAOA()}. + * + * @param aoa the angle of attack. + */ + public void setAOA(double aoa) { + aoa = MathUtil.clamp(aoa, 0, Math.PI); + if (MathUtil.equals(this.aoa, aoa)) + return; + + this.aoa = aoa; + if (aoa < 0.001) { + this.sinAOA = aoa; + this.sincAOA = 1.0; + } else { + this.sinAOA = Math.sin(aoa); + this.sincAOA = sinAOA / aoa; + } + fireChangeEvent(); + } + + + /** + * Sets the angle of attack with the sine. The value sinAOA is assumed + * to be the sine of aoa for cases in which this value is known. + * The AOA must still be specified, as the sine is not unique in the range + * of 0..180 degrees. + * + * @param aoa the angle of attack in radians. + * @param sinAOA the sine of the angle of attack. + */ + public void setAOA(double aoa, double sinAOA) { + aoa = MathUtil.clamp(aoa, 0, Math.PI); + sinAOA = MathUtil.clamp(sinAOA, 0, 1); + if (MathUtil.equals(this.aoa, aoa)) + return; + + assert (Math.abs(Math.sin(aoa) - sinAOA) < 0.0001) : "Illegal sine: aoa=" + aoa + " sinAOA=" + sinAOA; + + this.aoa = aoa; + this.sinAOA = sinAOA; + if (aoa < 0.001) { + this.sincAOA = 1.0; + } else { + this.sincAOA = sinAOA / aoa; + } + fireChangeEvent(); + } + + + /** + * Return the angle of attack. + */ + public double getAOA() { + return aoa; + } + + /** + * Return the sine of the angle of attack. + */ + public double getSinAOA() { + return sinAOA; + } + + /** + * Return the sinc of the angle of attack (sin(AOA) / AOA). This method returns + * one if the angle of attack is zero. + */ + public double getSincAOA() { + return sincAOA; + } + + + /** + * Set the direction of the lateral airflow. + */ + public void setTheta(double theta) { + if (MathUtil.equals(this.theta, theta)) + return; + this.theta = theta; + fireChangeEvent(); + } + + /** + * Return the direction of the lateral airflow. + */ + public double getTheta() { + return theta; + } + + + /** + * Set the current Mach speed. This should be (but is not required to be) in + * reference to the speed of sound of the atmospheric conditions. + */ + public void setMach(double mach) { + mach = Math.max(mach, 0); + if (MathUtil.equals(this.mach, mach)) + return; + + this.mach = mach; + if (mach < 1) + this.beta = MathUtil.safeSqrt(1 - mach * mach); + else + this.beta = MathUtil.safeSqrt(mach * mach - 1); + fireChangeEvent(); + } + + /** + * Return the current Mach speed. + */ + public double getMach() { + return mach; + } + + /** + * Returns the current rocket velocity, calculated from the Mach number and the + * speed of sound. If either of these parameters are changed, the velocity changes + * as well. + * + * @return the velocity of the rocket. + */ + public double getVelocity() { + return mach * atmosphericConditions.getMachSpeed(); + } + + /** + * Sets the Mach speed according to the given velocity and the current speed of sound. + * + * @param velocity the current velocity. + */ + public void setVelocity(double velocity) { + setMach(velocity / atmosphericConditions.getMachSpeed()); + } + + + /** + * Return sqrt(abs(1 - Mach^2)). This is calculated in the setting call and is + * therefore fast. + */ + public double getBeta() { + return beta; + } + + + /** + * Return the current roll rate. + */ + public double getRollRate() { + return rollRate; + } + + + /** + * Set the current roll rate. + */ + public void setRollRate(double rate) { + if (MathUtil.equals(this.rollRate, rate)) + return; + + this.rollRate = rate; + fireChangeEvent(); + } + + + public double getPitchRate() { + return pitchRate; + } + + + public void setPitchRate(double pitchRate) { + if (MathUtil.equals(this.pitchRate, pitchRate)) + return; + this.pitchRate = pitchRate; + fireChangeEvent(); + } + + + public double getYawRate() { + return yawRate; + } + + + public void setYawRate(double yawRate) { + if (MathUtil.equals(this.yawRate, yawRate)) + return; + this.yawRate = yawRate; + fireChangeEvent(); + } + + + + + /** + * @return the pitchCenter + */ + public Coordinate getPitchCenter() { + return pitchCenter; + } + + + /** + * @param pitchCenter the pitchCenter to set + */ + public void setPitchCenter(Coordinate pitchCenter) { + if (this.pitchCenter.equals(pitchCenter)) + return; + this.pitchCenter = pitchCenter; + fireChangeEvent(); + } + + + /** + * Return the current atmospheric conditions. Note that this method returns a + * reference to the {@link AtmosphericConditions} object used by this object. + * Changes made to the object will modify the encapsulated object, but will NOT + * generate change events. + * + * @return the current atmospheric conditions. + */ + public AtmosphericConditions getAtmosphericConditions() { + return atmosphericConditions; + } + + /** + * Set the current atmospheric conditions. This method will fire a change event + * if a change occurs. + */ + public void setAtmosphericConditions(AtmosphericConditions cond) { + if (atmosphericConditions.equals(cond)) + return; + modIDadd += atmosphericConditions.getModID(); + atmosphericConditions = cond; + fireChangeEvent(); + } + + + /** + * Retrieve the modification count of this object. Each time it is modified + * the modification count is increased by one. + * + * @return the number of times this object has been modified since instantiation. + */ + @Override + public int getModID() { + return modID + modIDadd + this.atmosphericConditions.getModID(); + } + + + @Override + public String toString() { + return String.format("FlightConditions[" + + "aoa=%.2f\u00b0," + + "theta=%.2f\u00b0," + + "mach=%.3f," + + "rollRate=%.2f," + + "pitchRate=%.2f," + + "yawRate=%.2f," + + "refLength=%.3f," + + "pitchCenter=" + pitchCenter.toString() + "," + + "atmosphericConditions=" + atmosphericConditions.toString() + + "]", + aoa * 180 / Math.PI, theta * 180 / Math.PI, mach, rollRate, pitchRate, yawRate, refLength); + } + + + /** + * Return a copy of the flight conditions. The copy has no listeners. The + * atmospheric conditions is also cloned. + */ + @Override + public FlightConditions clone() { + try { + FlightConditions cond = (FlightConditions) super.clone(); + cond.listenerList = new ArrayList(); + cond.event = new EventObject(cond); + cond.atmosphericConditions = atmosphericConditions.clone(); + return cond; + } catch (CloneNotSupportedException e) { + throw new BugException("clone not supported!", e); + } + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof FlightConditions)) + return false; + + FlightConditions other = (FlightConditions) obj; + + return (MathUtil.equals(this.refLength, other.refLength) && + MathUtil.equals(this.aoa, other.aoa) && + MathUtil.equals(this.theta, other.theta) && + MathUtil.equals(this.mach, other.mach) && + MathUtil.equals(this.rollRate, other.rollRate) && + MathUtil.equals(this.pitchRate, other.pitchRate) && + MathUtil.equals(this.yawRate, other.yawRate) && + this.pitchCenter.equals(other.pitchCenter) && this.atmosphericConditions.equals(other.atmosphericConditions)); + } + + @Override + public int hashCode() { + return (int) (1000 * (refLength + aoa + theta + mach + rollRate + pitchRate + yawRate)); + } + + + @Override + public void addChangeListener(EventListener listener) { + listenerList.add(0, listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listenerList.remove(listener); + } + + protected void fireChangeEvent() { + modID = UniqueID.next(); + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] listeners = listenerList.toArray(new EventListener[0]); + for (EventListener l : listeners) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(event); + } + } + } +} diff --git a/core/src/net/sf/openrocket/aerodynamics/Warning.java b/core/src/net/sf/openrocket/aerodynamics/Warning.java new file mode 100644 index 00000000..e44541c0 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/Warning.java @@ -0,0 +1,164 @@ +package net.sf.openrocket.aerodynamics; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public abstract class Warning { + + private static final Translator trans = Application.getTranslator(); + + /** + * Return a Warning with the specific text. + */ + public static Warning fromString(String text) { + return new Warning.Other(text); + } + + + /** + * Return true if the other warning should replace + * this warning. The method should return true if the other + * warning indicates a "worse" condition than the current warning. + * + * @param other the warning to compare to + * @return whether this warning should be replaced + */ + public abstract boolean replaceBy(Warning other); + + + /** + * Two Warnings are by default considered equal if they are of + * the same class. Therefore only one instance of a particular warning type + * is stored in a {@link WarningSet}. Subclasses may override this method for + * more specific functionality. + */ + @Override + public boolean equals(Object o) { + return (o.getClass() == this.getClass()); + } + + /** + * A hashCode method compatible with the equals method. + */ + @Override + public int hashCode() { + return this.getClass().hashCode(); + } + + + + + ///////////// Specific warning classes ///////////// + + + /** + * A Warning indicating a large angle of attack was encountered. + * + * @author Sampo Niskanen + */ + public static class LargeAOA extends Warning { + private double aoa; + + /** + * Sole constructor. The argument is the AOA that caused this warning. + * + * @param aoa the angle of attack that caused this warning + */ + public LargeAOA(double aoa) { + this.aoa = aoa; + } + + @Override + public String toString() { + if (Double.isNaN(aoa)) + //// Large angle of attack encountered. + return trans.get("Warning.LargeAOA.str1"); + //// Large angle of attack encountered ( + return (trans.get("Warning.LargeAOA.str2") + + UnitGroup.UNITS_ANGLE.getDefaultUnit().toString(aoa) + ")."); + } + + @Override + public boolean replaceBy(Warning other) { + if (!(other instanceof LargeAOA)) + return false; + + LargeAOA o = (LargeAOA)other; + if (Double.isNaN(this.aoa)) // If this has value NaN then replace + return true; + return (o.aoa > this.aoa); + } + } + + + + /** + * An unspecified warning type. This warning type holds a String + * describing it. Two warnings of this type are considered equal if the strings + * are identical. + * + * @author Sampo Niskanen + */ + public static class Other extends Warning { + private String description; + + public Other(String description) { + this.description = description; + } + + @Override + public String toString() { + return description; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof Other)) + return false; + + Other o = (Other)other; + return (o.description.equals(this.description)); + } + + @Override + public int hashCode() { + return description.hashCode(); + } + + @Override + public boolean replaceBy(Warning other) { + return false; + } + } + + + /** A Warning that the body diameter is discontinuous. */ +////Discontinuity in rocket body diameter. + public static final Warning DISCONTINUITY = + new Other(trans.get("Warning.DISCONTINUITY")); + + /** A Warning that the fins are thick compared to the rocket body. */ +////Thick fins may not be modeled accurately. + public static final Warning THICK_FIN = + new Other(trans.get("Warning.THICK_FIN")); + + /** A Warning that the fins have jagged edges. */ +////Jagged-edged fin predictions may be inaccurate. + public static final Warning JAGGED_EDGED_FIN = + new Other(trans.get("Warning.JAGGED_EDGED_FIN")); + + /** A Warning that simulation listeners have affected the simulation */ +////Listeners modified the flight simulation + public static final Warning LISTENERS_AFFECTED = + new Other(trans.get("Warning.LISTENERS_AFFECTED")); + +////Recovery device opened while motor still burning. + public static final Warning RECOVERY_DEPLOYMENT_WHILE_BURNING = + new Other(trans.get("Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING")); + + + //// Invalid parameter encountered, ignoring. + public static final Warning FILE_INVALID_PARAMETER = + new Other(trans.get("Warning.FILE_INVALID_PARAMETER")); +} diff --git a/core/src/net/sf/openrocket/aerodynamics/WarningSet.java b/core/src/net/sf/openrocket/aerodynamics/WarningSet.java new file mode 100644 index 00000000..c4e81c62 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/WarningSet.java @@ -0,0 +1,136 @@ +package net.sf.openrocket.aerodynamics; + +import java.util.AbstractSet; +import java.util.Iterator; + +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.Mutable; + +/** + * A set that contains multiple Warnings. When adding a + * {@link Warning} to this set, the contents is checked for a warning of the + * same type. If one is found, then the warning left in the set is determined + * by the method {@link Warning#replaceBy(Warning)}. + *

+ * A WarningSet can be made immutable by calling {@link #immute()}. + * + * @author Sampo Niskanen + */ +public class WarningSet extends AbstractSet implements Cloneable, Monitorable { + + private ArrayList warnings = new ArrayList(); + + private Mutable mutable = new Mutable(); + + private int modID = 0; + + /** + * Add a Warning to the set. If a warning of the same type + * exists in the set, the warning that is left in the set is defined by the + * method {@link Warning#replaceBy(Warning)}. + * + * @throws IllegalStateException if this warning set has been made immutable. + */ + @Override + public boolean add(Warning w) { + mutable.check(); + + modID++; + int index = warnings.indexOf(w); + + if (index < 0) { + warnings.add(w); + return false; + } + + Warning old = warnings.get(index); + if (old.replaceBy(w)) { + warnings.set(index, w); + } + + return true; + } + + /** + * Add a Warning with the specified text to the set. The Warning object + * is created using the {@link Warning#fromString(String)} method. If a warning of the + * same type exists in the set, the warning that is left in the set is defined by the + * method {@link Warning#replaceBy(Warning)}. + * + * @param s the warning text. + * @throws IllegalStateException if this warning set has been made immutable. + */ + public boolean add(String s) { + mutable.check(); + return add(Warning.fromString(s)); + } + + + @Override + public Iterator iterator() { + final Iterator iterator = warnings.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public Warning next() { + return iterator.next(); + } + + @Override + public void remove() { + mutable.check(); + iterator.remove(); + } + + }; + } + + @Override + public int size() { + return warnings.size(); + } + + + public void immute() { + mutable.immute(); + } + + + @Override + public WarningSet clone() { + try { + + WarningSet newSet = (WarningSet) super.clone(); + newSet.warnings = this.warnings.clone(); + newSet.mutable = this.mutable.clone(); + return newSet; + + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException occurred, report bug!", e); + } + } + + + @Override + public String toString() { + String s = ""; + + for (Warning w : warnings) { + if (s.length() > 0) + s = s + ","; + s += w.toString(); + } + return "WarningSet[" + s + "]"; + } + + @Override + public int getModID() { + return modID; + } +} diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java new file mode 100644 index 00000000..bd8ee75e --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java @@ -0,0 +1,776 @@ +package net.sf.openrocket.aerodynamics.barrowman; + +import static java.lang.Math.pow; +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.Arrays; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.LinearInterpolator; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.PolyInterpolator; + + +public class FinSetCalc extends RocketComponentCalc { + private static final double STALL_ANGLE = (20 * Math.PI / 180); + + /** Number of divisions in the fin chords. */ + protected static final int DIVISIONS = 48; + + + + protected double macLength = Double.NaN; // MAC length + protected double macLead = Double.NaN; // MAC leading edge position + protected double macSpan = Double.NaN; // MAC spanwise position + protected double finArea = Double.NaN; // Fin area + protected double ar = Double.NaN; // Fin aspect ratio + protected double span = Double.NaN; // Fin span + protected double cosGamma = Double.NaN; // Cosine of midchord sweep angle + protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle + protected double rollSum = Double.NaN; // Roll damping sum term + + protected int interferenceFinCount = -1; // No. of fins in interference + + protected double[] chordLead = new double[DIVISIONS]; + protected double[] chordTrail = new double[DIVISIONS]; + protected double[] chordLength = new double[DIVISIONS]; + + + protected final WarningSet geometryWarnings = new WarningSet(); + + private double[] poly = new double[6]; + + private final double thickness; + private final double bodyRadius; + private final int finCount; + private final double baseRotation; + private final double cantAngle; + private final FinSet.CrossSection crossSection; + + public FinSetCalc(RocketComponent component) { + super(component); + if (!(component instanceof FinSet)) { + throw new IllegalArgumentException("Illegal component type " + component); + } + + FinSet fin = (FinSet) component; + thickness = fin.getThickness(); + bodyRadius = fin.getBodyRadius(); + finCount = fin.getFinCount(); + baseRotation = fin.getBaseRotation(); + cantAngle = fin.getCantAngle(); + span = fin.getSpan(); + finArea = fin.getFinArea(); + crossSection = fin.getCrossSection(); + + calculateFinGeometry(fin); + calculatePoly(); + calculateInterferenceFinCount(fin); + } + + + /* + * Calculates the non-axial forces produced by the fins (normal and side forces, + * pitch, yaw and roll moments, CP position, CNa). + */ + @Override + public void calculateNonaxialForces(FlightConditions conditions, + AerodynamicForces forces, WarningSet warnings) { + + + if (span < 0.001) { + forces.setCm(0); + forces.setCN(0); + forces.setCNa(0); + forces.setCP(Coordinate.NUL); + forces.setCroll(0); + forces.setCrollDamp(0); + forces.setCrollForce(0); + forces.setCside(0); + forces.setCyaw(0); + return; + } + + + // Add warnings (radius/2 == diameter/4) + if (thickness > bodyRadius / 2) { + warnings.add(Warning.THICK_FIN); + } + warnings.addAll(geometryWarnings); + + + + //////// Calculate CNa. ///////// + + // One fin without interference (both sub- and supersonic): + double cna1 = calculateFinCNa1(conditions); + + + + // Multiple fins with fin-fin interference + double cna; + double theta = conditions.getTheta(); + double angle = baseRotation; + + + // Compute basic CNa without interference effects + if (finCount == 1 || finCount == 2) { + // Basic CNa from geometry + double mul = 0; + for (int i = 0; i < finCount; i++) { + mul += MathUtil.pow2(Math.sin(theta - angle)); + angle += 2 * Math.PI / finCount; + } + cna = cna1 * mul; + } else { + // Basic CNa assuming full efficiency + cna = cna1 * finCount / 2.0; + } + + + // Take into account fin-fin interference effects + switch (interferenceFinCount) { + case 1: + case 2: + case 3: + case 4: + // No interference effect + break; + + case 5: + cna *= 0.948; + break; + + case 6: + cna *= 0.913; + break; + + case 7: + cna *= 0.854; + break; + + case 8: + cna *= 0.81; + break; + + default: + // Assume 75% efficiency + cna *= 0.75; + warnings.add("Too many parallel fins"); + break; + } + + /* + * Used in 0.9.5 and earlier. Takes into account rotation angle for three + * and four fins, does not take into account interference from other fin sets. + * + switch (fins) { + case 1: + case 2: + // from geometry + double mul = 0; + for (int i=0; i < fins; i++) { + mul += MathUtil.pow2(Math.sin(theta - angle)); + angle += 2 * Math.PI / fins; + } + cna = cna1*mul; + break; + + case 3: + // multiplier 1.5, sinusoidal reduction of 15% + cna = cna1 * 1.5 * (1 - 0.15*pow2(Math.cos(1.5 * (theta-angle)))); + break; + + case 4: + // multiplier 2.0, sinusoidal reduction of 6% + cna = cna1 * 2.0 * (1 - 0.06*pow2(Math.sin(2 * (theta-angle)))); + break; + + case 5: + cna = 2.37 * cna1; + break; + + case 6: + cna = 2.74 * cna1; + break; + + case 7: + cna = 2.99 * cna1; + break; + + case 8: + cna = 3.24 * cna1; + break; + + default: + // Assume N/2 * 3/4 efficiency for more fins + cna = cna1 * fins * 3.0/8.0; + break; + } + */ + + // Body-fin interference effect + double r = bodyRadius; + double tau = r / (span + r); + if (Double.isNaN(tau) || Double.isInfinite(tau)) + tau = 0; + cna *= 1 + tau; // Classical Barrowman + // cna *= pow2(1 + tau); // Barrowman thesis (too optimistic??) + + + + // TODO: LOW: check for fin tip mach cone interference + // (Barrowman thesis pdf-page 40) + + // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25 + + + + // Calculate CP position + double x = macLead + calculateCPPos(conditions) * macLength; + + + + // Calculate roll forces, reduce forcing above stall angle + + // Without body-fin interference effect: + // forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() / + // conditions.getRefLength(); + // With body-fin interference effect: + forces.setCrollForce(finCount * (macSpan + r) * cna1 * (1 + tau) * cantAngle / conditions.getRefLength()); + + + + + if (conditions.getAOA() > STALL_ANGLE) { + // System.out.println("Fin stalling in roll"); + forces.setCrollForce(forces.getCrollForce() * MathUtil.clamp( + 1 - (conditions.getAOA() - STALL_ANGLE) / (STALL_ANGLE / 2), 0, 1)); + } + forces.setCrollDamp(calculateDampingMoment(conditions)); + forces.setCroll(forces.getCrollForce() - forces.getCrollDamp()); + + + + // System.out.printf(component.getName() + ": roll rate:%.3f force:%.3f damp:%.3f " + + // "total:%.3f\n", + // conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll); + + forces.setCNa(cna); + forces.setCN(cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE)); + forces.setCP(new Coordinate(x, 0, 0, cna)); + forces.setCm(forces.getCN() * x / conditions.getRefLength()); + + /* + * TODO: HIGH: Compute actual side force and yaw moment. + * This is not currently performed because it produces strange results for + * stable rockets that have two fins in the front part of the fuselage, + * where the rocket flies at an ever-increasing angle of attack. This may + * be due to incorrect computation of pitch/yaw damping moments. + */ + // if (fins == 1 || fins == 2) { + // forces.Cside = fins * cna1 * Math.cos(theta-angle) * Math.sin(theta-angle); + // forces.Cyaw = fins * forces.Cside * x / conditions.getRefLength(); + // } else { + // forces.Cside = 0; + // forces.Cyaw = 0; + // } + forces.setCside(0); + forces.setCyaw(0); + + + } + + + /** + * Returns the MAC length of the fin. This is required in the friction drag + * computation. + * + * @return the MAC length of the fin. + */ + public double getMACLength() { + return macLength; + } + + public double getMidchordPos() { + return macLead + 0.5 * macLength; + } + + + + /** + * Pre-calculates the fin geometry values. + */ + protected void calculateFinGeometry(FinSet component) { + + span = component.getSpan(); + finArea = component.getFinArea(); + ar = 2 * pow2(span) / finArea; + + Coordinate[] points = component.getFinPoints(); + + // Check for jagged edges + geometryWarnings.clear(); + boolean down = false; + for (int i = 1; i < points.length; i++) { + if ((points[i].y > points[i - 1].y + 0.001) && down) { + geometryWarnings.add(Warning.JAGGED_EDGED_FIN); + break; + } + if (points[i].y < points[i - 1].y - 0.001) { + down = true; + } + } + + + // Calculate the chord lead and trail positions and length + + Arrays.fill(chordLead, Double.POSITIVE_INFINITY); + Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY); + Arrays.fill(chordLength, 0); + + for (int point = 1; point < points.length; point++) { + double x1 = points[point - 1].x; + double y1 = points[point - 1].y; + double x2 = points[point].x; + double y2 = points[point].y; + + if (MathUtil.equals(y1, y2)) + continue; + + int i1 = (int) (y1 * 1.0001 / span * (DIVISIONS - 1)); + int i2 = (int) (y2 * 1.0001 / span * (DIVISIONS - 1)); + i1 = MathUtil.clamp(i1, 0, DIVISIONS - 1); + i2 = MathUtil.clamp(i2, 0, DIVISIONS - 1); + if (i1 > i2) { + int tmp = i2; + i2 = i1; + i1 = tmp; + } + + for (int i = i1; i <= i2; i++) { + // Intersection point (x,y) + double y = i * span / (DIVISIONS - 1); + double x = (y - y2) / (y1 - y2) * x1 + (y1 - y) / (y1 - y2) * x2; + if (x < chordLead[i]) + chordLead[i] = x; + if (x > chordTrail[i]) + chordTrail[i] = x; + + // TODO: LOW: If fin point exactly on chord line, might be counted twice: + if (y1 < y2) { + chordLength[i] -= x; + } else { + chordLength[i] += x; + } + } + } + + // Check and correct any inconsistencies + for (int i = 0; i < DIVISIONS; i++) { + if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) || + Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) { + chordLead[i] = 0; + chordTrail[i] = 0; + } + if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) { + chordLength[i] = 0; + } + if (chordLength[i] > chordTrail[i] - chordLead[i]) { + chordLength[i] = chordTrail[i] - chordLead[i]; + } + } + + + /* Calculate fin properties: + * + * macLength // MAC length + * macLead // MAC leading edge position + * macSpan // MAC spanwise position + * ar // Fin aspect ratio (already set) + * span // Fin span (already set) + */ + macLength = 0; + macLead = 0; + macSpan = 0; + cosGamma = 0; + cosGammaLead = 0; + rollSum = 0; + double area = 0; + double radius = component.getBodyRadius(); + + final double dy = span / (DIVISIONS - 1); + for (int i = 0; i < DIVISIONS; i++) { + double length = chordTrail[i] - chordLead[i]; + double y = i * dy; + + macLength += length * length; + macSpan += y * length; + macLead += chordLead[i] * length; + area += length; + rollSum += chordLength[i] * pow2(radius + y); + + if (i > 0) { + double dx = (chordTrail[i] + chordLead[i]) / 2 - (chordTrail[i - 1] + chordLead[i - 1]) / 2; + cosGamma += dy / MathUtil.hypot(dx, dy); + + dx = chordLead[i] - chordLead[i - 1]; + cosGammaLead += dy / MathUtil.hypot(dx, dy); + } + } + + macLength *= dy; + macSpan *= dy; + macLead *= dy; + area *= dy; + rollSum *= dy; + + macLength /= area; + macSpan /= area; + macLead /= area; + cosGamma /= (DIVISIONS - 1); + cosGammaLead /= (DIVISIONS - 1); + } + + + /////////////// CNa1 calculation //////////////// + + private static final double CNA_SUBSONIC = 0.9; + private static final double CNA_SUPERSONIC = 1.5; + private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC) - 1, 1.5); + private static final double GAMMA = 1.4; + private static final LinearInterpolator K1, K2, K3; + private static final PolyInterpolator cnaInterpolator = new PolyInterpolator( + new double[] { CNA_SUBSONIC, CNA_SUPERSONIC }, + new double[] { CNA_SUBSONIC, CNA_SUPERSONIC }, + new double[] { CNA_SUBSONIC } + ); + /* Pre-calculate the values for K1, K2 and K3 */ + static { + // Up to Mach 5 + int n = (int) ((5.0 - CNA_SUPERSONIC) * 10); + double[] x = new double[n]; + double[] k1 = new double[n]; + double[] k2 = new double[n]; + double[] k3 = new double[n]; + for (int i = 0; i < n; i++) { + double M = CNA_SUPERSONIC + i * 0.1; + double beta = MathUtil.safeSqrt(M * M - 1); + x[i] = M; + k1[i] = 2.0 / beta; + k2[i] = ((GAMMA + 1) * pow(M, 4) - 4 * pow2(beta)) / (4 * pow(beta, 4)); + k3[i] = ((GAMMA + 1) * pow(M, 8) + (2 * pow2(GAMMA) - 7 * GAMMA - 5) * pow(M, 6) + + 10 * (GAMMA + 1) * pow(M, 4) + 8) / (6 * pow(beta, 7)); + } + K1 = new LinearInterpolator(x, k1); + K2 = new LinearInterpolator(x, k2); + K3 = new LinearInterpolator(x, k3); + + // System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]); + // System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]); + // System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]); + } + + + protected double calculateFinCNa1(FlightConditions conditions) { + double mach = conditions.getMach(); + double ref = conditions.getRefArea(); + double alpha = MathUtil.min(conditions.getAOA(), + Math.PI - conditions.getAOA(), STALL_ANGLE); + + // Subsonic case + if (mach <= CNA_SUBSONIC) { + return 2 * Math.PI * pow2(span) / (1 + MathUtil.safeSqrt(1 + (1 - pow2(mach)) * + pow2(pow2(span) / (finArea * cosGamma)))) / ref; + } + + // Supersonic case + if (mach >= CNA_SUPERSONIC) { + return finArea * (K1.getValue(mach) + K2.getValue(mach) * alpha + + K3.getValue(mach) * pow2(alpha)) / ref; + } + + // Transonic case, interpolate + double subV, superV; + double subD, superD; + + double sq = MathUtil.safeSqrt(1 + (1 - pow2(CNA_SUBSONIC)) * pow2(span * span / (finArea * cosGamma))); + subV = 2 * Math.PI * pow2(span) / ref / (1 + sq); + subD = 2 * mach * Math.PI * pow(span, 6) / (pow2(finArea * cosGamma) * ref * + sq * pow2(1 + sq)); + + superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC) * alpha + + K3.getValue(CNA_SUPERSONIC) * pow2(alpha)) / ref; + superD = -finArea / ref * 2 * CNA_SUPERSONIC / CNA_SUPERSONIC_B; + + // System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD); + + return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0); + } + + + + + private double calculateDampingMoment(FlightConditions conditions) { + double rollRate = conditions.getRollRate(); + + if (Math.abs(rollRate) < 0.1) + return 0; + + double mach = conditions.getMach(); + double absRate = Math.abs(rollRate); + + + /* + * At low speeds and relatively large roll rates (i.e. near apogee) the + * fin tips rotate well above stall angle. In this case sum the chords + * separately. + */ + if (absRate * (bodyRadius + span) / conditions.getVelocity() > 15 * Math.PI / 180) { + double sum = 0; + for (int i = 0; i < DIVISIONS; i++) { + double dist = bodyRadius + span * i / DIVISIONS; + double aoa = Math.min(absRate * dist / conditions.getVelocity(), 15 * Math.PI / 180); + sum += chordLength[i] * dist * aoa; + } + sum = sum * (span / DIVISIONS) * 2 * Math.PI / conditions.getBeta() / + (conditions.getRefArea() * conditions.getRefLength()); + + // System.out.println("SPECIAL: " + + // (MathUtil.sign(rollRate) *component.getFinCount() * sum)); + return MathUtil.sign(rollRate) * finCount * sum; + } + + + + if (mach <= CNA_SUBSONIC) { + // System.out.println("BASIC: "+ + // (component.getFinCount() * 2*Math.PI * rollRate * rollSum / + // (conditions.getRefArea() * conditions.getRefLength() * + // conditions.getVelocity() * conditions.getBeta()))); + + return finCount * 2 * Math.PI * rollRate * rollSum / + (conditions.getRefArea() * conditions.getRefLength() * + conditions.getVelocity() * conditions.getBeta()); + } + if (mach >= CNA_SUPERSONIC) { + + double vel = conditions.getVelocity(); + double k1 = K1.getValue(mach); + double k2 = K2.getValue(mach); + double k3 = K3.getValue(mach); + + double sum = 0; + + for (int i = 0; i < DIVISIONS; i++) { + double y = i * span / (DIVISIONS - 1); + double angle = rollRate * (bodyRadius + y) / vel; + + sum += (k1 * angle + k2 * angle * angle + k3 * angle * angle * angle) + * chordLength[i] * (bodyRadius + y); + } + + return finCount * sum * span / (DIVISIONS - 1) / + (conditions.getRefArea() * conditions.getRefLength()); + } + + // Transonic, do linear interpolation + + FlightConditions cond = conditions.clone(); + cond.setMach(CNA_SUBSONIC - 0.01); + double subsonic = calculateDampingMoment(cond); + cond.setMach(CNA_SUPERSONIC + 0.01); + double supersonic = calculateDampingMoment(cond); + + return subsonic * (CNA_SUPERSONIC - mach) / (CNA_SUPERSONIC - CNA_SUBSONIC) + + supersonic * (mach - CNA_SUBSONIC) / (CNA_SUPERSONIC - CNA_SUBSONIC); + } + + + + + /** + * Return the relative position of the CP along the mean aerodynamic chord. + * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an + * empirical formula, between these two using an interpolation polynomial. + * + * @param cond Mach speed used + * @return CP position along the MAC + */ + private double calculateCPPos(FlightConditions cond) { + double m = cond.getMach(); + if (m <= 0.5) { + // At subsonic speeds CP at quarter chord + return 0.25; + } + if (m >= 2) { + // At supersonic speeds use empirical formula + double beta = cond.getBeta(); + return (ar * beta - 0.67) / (2 * ar * beta - 1); + } + + // In between use interpolation polynomial + double x = 1.0; + double val = 0; + + for (int i = 0; i < poly.length; i++) { + val += poly[i] * x; + x *= m; + } + + return val; + } + + /** + * Calculate CP position interpolation polynomial coefficients from the + * fin geometry. This is a fifth order polynomial that satisfies + * + * p(0.5)=0.25 + * p'(0.5)=0 + * p(2) = f(2) + * p'(2) = f'(2) + * p''(2) = 0 + * p'''(2) = 0 + * + * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1). + * + * The values were calculated analytically in Mathematica. The coefficients + * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ... + */ + private void calculatePoly() { + double denom = pow2(1 - 3.4641 * ar); // common denominator + + poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom; + poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom; + poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom; + poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom; + poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom; + poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom; + } + + + // @SuppressWarnings("null") + // public static void main(String arg[]) { + // Rocket rocket = TestRocket.makeRocket(); + // FinSet finset = null; + // + // Iterator iter = rocket.deepIterator(); + // while (iter.hasNext()) { + // RocketComponent c = iter.next(); + // if (c instanceof FinSet) { + // finset = (FinSet)c; + // break; + // } + // } + // + // ((TrapezoidFinSet)finset).setHeight(0.10); + // ((TrapezoidFinSet)finset).setRootChord(0.10); + // ((TrapezoidFinSet)finset).setTipChord(0.10); + // ((TrapezoidFinSet)finset).setSweep(0.0); + // + // + // FinSetCalc calc = new FinSetCalc(finset); + // + // calc.calculateFinGeometry(); + // FlightConditions cond = new FlightConditions(new Configuration(rocket)); + // for (double m=0; m < 3; m+=0.05) { + // cond.setMach(m); + // cond.setAOA(0.0*Math.PI/180); + // double cna = calc.calculateFinCNa1(cond); + // System.out.printf("%5.2f "+cna+"\n", m); + // } + // + // } + + + @Override + public double calculatePressureDragForce(FlightConditions conditions, + double stagnationCD, double baseCD, WarningSet warnings) { + + double mach = conditions.getMach(); + double drag = 0; + + // Pressure fore-drag + if (crossSection == FinSet.CrossSection.AIRFOIL || + crossSection == FinSet.CrossSection.ROUNDED) { + + // Round leading edge + if (mach < 0.9) { + drag = Math.pow(1 - pow2(mach), -0.417) - 1; + } else if (mach < 1) { + drag = 1 - 1.785 * (mach - 0.9); + } else { + drag = 1.214 - 0.502 / pow2(mach) + 0.1095 / pow2(pow2(mach)); + } + + } else if (crossSection == FinSet.CrossSection.SQUARE) { + drag = stagnationCD; + } else { + throw new UnsupportedOperationException("Unsupported fin profile: " + crossSection); + } + + // Slanted leading edge + drag *= pow2(cosGammaLead); + + // Trailing edge drag + if (crossSection == FinSet.CrossSection.SQUARE) { + drag += baseCD; + } else if (crossSection == FinSet.CrossSection.ROUNDED) { + drag += baseCD / 2; + } + // Airfoil assumed to have zero base drag + + + // Scale to correct reference area + drag *= finCount * span * thickness / conditions.getRefArea(); + + return drag; + } + + + private void calculateInterferenceFinCount(FinSet component) { + RocketComponent parent = component.getParent(); + if (parent == null) { + throw new IllegalStateException("fin set without parent component"); + } + + double lead = component.toRelative(Coordinate.NUL, parent)[0].x; + double trail = component.toRelative(new Coordinate(component.getLength()), + parent)[0].x; + + /* + * The counting fails if the fin root chord is very small, in that case assume + * no other fin interference than this fin set. + */ + if (trail - lead < 0.007) { + interferenceFinCount = finCount; + } else { + interferenceFinCount = 0; + for (RocketComponent c : parent.getChildren()) { + if (c instanceof FinSet) { + double finLead = c.toRelative(Coordinate.NUL, parent)[0].x; + double finTrail = c.toRelative(new Coordinate(c.getLength()), parent)[0].x; + + // Compute overlap of the fins + + if ((finLead < trail - 0.005) && (finTrail > lead + 0.005)) { + interferenceFinCount += ((FinSet) c).getFinCount(); + } + } + } + } + if (interferenceFinCount < component.getFinCount()) { + throw new BugException("Counted " + interferenceFinCount + " parallel fins, " + + "when component itself has " + component.getFinCount() + + ", fin points=" + Arrays.toString(component.getFinPoints())); + } + } + +} diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java new file mode 100644 index 00000000..cd7fea01 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java @@ -0,0 +1,39 @@ +package net.sf.openrocket.aerodynamics.barrowman; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.MathUtil; + +public class LaunchLugCalc extends RocketComponentCalc { + + private double CDmul; + private double refArea; + + public LaunchLugCalc(RocketComponent component) { + super(component); + + LaunchLug lug = (LaunchLug)component; + double ld = lug.getLength() / (2*lug.getOuterRadius()); + + CDmul = Math.max(1.3 - ld, 1); + refArea = Math.PI * MathUtil.pow2(lug.getOuterRadius()) - + Math.PI * MathUtil.pow2(lug.getInnerRadius()) * Math.max(1 - ld, 0); + } + + @Override + public void calculateNonaxialForces(FlightConditions conditions, + AerodynamicForces forces, WarningSet warnings) { + // Nothing to be done + } + + @Override + public double calculatePressureDragForce(FlightConditions conditions, + double stagnationCD, double baseCD, WarningSet warnings) { + + return CDmul*stagnationCD * refArea / conditions.getRefArea(); + } + +} diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java new file mode 100644 index 00000000..71f4ef0c --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java @@ -0,0 +1,42 @@ +package net.sf.openrocket.aerodynamics.barrowman; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; + +public abstract class RocketComponentCalc { + + public RocketComponentCalc(RocketComponent component) { + + } + + /** + * Calculate the non-axial forces produced by the component (normal and side forces, + * pitch, yaw and roll moments and CP position). The values are stored in the + * AerodynamicForces object. Additionally the value of CNa is computed + * and stored if possible without large amount of extra calculation, otherwise + * NaN is stored. The CP coordinate is stored in local coordinates and moments are + * computed around the local origin. + * + * @param conditions the flight conditions. + * @param forces the object in which to store the values. + * @param warnings set in which to store possible warnings. + */ + public abstract void calculateNonaxialForces(FlightConditions conditions, + AerodynamicForces forces, WarningSet warnings); + + + /** + * Calculates the pressure drag of the component. This component does NOT include + * the effect of discontinuities in the rocket body. + * + * @param conditions the flight conditions. + * @param stagnationCD the current stagnation drag coefficient + * @param baseCD the current base drag coefficient + * @param warnings set in which to store possible warnings + * @return the pressure drag of the component + */ + public abstract double calculatePressureDragForce(FlightConditions conditions, + double stagnationCD, double baseCD, WarningSet warnings); +} diff --git a/core/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java b/core/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java new file mode 100644 index 00000000..23178a33 --- /dev/null +++ b/core/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java @@ -0,0 +1,440 @@ +package net.sf.openrocket.aerodynamics.barrowman; + +import static net.sf.openrocket.models.atmosphere.AtmosphericConditions.GAMMA; +import static net.sf.openrocket.util.MathUtil.pow2; +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.LinearInterpolator; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.PolyInterpolator; + + + +/** + * Calculates the aerodynamic properties of a SymmetricComponent. + *

+ * CP and CNa are calculated by the Barrowman method extended to account for body lift + * by the method presented by Galejs. Supersonic CNa and CP are assumed to be the + * same as the subsonic values. + * + * + * @author Sampo Niskanen + */ +public class SymmetricComponentCalc extends RocketComponentCalc { + + public static final double BODY_LIFT_K = 1.1; + + private final double length; + private final double foreRadius, aftRadius; + private final double fineness; + private final Transition.Shape shape; + private final double param; + private final double frontalArea; + private final double fullVolume; + private final double planformArea, planformCenter; + private final double sinphi; + + public SymmetricComponentCalc(RocketComponent c) { + super(c); + if (!(c instanceof SymmetricComponent)) { + throw new IllegalArgumentException("Illegal component type " + c); + } + SymmetricComponent component = (SymmetricComponent) c; + + + length = component.getLength(); + foreRadius = component.getForeRadius(); + aftRadius = component.getAftRadius(); + + fineness = length / (2 * Math.abs(aftRadius - foreRadius)); + fullVolume = component.getFullVolume(); + planformArea = component.getComponentPlanformArea(); + planformCenter = component.getComponentPlanformCenter(); + + if (component instanceof BodyTube) { + shape = null; + param = 0; + frontalArea = 0; + sinphi = 0; + } else if (component instanceof Transition) { + shape = ((Transition) component).getType(); + param = ((Transition) component).getShapeParameter(); + frontalArea = Math.abs(Math.PI * (foreRadius * foreRadius - aftRadius * aftRadius)); + + double r = component.getRadius(0.99 * length); + sinphi = (aftRadius - r) / MathUtil.hypot(aftRadius - r, 0.01 * length); + } else { + throw new UnsupportedOperationException("Unknown component type " + + component.getComponentName()); + } + } + + + private boolean isTube = false; + private double cnaCache = Double.NaN; + private double cpCache = Double.NaN; + + + /** + * Calculates the non-axial forces produced by the fins (normal and side forces, + * pitch, yaw and roll moments, CP position, CNa). + *

+ * This method uses the Barrowman method for CP and CNa calculation and the + * extension presented by Galejs for the effect of body lift. + *

+ * The CP and CNa at supersonic speeds are assumed to be the same as those at + * subsonic speeds. + */ + @Override + public void calculateNonaxialForces(FlightConditions conditions, + AerodynamicForces forces, WarningSet warnings) { + + // Pre-calculate and store the results + if (Double.isNaN(cnaCache)) { + final double r0 = foreRadius; + final double r1 = aftRadius; + + if (MathUtil.equals(r0, r1)) { + isTube = true; + cnaCache = 0; + } else { + isTube = false; + + final double A0 = Math.PI * pow2(r0); + final double A1 = Math.PI * pow2(r1); + + cnaCache = 2 * (A1 - A0); + // System.out.println("cnaCache = " + cnaCache); + cpCache = (length * A1 - fullVolume) / (A1 - A0); + } + } + + Coordinate cp; + + // If fore == aft, only body lift is encountered + if (isTube) { + cp = getLiftCP(conditions, warnings); + } else { + cp = new Coordinate(cpCache, 0, 0, cnaCache * conditions.getSincAOA() / + conditions.getRefArea()).average(getLiftCP(conditions, warnings)); + } + + forces.setCP(cp); + forces.setCNa(cp.weight); + forces.setCN(forces.getCNa() * conditions.getAOA()); + forces.setCm(forces.getCN() * cp.x / conditions.getRefLength()); + forces.setCroll(0); + forces.setCrollDamp(0); + forces.setCrollForce(0); + forces.setCside(0); + forces.setCyaw(0); + + + // Add warning on supersonic flight + if (conditions.getMach() > 1.1) { + warnings.add("Body calculations may not be entirely accurate at supersonic speeds."); + } + + } + + + + /** + * Calculate the body lift effect according to Galejs. + */ + protected Coordinate getLiftCP(FlightConditions conditions, WarningSet warnings) { + + /* + * Without this extra multiplier the rocket may become unstable at apogee + * when turning around, and begin oscillating horizontally. During the flight + * of the rocket this has no effect. It is effective only when AOA > 45 deg + * and the velocity is less than 15 m/s. + * + * TODO: MEDIUM: This causes an anomaly to the flight results with the CP jumping at apogee + */ + double mul = 1; + if ((conditions.getMach() < 0.05) && (conditions.getAOA() > Math.PI / 4)) { + mul = pow2(conditions.getMach() / 0.05); + } + + return new Coordinate(planformCenter, 0, 0, mul * BODY_LIFT_K * planformArea / conditions.getRefArea() * + conditions.getSinAOA() * conditions.getSincAOA()); // sin(aoa)^2 / aoa + } + + + + private LinearInterpolator interpolator = null; + + @Override + public double calculatePressureDragForce(FlightConditions conditions, + double stagnationCD, double baseCD, WarningSet warnings) { + + // Check for simple cases first + if (MathUtil.equals(foreRadius, aftRadius)) + return 0; + + if (length < 0.001) { + if (foreRadius < aftRadius) { + return stagnationCD * frontalArea / conditions.getRefArea(); + } else { + return baseCD * frontalArea / conditions.getRefArea(); + } + } + + + // Boattail drag computed directly from base drag + if (aftRadius < foreRadius) { + if (fineness >= 3) + return 0; + double cd = baseCD * frontalArea / conditions.getRefArea(); + if (fineness <= 1) + return cd; + return cd * (3 - fineness) / 2; + } + + + // All nose cones and shoulders from pre-calculated and interpolating + if (interpolator == null) { + calculateNoseInterpolator(); + } + + return interpolator.getValue(conditions.getMach()) * frontalArea / conditions.getRefArea(); + } + + + + /* + * Experimental values of pressure drag for different nose cone shapes with a fineness + * ratio of 3. The data is taken from 'Collection of Zero-Lift Drag Data on Bodies + * of Revolution from Free-Flight Investigations', NASA TR-R-100, NTRS 19630004995, + * page 16. + * + * This data is extrapolated for other fineness ratios. + */ + + private static final LinearInterpolator ellipsoidInterpolator = new LinearInterpolator( + new double[] { 1.2, 1.25, 1.3, 1.4, 1.6, 2.0, 2.4 }, + new double[] { 0.110, 0.128, 0.140, 0.148, 0.152, 0.159, 0.162 /* constant */} + ); + private static final LinearInterpolator x14Interpolator = new LinearInterpolator( + new double[] { 1.2, 1.3, 1.4, 1.6, 1.8, 2.2, 2.6, 3.0, 3.6 }, + new double[] { 0.140, 0.156, 0.169, 0.192, 0.206, 0.227, 0.241, 0.249, 0.252 } + ); + private static final LinearInterpolator x12Interpolator = new LinearInterpolator( + new double[] { 0.925, 0.95, 1.0, 1.05, 1.1, 1.2, 1.3, 1.7, 2.0 }, + new double[] { 0, 0.014, 0.050, 0.060, 0.059, 0.081, 0.084, 0.085, 0.078 } + ); + private static final LinearInterpolator x34Interpolator = new LinearInterpolator( + new double[] { 0.8, 0.9, 1.0, 1.06, 1.2, 1.4, 1.6, 2.0, 2.8, 3.4 }, + new double[] { 0, 0.015, 0.078, 0.121, 0.110, 0.098, 0.090, 0.084, 0.078, 0.074 } + ); + private static final LinearInterpolator vonKarmanInterpolator = new LinearInterpolator( + new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.6, 2.0, 3.0 }, + new double[] { 0, 0.010, 0.027, 0.055, 0.070, 0.081, 0.095, 0.097, 0.091, 0.083 } + ); + private static final LinearInterpolator lvHaackInterpolator = new LinearInterpolator( + new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.6, 2.0 }, + new double[] { 0, 0.010, 0.024, 0.066, 0.084, 0.100, 0.114, 0.117, 0.113 } + ); + private static final LinearInterpolator parabolicInterpolator = new LinearInterpolator( + new double[] { 0.95, 0.975, 1.0, 1.05, 1.1, 1.2, 1.4, 1.7 }, + new double[] { 0, 0.016, 0.041, 0.092, 0.109, 0.119, 0.113, 0.108 } + ); + private static final LinearInterpolator parabolic12Interpolator = new LinearInterpolator( + new double[] { 0.8, 0.9, 0.95, 1.0, 1.05, 1.1, 1.3, 1.5, 1.8 }, + new double[] { 0, 0.016, 0.042, 0.100, 0.126, 0.125, 0.100, 0.090, 0.088 } + ); + private static final LinearInterpolator parabolic34Interpolator = new LinearInterpolator( + new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.7 }, + new double[] { 0, 0.023, 0.073, 0.098, 0.107, 0.106, 0.089, 0.082 } + ); + private static final LinearInterpolator bluntInterpolator = new LinearInterpolator(); + static { + for (double m = 0; m < 3; m += 0.05) + bluntInterpolator.addPoint(m, BarrowmanCalculator.calculateStagnationCD(m)); + } + + /** + * Calculate the LinearInterpolator 'interpolator'. After this call, if can be used + * to get the pressure drag coefficient at any Mach number. + * + * First, the transonic/supersonic region is computed. For conical and ogive shapes + * this is calculated directly. For other shapes, the values for fineness-ratio 3 + * transitions are taken from the experimental values stored above (for parameterized + * shapes the values are interpolated between the parameter values). These are then + * extrapolated to the current fineness ratio. + * + * Finally, if the first data points in the interpolator are not zero, the subsonic + * region is interpolated in the form Cd = a*M^b + Cd(M=0). + */ + @SuppressWarnings("null") + private void calculateNoseInterpolator() { + LinearInterpolator int1 = null, int2 = null; + double p = 0; + + interpolator = new LinearInterpolator(); + + + /* + * Take into account nose cone shape. Conical and ogive generate the interpolator + * directly. Others store a interpolator for fineness ratio 3 into int1, or + * for parameterized shapes store the bounding fineness ratio 3 interpolators into + * int1 and int2 and set 0 <= p <= 1 according to the bounds. + */ + switch (shape) { + case CONICAL: + interpolator = calculateOgiveNoseInterpolator(0, sinphi); // param==0 -> conical + break; + + case OGIVE: + interpolator = calculateOgiveNoseInterpolator(param, sinphi); + break; + + case ELLIPSOID: + int1 = ellipsoidInterpolator; + break; + + case POWER: + if (param <= 0.25) { + int1 = bluntInterpolator; + int2 = x14Interpolator; + p = param * 4; + } else if (param <= 0.5) { + int1 = x14Interpolator; + int2 = x12Interpolator; + p = (param - 0.25) * 4; + } else if (param <= 0.75) { + int1 = x12Interpolator; + int2 = x34Interpolator; + p = (param - 0.5) * 4; + } else { + int1 = x34Interpolator; + int2 = calculateOgiveNoseInterpolator(0, 1 / MathUtil.safeSqrt(1 + 4 * pow2(fineness))); + p = (param - 0.75) * 4; + } + break; + + case PARABOLIC: + if (param <= 0.5) { + int1 = calculateOgiveNoseInterpolator(0, 1 / MathUtil.safeSqrt(1 + 4 * pow2(fineness))); + int2 = parabolic12Interpolator; + p = param * 2; + } else if (param <= 0.75) { + int1 = parabolic12Interpolator; + int2 = parabolic34Interpolator; + p = (param - 0.5) * 4; + } else { + int1 = parabolic34Interpolator; + int2 = parabolicInterpolator; + p = (param - 0.75) * 4; + } + break; + + case HAACK: + int1 = vonKarmanInterpolator; + int2 = lvHaackInterpolator; + p = param * 3; + break; + + default: + throw new UnsupportedOperationException("Unknown transition shape: " + shape); + } + + if (p < 0 || p > 1.00001) { + throw new BugException("Inconsistent parameter value p=" + p + " shape=" + shape); + } + + + // Check for parameterized shape and interpolate if necessary + if (int2 != null) { + LinearInterpolator int3 = new LinearInterpolator(); + for (double m : int1.getXPoints()) { + int3.addPoint(m, p * int2.getValue(m) + (1 - p) * int1.getValue(m)); + } + for (double m : int2.getXPoints()) { + int3.addPoint(m, p * int2.getValue(m) + (1 - p) * int1.getValue(m)); + } + int1 = int3; + } + + // Extrapolate for fineness ratio if necessary + if (int1 != null) { + double log4 = Math.log(fineness + 1) / Math.log(4); + for (double m : int1.getXPoints()) { + double stag = bluntInterpolator.getValue(m); + interpolator.addPoint(m, stag * Math.pow(int1.getValue(m) / stag, log4)); + } + } + + + /* + * Now the transonic/supersonic region is ok. We still need to interpolate + * the subsonic region, if the values are non-zero. + */ + + double min = interpolator.getXPoints()[0]; + double minValue = interpolator.getValue(min); + if (minValue < 0.001) { + // No interpolation necessary + return; + } + + double cdMach0 = 0.8 * pow2(sinphi); + double minDeriv = (interpolator.getValue(min + 0.01) - minValue) / 0.01; + + // These should not occur, but might cause havoc for the interpolation + if ((cdMach0 >= minValue - 0.01) || (minDeriv <= 0.01)) { + return; + } + + // Cd = a*M^b + cdMach0 + double a = minValue - cdMach0; + double b = minDeriv / a; + + for (double m = 0; m < minValue; m += 0.05) { + interpolator.addPoint(m, a * Math.pow(m, b) + cdMach0); + } + } + + + private static final PolyInterpolator conicalPolyInterpolator = + new PolyInterpolator(new double[] { 1.0, 1.3 }, new double[] { 1.0, 1.3 }); + + private static LinearInterpolator calculateOgiveNoseInterpolator(double param, + double sinphi) { + LinearInterpolator interpolator = new LinearInterpolator(); + + // In the range M = 1 ... 1.3 use polynomial approximation + double cdMach1 = 2.1 * pow2(sinphi) + 0.6019 * sinphi; + + double[] poly = conicalPolyInterpolator.interpolator( + 1.0 * sinphi, cdMach1, + 4 / (GAMMA + 1) * (1 - 0.5 * cdMach1), -1.1341 * sinphi + ); + + // Shape parameter multiplier + double mul = 0.72 * pow2(param - 0.5) + 0.82; + + for (double m = 1; m < 1.3001; m += 0.02) { + interpolator.addPoint(m, mul * PolyInterpolator.eval(m, poly)); + } + + // Above M = 1.3 use direct formula + for (double m = 1.32; m < 4; m += 0.02) { + interpolator.addPoint(m, mul * (2.1 * pow2(sinphi) + 0.5 * sinphi / MathUtil.safeSqrt(m * m - 1))); + } + + return interpolator; + } + + + +} diff --git a/core/src/net/sf/openrocket/arch/SystemInfo.java b/core/src/net/sf/openrocket/arch/SystemInfo.java new file mode 100644 index 00000000..7b526f2c --- /dev/null +++ b/core/src/net/sf/openrocket/arch/SystemInfo.java @@ -0,0 +1,83 @@ +package net.sf.openrocket.arch; + +import java.io.File; + +import net.sf.openrocket.util.BugException; + +public class SystemInfo { + + + /** + * Enumeration of supported operating systems. + * + * @see JNLP os and arch Value Collection + * @author Sampo Niskanen + */ + public enum Platform { + WINDOWS, + MAC_OS, + UNIX; + } + + + /** + * Return the current operating system. + * + * @return the operating system of the current system. + */ + public static Platform getPlatform() { + String os = System.getProperty("os.name").toLowerCase(); + + if (os.indexOf("win") >= 0) { + return Platform.WINDOWS; + } else if (os.indexOf("mac") >= 0) { + return Platform.MAC_OS; + } else { + /* + * Assume UNIX otherwise, e.g. "Linux", "Solaris", "AIX" etc. + */ + return Platform.UNIX; + } + } + + + + + /** + * Return the application data directory of this user. The location depends + * on the current platform. + *

+ * The directory will not be created by this method. + * + * @return the application directory for OpenRocket + */ + public static File getUserApplicationDirectory() { + final String homeDir = System.getProperty("user.home"); + final File dir; + + switch (getPlatform()) { + case WINDOWS: + String appdata = System.getenv("APPDATA"); + if (appdata != null) { + dir = new File(appdata, "OpenRocket/"); + } else { + dir = new File(homeDir, "OpenRocket/"); + } + break; + + case MAC_OS: + dir = new File(homeDir, "Library/Application Support/OpenRocket/"); + break; + + case UNIX: + dir = new File(homeDir, ".openrocket/"); + break; + + default: + throw new BugException("Not implemented for platform " + getPlatform()); + } + + return dir; + } + +} diff --git a/core/src/net/sf/openrocket/communication/BugReporter.java b/core/src/net/sf/openrocket/communication/BugReporter.java new file mode 100644 index 00000000..7f081ba9 --- /dev/null +++ b/core/src/net/sf/openrocket/communication/BugReporter.java @@ -0,0 +1,58 @@ +package net.sf.openrocket.communication; + +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; + +import net.sf.openrocket.util.BuildProperties; + +public class BugReporter extends Communicator { + + // Inhibit instantiation + private BugReporter() { + } + + + /** + * Send the provided report to the OpenRocket bug report URL. If the connection + * fails or the server does not respond with the correct response code, an + * exception is thrown. + * + * @param report the report to send. + * @throws IOException if an error occurs while connecting to the server or + * the server responds with a wrong response code. + */ + public static void sendBugReport(String report) throws IOException { + + HttpURLConnection connection = connectionSource.getConnection(BUG_REPORT_URL); + + connection.setConnectTimeout(CONNECTION_TIMEOUT); + connection.setInstanceFollowRedirects(true); + connection.setRequestMethod("POST"); + connection.setUseCaches(false); + connection.setRequestProperty("X-OpenRocket-Version", encode(BuildProperties.getVersion())); + + String post; + post = (VERSION_PARAM + "=" + encode(BuildProperties.getVersion()) + + "&" + BUG_REPORT_PARAM + "=" + encode(report)); + + OutputStreamWriter wr = null; + try { + // Send post information + connection.setDoOutput(true); + wr = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); + wr.write(post); + wr.flush(); + + if (connection.getResponseCode() != BUG_REPORT_RESPONSE_CODE) { + throw new IOException("Server responded with code " + + connection.getResponseCode() + ", expecting " + BUG_REPORT_RESPONSE_CODE); + } + } finally { + if (wr != null) + wr.close(); + connection.disconnect(); + } + } + +} diff --git a/core/src/net/sf/openrocket/communication/Communicator.java b/core/src/net/sf/openrocket/communication/Communicator.java new file mode 100644 index 00000000..06c666b7 --- /dev/null +++ b/core/src/net/sf/openrocket/communication/Communicator.java @@ -0,0 +1,74 @@ +package net.sf.openrocket.communication; + +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URLEncoder; + +import net.sf.openrocket.util.BugException; + +public abstract class Communicator { + + protected static final String BUG_REPORT_URL; + + protected static final String UPDATE_INFO_URL; + + static { + String url; + url = System.getProperty("openrocket.debug.bugurl"); + if (url == null) + url = "http://openrocket.sourceforge.net/actions/reportbug"; + BUG_REPORT_URL = url; + + url = System.getProperty("openrocket.debug.updateurl"); + if (url == null) + url = "http://openrocket.sourceforge.net/actions/updates"; + UPDATE_INFO_URL = url; + } + + + protected static final String VERSION_PARAM = "version"; + + + protected static final String BUG_REPORT_PARAM = "content"; + protected static final int BUG_REPORT_RESPONSE_CODE = HttpURLConnection.HTTP_ACCEPTED; + protected static final int CONNECTION_TIMEOUT = 10000; // in milliseconds + + protected static final int UPDATE_INFO_UPDATE_AVAILABLE = HttpURLConnection.HTTP_OK; + protected static final int UPDATE_INFO_NO_UPDATE_CODE = HttpURLConnection.HTTP_NO_CONTENT; + protected static final String UPDATE_INFO_CONTENT_TYPE = "text/plain"; + + // Limit the number of bytes that can be read from the server + protected static final int MAX_INPUT_BYTES = 20000; + + + protected static ConnectionSource connectionSource = new DefaultConnectionSource(); + + + /** + * Set the source of the network connections. This can be used for unit testing. + * By default the source is a DefaultConnectionSource. + * + * @param source the source of the connections. + */ + public static void setConnectionSource(ConnectionSource source) { + connectionSource = source; + } + + + /** + * URL-encode the specified string in UTF-8 encoding. + * + * @param str the string to encode (null ok) + * @return the encoded string or "null" + */ + public static String encode(String str) { + if (str == null) + return "null"; + try { + return URLEncoder.encode(str, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new BugException("Unsupported encoding UTF-8", e); + } + } + +} diff --git a/core/src/net/sf/openrocket/communication/ConnectionSource.java b/core/src/net/sf/openrocket/communication/ConnectionSource.java new file mode 100644 index 00000000..b5b0fdcc --- /dev/null +++ b/core/src/net/sf/openrocket/communication/ConnectionSource.java @@ -0,0 +1,21 @@ +package net.sf.openrocket.communication; + +import java.io.IOException; +import java.net.HttpURLConnection; + +/** + * A source for network connections. This interface exists to enable unit testing. + * + * @author Sampo Niskanen + */ +public interface ConnectionSource { + + /** + * Return a connection to the specified url. + * @param url the URL to connect to. + * @return the corresponding HttpURLConnection + * @throws IOException if an IOException occurs + */ + public HttpURLConnection getConnection(String url) throws IOException; + +} diff --git a/core/src/net/sf/openrocket/communication/DefaultConnectionSource.java b/core/src/net/sf/openrocket/communication/DefaultConnectionSource.java new file mode 100644 index 00000000..501c37ac --- /dev/null +++ b/core/src/net/sf/openrocket/communication/DefaultConnectionSource.java @@ -0,0 +1,21 @@ +package net.sf.openrocket.communication; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * Default implementation of ConnectionSource, which simply opens a new + * HttpURLConnection from a URL object. + * + * @author Sampo Niskanen + */ +public class DefaultConnectionSource implements ConnectionSource { + + @Override + public HttpURLConnection getConnection(String urlString) throws IOException { + URL url = new URL(urlString); + return (HttpURLConnection) url.openConnection(); + } + +} diff --git a/core/src/net/sf/openrocket/communication/UpdateInfo.java b/core/src/net/sf/openrocket/communication/UpdateInfo.java new file mode 100644 index 00000000..78458b81 --- /dev/null +++ b/core/src/net/sf/openrocket/communication/UpdateInfo.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.communication; + +import java.util.List; + +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.ComparablePair; + +public class UpdateInfo { + + private final String latestVersion; + + private final ArrayList> updates; + + + public UpdateInfo() { + this.latestVersion = BuildProperties.getVersion(); + this.updates = new ArrayList>(); + } + + public UpdateInfo(String version, List> updates) { + this.latestVersion = version; + this.updates = new ArrayList>(updates); + } + + + + /** + * Get the latest OpenRocket version. If it is the current version, then the value + * of {@link BuildProperties#getVersion()} is returned. + * + * @return the latest OpenRocket version. + */ + public String getLatestVersion() { + return latestVersion; + } + + + /** + * Return a list of the new features/updates that are available. The list has a + * priority for each update and a message text. The returned list may be modified. + * + * @return a modifiable list of the updates. + */ + public List> getUpdates() { + return updates.clone(); + } + + @Override + public String toString() { + return "UpdateInfo[version=" + latestVersion + "; updates=" + updates.toString() + "]"; + } + +} diff --git a/core/src/net/sf/openrocket/communication/UpdateInfoRetriever.java b/core/src/net/sf/openrocket/communication/UpdateInfoRetriever.java new file mode 100644 index 00000000..3122f085 --- /dev/null +++ b/core/src/net/sf/openrocket/communication/UpdateInfoRetriever.java @@ -0,0 +1,240 @@ +package net.sf.openrocket.communication; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Locale; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.ComparablePair; +import net.sf.openrocket.util.LimitedInputStream; + +public class UpdateInfoRetriever { + private static final LogHelper log = Application.getLogger(); + + private UpdateInfoFetcher fetcher = null; + + + /** + * Start an asynchronous task that will fetch information about the latest + * OpenRocket version. This will overwrite any previous fetching operation. + * This call will return immediately. + */ + public void start() { + fetcher = new UpdateInfoFetcher(); + fetcher.setDaemon(true); + fetcher.start(); + } + + + /** + * Check whether the update info fetching is still in progress. + * + * @return true if the communication is still in progress. + */ + public boolean isRunning() { + if (fetcher == null) { + throw new IllegalStateException("startFetchUpdateInfo() has not been called"); + } + return fetcher.isAlive(); + } + + + /** + * Retrieve the result of the background update info fetcher. This method returns + * the result of the previous call to {@link #start()}. It must be + * called before calling this method. + *

+ * This method will return null if the info fetcher is still running or + * if it encountered a problem in communicating with the server. The difference can + * be checked using {@link #isRunning()}. + * + * @return the update result, or null if the fetching is still in progress + * or an error occurred while communicating with the server. + * @throws IllegalStateException if {@link #start()} has not been called. + */ + public UpdateInfo getUpdateInfo() { + if (fetcher == null) { + throw new IllegalStateException("start() has not been called"); + } + return fetcher.info; + } + + + + /** + * Parse the data received from the server. + * + * @param r the Reader from which to read. + * @return an UpdateInfo construct, or null if the data was invalid. + * @throws IOException if an I/O exception occurs. + */ + /* package-private */ + static UpdateInfo parseUpdateInput(Reader r) throws IOException { + BufferedReader reader; + if (r instanceof BufferedReader) { + reader = (BufferedReader) r; + } else { + reader = new BufferedReader(r); + } + + + String version = null; + ArrayList> updates = + new ArrayList>(); + + String str = reader.readLine(); + while (str != null) { + if (str.matches("^Version: *[0-9]+\\.[0-9]+\\.[0-9]+[a-zA-Z0-9.-]* *$")) { + version = str.substring(8).trim(); + } else if (str.matches("^[0-9]+:\\p{Print}+$")) { + int index = str.indexOf(':'); + int value = Integer.parseInt(str.substring(0, index)); + String desc = str.substring(index + 1).trim(); + if (!desc.equals("")) { + updates.add(new ComparablePair(value, desc)); + } + } + // Ignore anything else + str = reader.readLine(); + } + + if (version != null) { + return new UpdateInfo(version, updates); + } else { + return null; + } + } + + + + /** + * An asynchronous task that fetches and parses the update info. + * + * @author Sampo Niskanen + */ + private class UpdateInfoFetcher extends Thread { + + private volatile UpdateInfo info = null; + + @Override + public void run() { + try { + doConnection(); + } catch (IOException e) { + log.info("Fetching update failed: " + e); + return; + } + } + + + private void doConnection() throws IOException { + String url = Communicator.UPDATE_INFO_URL + "?" + Communicator.VERSION_PARAM + "=" + + Communicator.encode(BuildProperties.getVersion()); + + HttpURLConnection connection = Communicator.connectionSource.getConnection(url); + + connection.setConnectTimeout(Communicator.CONNECTION_TIMEOUT); + connection.setInstanceFollowRedirects(true); + connection.setRequestMethod("GET"); + connection.setUseCaches(false); + connection.setDoInput(true); + connection.setRequestProperty("X-OpenRocket-Version", + Communicator.encode(BuildProperties.getVersion() + " " + BuildProperties.getBuildSource())); + connection.setRequestProperty("X-OpenRocket-ID", + Communicator.encode(Application.getPreferences().getUniqueID())); + connection.setRequestProperty("X-OpenRocket-OS", + Communicator.encode(System.getProperty("os.name") + " " + + System.getProperty("os.arch"))); + connection.setRequestProperty("X-OpenRocket-Java", + Communicator.encode(System.getProperty("java.vendor") + " " + + System.getProperty("java.version"))); + connection.setRequestProperty("X-OpenRocket-Country", + Communicator.encode(System.getProperty("user.country") + " " + + System.getProperty("user.timezone"))); + connection.setRequestProperty("X-OpenRocket-Locale", + Communicator.encode(Locale.getDefault().toString())); + connection.setRequestProperty("X-OpenRocket-CPUs", "" + Runtime.getRuntime().availableProcessors()); + + InputStream is = null; + try { + connection.connect(); + + log.debug("Update response code: " + connection.getResponseCode()); + + if (connection.getResponseCode() == Communicator.UPDATE_INFO_NO_UPDATE_CODE) { + // No updates are available + log.info("No updates available"); + info = new UpdateInfo(); + return; + } + + if (connection.getResponseCode() != Communicator.UPDATE_INFO_UPDATE_AVAILABLE) { + // Error communicating with server + log.warn("Unknown server response code: " + connection.getResponseCode()); + return; + } + + String contentType = connection.getContentType(); + if (contentType == null || + contentType.toLowerCase().indexOf(Communicator.UPDATE_INFO_CONTENT_TYPE) < 0) { + // Unknown response type + log.warn("Unknown Content-type received:" + contentType); + return; + } + + // Update is available, parse input + is = connection.getInputStream(); + is = new LimitedInputStream(is, Communicator.MAX_INPUT_BYTES); + String encoding = connection.getContentEncoding(); + if (encoding == null || encoding.equals("")) + encoding = "UTF-8"; + BufferedReader reader = new BufferedReader(new InputStreamReader(is, encoding)); + + String version = null; + ArrayList> updates = + new ArrayList>(); + + String line = reader.readLine(); + while (line != null) { + + if (line.matches("^Version:[a-zA-Z0-9._ -]{1,30}$")) { + version = line.substring(8).trim(); + } else if (line.matches("^[0-9]{1,9}:\\P{Cntrl}{1,300}$")) { + String[] split = line.split(":", 2); + int n = Integer.parseInt(split[0]); + updates.add(new ComparablePair(n, split[1].trim())); + } + // Ignore line otherwise + line = reader.readLine(); + } + + // Check version input + if (version == null || version.length() == 0 || + version.equalsIgnoreCase(BuildProperties.getVersion())) { + // Invalid response + log.warn("Invalid version received, ignoring."); + return; + } + + + info = new UpdateInfo(version, updates); + log.info("Found update: " + info); + } finally { + try { + if (is != null) + is.close(); + connection.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } +} diff --git a/core/src/net/sf/openrocket/database/Database.java b/core/src/net/sf/openrocket/database/Database.java new file mode 100644 index 00000000..37636304 --- /dev/null +++ b/core/src/net/sf/openrocket/database/Database.java @@ -0,0 +1,243 @@ +package net.sf.openrocket.database; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import net.sf.openrocket.file.Loader; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.JarUtil; + + + +/** + * A database set. This class functions as a Set that contains items + * of a specific type. Additionally, the items can be accessed via an index number. + * The elements are always kept in their natural order. + * + * @author Sampo Niskanen + */ +public class Database> extends AbstractSet { + private static final LogHelper log = Application.getLogger(); + + private final List list = new ArrayList(); + private final ArrayList> listeners = + new ArrayList>(); + private final Loader loader; + + + public Database() { + loader = null; + } + + public Database(Loader loader) { + this.loader = loader; + } + + + + @Override + public Iterator iterator() { + return new DBIterator(); + } + + @Override + public int size() { + return list.size(); + } + + @Override + public boolean add(T element) { + int index; + + index = Collections.binarySearch(list, element); + if (index >= 0) { + // List might contain the element + if (list.contains(element)) { + return false; + } + } else { + index = -(index + 1); + } + list.add(index, element); + fireAddEvent(element); + return true; + } + + + /** + * Get the element with the specified index. + * @param index the index to retrieve. + * @return the element at the index. + */ + public T get(int index) { + return list.get(index); + } + + /** + * Return the index of the given Motor, or -1 if not in the database. + * + * @param m the motor + * @return the index of the motor + */ + public int indexOf(T m) { + return list.indexOf(m); + } + + + public void addDatabaseListener(DatabaseListener listener) { + listeners.add(listener); + } + + public void removeChangeListener(DatabaseListener listener) { + listeners.remove(listener); + } + + + + @SuppressWarnings("unchecked") + protected void fireAddEvent(T element) { + Object[] array = listeners.toArray(); + for (Object l : array) { + ((DatabaseListener) l).elementAdded(element, this); + } + } + + @SuppressWarnings("unchecked") + protected void fireRemoveEvent(T element) { + Object[] array = listeners.toArray(); + for (Object l : array) { + ((DatabaseListener) l).elementRemoved(element, this); + } + } + + + + //////// Directory loading + + + + /** + * Load all files in a directory to the motor database. Only files with file + * names matching the given pattern (as matched by String.matches(String)) + * are processed. + * + * @param dir the directory to read. + * @param pattern the pattern to match the file names to. + * @throws IOException if an IO error occurs when reading the JAR archive + * (errors reading individual files are printed to stderr). + */ + public void loadDirectory(File dir, final String pattern) throws IOException { + if (loader == null) { + throw new IllegalStateException("no file loader set"); + } + + File[] files = dir.listFiles(new FilenameFilter() { + @Override + public boolean accept(File directory, String name) { + return name.matches(pattern); + } + }); + if (files == null) { + throw new IOException("not a directory: " + dir); + } + for (File file : files) { + try { + this.addAll(loader.load(new FileInputStream(file), file.getName())); + } catch (IOException e) { + log.warn("Error loading file " + file + ": " + e.getMessage(), e); + } + } + } + + + /** + * Read all files in a directory contained in the JAR file that this class belongs to. + * Only files whose names match the given pattern (as matched by + * String.matches(String)) will be read. + * + * @param dir the directory within the JAR archive to read. + * @param pattern the pattern to match the file names to. + * @throws IOException if an IO error occurs when reading the JAR archive + * (errors reading individual files are printed to stderr). + */ + public void loadJarDirectory(String dir, String pattern) throws IOException { + + // Process directory and extension + if (!dir.endsWith("/")) { + dir += "/"; + } + + // Find and open the jar file this class is contained in + File file = JarUtil.getCurrentJarFile(); + JarFile jarFile = new JarFile(file); + + try { + + // Loop through JAR entries searching for files to load + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.startsWith(dir) && name.matches(pattern)) { + try { + InputStream stream = jarFile.getInputStream(entry); + this.addAll(loader.load(stream, name)); + } catch (IOException e) { + log.warn("Error loading file " + file + ": " + e.getMessage(), e); + } + } + } + + } finally { + jarFile.close(); + } + } + + + + public void load(File file) throws IOException { + if (loader == null) { + throw new IllegalStateException("no file loader set"); + } + this.addAll(loader.load(new FileInputStream(file), file.getName())); + } + + + + /** + * Iterator class implementation that fires changes if remove() is called. + */ + private class DBIterator implements Iterator { + private Iterator iterator = list.iterator(); + private T current = null; + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + current = iterator.next(); + return current; + } + + @Override + public void remove() { + iterator.remove(); + fireRemoveEvent(current); + } + } +} diff --git a/core/src/net/sf/openrocket/database/DatabaseListener.java b/core/src/net/sf/openrocket/database/DatabaseListener.java new file mode 100644 index 00000000..a24b2dde --- /dev/null +++ b/core/src/net/sf/openrocket/database/DatabaseListener.java @@ -0,0 +1,9 @@ +package net.sf.openrocket.database; + +public interface DatabaseListener> { + + public void elementAdded(T element, Database source); + + public void elementRemoved(T element, Database source); + +} diff --git a/core/src/net/sf/openrocket/database/Databases.java b/core/src/net/sf/openrocket/database/Databases.java new file mode 100644 index 00000000..d8a132b0 --- /dev/null +++ b/core/src/net/sf/openrocket/database/Databases.java @@ -0,0 +1,202 @@ +package net.sf.openrocket.database; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.material.MaterialStorage; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; + + +/** + * A class that contains single instances of {@link Database} for specific purposes. + * + * @author Sampo Niskanen + */ +public class Databases { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + /* Static implementations of specific databases: */ + + /** + * A database of bulk materials (with bulk densities). + */ + public static final Database BULK_MATERIAL = new Database(); + /** + * A database of surface materials (with surface densities). + */ + public static final Database SURFACE_MATERIAL = new Database(); + /** + * A database of linear material (with length densities). + */ + public static final Database LINE_MATERIAL = new Database(); + + + + static { + + // Add default materials + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Acrylic"), 1190, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Balsa"), 170, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Basswood"), 500, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Birch"), 670, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Cardboard"), 680, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Carbonfiber"), 1780, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Cork"), 240, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.DepronXPS"), 40, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Fiberglass"), 1850, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Kraftphenolic"), 950, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Maple"), 755, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Paperoffice"), 820, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Pine"), 530, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Plywoodbirch"), 630, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.PolycarbonateLexan"), 1200, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Polystyrene"), 1050, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.PVC"), 1390, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Spruce"), 450, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.StyrofoamgenericEPS"), 20, false)); + // BULK_MATERIAL.add(new Material.Bulk("Styrofoam (Blue foam, XPS)", 32, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.StyrofoamBluefoamXPS"), 32, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Quantumtubing"), 1050, false)); + BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.BlueTube"), 1300, false)); + + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Ripstopnylon"), 0.067, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Mylar"), 0.021, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Polyethylenethin"), 0.015, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Polyethyleneheavy"), 0.040, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Silk"), 0.060, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Paperoffice"), 0.080, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Cellophane"), 0.018, false)); + SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Crepepaper"), 0.025, false)); + + //// Thread (heavy-duty) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Threadheavy-duty"), 0.0003, false)); + //// Elastic cord (round 2mm, 1/16 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordround2mm"), 0.0018, false)); + //// Elastic cord (flat 6mm, 1/4 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat6mm"), 0.0043, false)); + //// Elastic cord (flat 12mm, 1/2 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat12mm"), 0.008, false)); + //// Elastic cord (flat 19mm, 3/4 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat19mm"), 0.0012, false)); + //// Elastic cord (flat 25mm, 1 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat25mm"), 0.0016, false)); + //// Braided nylon (2 mm, 1/16 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Braidednylon2mm"), 0.001, false)); + //// Braided nylon (3 mm, 1/8 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Braidednylon3mm"), 0.0035, false)); + //// Tubular nylon (11 mm, 7/16 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Tubularnylon11mm"), 0.013, false)); + //// Tubular nylon (14 mm, 9/16 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Tubularnylon14mm"), 0.016, false)); + //// Tubular nylon (25 mm, 1 in) + LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Tubularnylon25mm"), 0.029, false)); + + + // Add user-defined materials + for (Material m : Application.getPreferences().getUserMaterials()) { + switch (m.getType()) { + case LINE: + LINE_MATERIAL.add(m); + break; + + case SURFACE: + SURFACE_MATERIAL.add(m); + break; + + case BULK: + BULK_MATERIAL.add(m); + break; + + default: + log.warn("ERROR: Unknown material type " + m); + } + } + + // Add database storage listener + MaterialStorage listener = new MaterialStorage(); + LINE_MATERIAL.addDatabaseListener(listener); + SURFACE_MATERIAL.addDatabaseListener(listener); + BULK_MATERIAL.addDatabaseListener(listener); + } + + + /* + * Used just for ensuring initialization of the class. + */ + public static void fakeMethod() { + + } + + + /** + * Find a material from the database with the specified type and name. Returns + * null if the specified material could not be found. + * + * @param type the material type. + * @param name the material name in the database. + * @return the material, or null if not found. + */ + public static Material findMaterial(Material.Type type, String name) { + Database db; + switch (type) { + case BULK: + db = BULK_MATERIAL; + break; + case SURFACE: + db = SURFACE_MATERIAL; + break; + case LINE: + db = LINE_MATERIAL; + break; + default: + throw new IllegalArgumentException("Illegal material type: " + type); + } + + for (Material m : db) { + if (m.getName().equalsIgnoreCase(name)) { + return m; + } + } + return null; + } + + + /** + * Find a material from the database or return a new material if the specified + * material with the specified density is not found. + * + * @param type the material type. + * @param name the material name. + * @param density the density of the material. + * @param userDefined whether a newly created material should be user-defined. + * @return the material object from the database or a new material. + */ + public static Material findMaterial(Material.Type type, String name, double density, + boolean userDefined) { + Database db; + switch (type) { + case BULK: + db = BULK_MATERIAL; + break; + case SURFACE: + db = SURFACE_MATERIAL; + break; + case LINE: + db = LINE_MATERIAL; + break; + default: + throw new IllegalArgumentException("Illegal material type: " + type); + } + + for (Material m : db) { + if (m.getName().equalsIgnoreCase(name) && MathUtil.equals(m.getDensity(), density)) { + return m; + } + } + return Material.newMaterial(type, name, density, userDefined); + } + + +} diff --git a/core/src/net/sf/openrocket/database/MotorDatabase.java b/core/src/net/sf/openrocket/database/MotorDatabase.java new file mode 100644 index 00000000..fdde5fac --- /dev/null +++ b/core/src/net/sf/openrocket/database/MotorDatabase.java @@ -0,0 +1,24 @@ +package net.sf.openrocket.database; + +import java.util.List; + +import net.sf.openrocket.motor.Motor; + +public interface MotorDatabase { + + /** + * Return all motors in the database matching a search criteria. Any search criteria that + * is null or NaN is ignored. + * + * @param type the motor type, or null. + * @param manufacturer the manufacturer, or null. + * @param designation the designation, or null. + * @param diameter the diameter, or NaN. + * @param length the length, or NaN. + * @return a list of all the matching motors. + */ + public List findMotors(Motor.Type type, + String manufacturer, String designation, double diameter, + double length); + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/database/ThrustCurveMotorSet.java b/core/src/net/sf/openrocket/database/ThrustCurveMotorSet.java new file mode 100644 index 00000000..99c422e3 --- /dev/null +++ b/core/src/net/sf/openrocket/database/ThrustCurveMotorSet.java @@ -0,0 +1,305 @@ +package net.sf.openrocket.database; + +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.sf.openrocket.motor.DesignationComparator; +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.Motor.Type; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.MathUtil; + +public class ThrustCurveMotorSet implements Comparable { + + // Comparators: + private static final Collator COLLATOR = Collator.getInstance(Locale.US); + static { + COLLATOR.setStrength(Collator.PRIMARY); + } + private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator(); + private static final ThrustCurveMotorComparator comparator = new ThrustCurveMotorComparator(); + + + + private final ArrayList motors = new ArrayList(); + private final Map digestMap = + new IdentityHashMap(); + + private final List delays = new ArrayList(); + + private Manufacturer manufacturer = null; + private String designation = null; + private String simplifiedDesignation = null; + private double diameter = -1; + private double length = -1; + private Motor.Type type = Motor.Type.UNKNOWN; + + + + public void addMotor(ThrustCurveMotor motor) { + + // Check for first insertion + if (motors.isEmpty()) { + manufacturer = motor.getManufacturer(); + designation = motor.getDesignation(); + simplifiedDesignation = simplifyDesignation(designation); + diameter = motor.getDiameter(); + length = motor.getLength(); + } + + // Verify that the motor can be added + if (!matches(motor)) { + throw new IllegalArgumentException("Motor does not match the set:" + + " manufacturer=" + manufacturer + + " designation=" + designation + + " diameter=" + diameter + + " length=" + length + + " set_size=" + motors.size() + + " motor=" + motor); + } + + // Update the type if now known + if (type == Motor.Type.UNKNOWN) { + type = motor.getMotorType(); + // Add "Plugged" option if hybrid + if (type == Motor.Type.HYBRID) { + if (!delays.contains(Motor.PLUGGED)) { + delays.add(Motor.PLUGGED); + } + } + } + + // Change the simplified designation if necessary + if (!designation.equalsIgnoreCase(motor.getDesignation().trim())) { + designation = simplifiedDesignation; + } + + // Add the standard delays + for (double d : motor.getStandardDelays()) { + d = Math.rint(d); + if (!delays.contains(d)) { + delays.add(d); + } + } + Collections.sort(delays); + + + // Check whether to add as new motor or overwrite existing + final String digest = MotorDigest.digestMotor(motor); + for (int index = 0; index < motors.size(); index++) { + Motor m = motors.get(index); + + if (digest.equals(digestMap.get(m)) && + motor.getDesignation().equals(m.getDesignation())) { + + // Match found, check which one to keep (or both) based on comment + String newCmt = motor.getDescription().replaceAll("\\s+", " ").trim(); + String oldCmt = m.getDescription().replaceAll("\\s+", " ").trim(); + + if (newCmt.length() == 0 || newCmt.equals(oldCmt)) { + // Do not replace and do not add + return; + } else if (oldCmt.length() == 0) { + // Replace existing motor + motors.set(index, motor); + digestMap.put(motor, digest); + return; + } + // else continue search and add both + + } + } + + // Motor not present, add it + motors.add(motor); + digestMap.put(motor, digest); + Collections.sort(motors, comparator); + + } + + + public boolean matches(ThrustCurveMotor m) { + if (motors.isEmpty()) + return true; + + if (manufacturer != m.getManufacturer()) + return false; + + if (!MathUtil.equals(diameter, m.getDiameter())) + return false; + + if (!MathUtil.equals(length, m.getLength())) + return false; + + if ((type != Type.UNKNOWN) && (m.getMotorType() != Type.UNKNOWN) && + (type != m.getMotorType())) { + return false; + } + + if (!simplifiedDesignation.equals(simplifyDesignation(m.getDesignation()))) + return false; + + return true; + } + + + public List getMotors() { + return motors.clone(); + } + + + public int getMotorCount() { + return motors.size(); + } + + + /** + * Return the standard delays applicable to this motor type. This is a union of + * all the delays of the motors included in this set. + * @return the delays + */ + public List getDelays() { + return Collections.unmodifiableList(delays); + } + + + /** + * Return the manufacturer of this motor type. + * @return the manufacturer + */ + public Manufacturer getManufacturer() { + return manufacturer; + } + + + /** + * Return the designation of this motor type. This is either the exact or simplified + * designation, depending on what motors have been added. + * @return the designation + */ + public String getDesignation() { + return designation; + } + + + /** + * Return the diameter of this motor type. + * @return the diameter + */ + public double getDiameter() { + return diameter; + } + + + /** + * Return the length of this motor type. + * @return the length + */ + public double getLength() { + return length; + } + + + /** + * Return the type of this motor type. If any of the added motors has a type different + * from UNKNOWN, then that type will be returned. + * @return the type + */ + public Motor.Type getType() { + return type; + } + + + + + @Override + public String toString() { + return "ThrustCurveMotorSet[" + manufacturer + " " + designation + + ", type=" + type + ", count=" + motors.size() + "]"; + } + + + + + private static final Pattern SIMPLIFY_PATTERN = Pattern.compile("^[0-9]*[ -]*([A-Z][0-9]+).*"); + + /** + * Simplify a motor designation, if possible. This attempts to reduce the designation + * into a simple letter + number notation for the impulse class and average thrust. + * + * @param str the designation to simplify + * @return the simplified designation, or the string itself if the format was not detected + */ + public static String simplifyDesignation(String str) { + str = str.trim(); + Matcher m = SIMPLIFY_PATTERN.matcher(str); + if (m.matches()) { + return m.group(1); + } else { + return str; + } + } + + + /** + * Comparator for deciding in which order to display matching motors. + */ + private static class ThrustCurveMotorComparator implements Comparator { + + @Override + public int compare(ThrustCurveMotor o1, ThrustCurveMotor o2) { + // 1. Designation + if (!o1.getDesignation().equals(o2.getDesignation())) { + return o1.getDesignation().compareTo(o2.getDesignation()); + } + + // 2. Number of data points (more is better) + if (o1.getTimePoints().length != o2.getTimePoints().length) { + return o2.getTimePoints().length - o1.getTimePoints().length; + } + + // 3. Comment length (longer is better) + return o2.getDescription().length() - o1.getDescription().length(); + } + + } + + + @Override + public int compareTo(ThrustCurveMotorSet other) { + + int value; + + // 1. Manufacturer + value = COLLATOR.compare(this.manufacturer.getDisplayName(), + other.manufacturer.getDisplayName()); + if (value != 0) + return value; + + // 2. Designation + value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation); + if (value != 0) + return value; + + // 3. Diameter + value = (int) ((this.diameter - other.diameter) * 1000000); + if (value != 0) + return value; + + // 4. Length + value = (int) ((this.length - other.length) * 1000000); + return value; + + } + +} diff --git a/core/src/net/sf/openrocket/database/ThrustCurveMotorSetDatabase.java b/core/src/net/sf/openrocket/database/ThrustCurveMotorSetDatabase.java new file mode 100644 index 00000000..1aeb6cd4 --- /dev/null +++ b/core/src/net/sf/openrocket/database/ThrustCurveMotorSetDatabase.java @@ -0,0 +1,213 @@ +package net.sf.openrocket.database; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; + +/** + * A database containing ThrustCurveMotorSet objects and allowing adding a motor + * to the database. + * + * @author Sampo Niskanen + */ +public abstract class ThrustCurveMotorSetDatabase implements MotorDatabase { + + private static final LogHelper logger = Application.getLogger(); + + protected List motorSets; + + private volatile boolean startedLoading = false; + private volatile boolean endedLoading = false; + private final boolean asynchronous; + + /** Set to true the first time {@link #blockUntilLoaded()} is called. */ + protected volatile boolean inUse = false; + + /** + * Sole constructor. + * + * @param asynchronous whether to load motors asynchronously in a background thread. + */ + public ThrustCurveMotorSetDatabase(boolean asynchronous) { + this.asynchronous = asynchronous; + } + + + /* (non-Javadoc) + * @see net.sf.openrocket.database.ThrustCurveMotorSetDatabaseI#getMotorSets() + */ + public List getMotorSets() { + blockUntilLoaded(); + return motorSets; + } + + + + /* (non-Javadoc) + * @see net.sf.openrocket.database.ThrustCurveMotorSetDatabaseI#findMotors(net.sf.openrocket.motor.Motor.Type, java.lang.String, java.lang.String, double, double) + */ + @Override + public List findMotors(Motor.Type type, String manufacturer, String designation, + double diameter, double length) { + blockUntilLoaded(); + ArrayList results = new ArrayList(); + + for (ThrustCurveMotorSet set : motorSets) { + for (ThrustCurveMotor m : set.getMotors()) { + boolean match = true; + if (type != null && type != set.getType()) + match = false; + else if (manufacturer != null && !m.getManufacturer().matches(manufacturer)) + match = false; + else if (designation != null && !designation.equalsIgnoreCase(m.getDesignation())) + match = false; + else if (!Double.isNaN(diameter) && (Math.abs(diameter - m.getDiameter()) > 0.0015)) + match = false; + else if (!Double.isNaN(length) && (Math.abs(length - m.getLength()) > 0.0015)) + match = false; + + if (match) + results.add(m); + } + } + + return results; + } + + + /** + * Add a motor to the database. If a matching ThrustCurveMototSet is found, + * the motor is added to that set, otherwise a new set is created and added to the + * database. + * + * @param motor the motor to add + */ + protected void addMotor(ThrustCurveMotor motor) { + // Iterate from last to first, as this is most likely to hit early when loading files + for (int i = motorSets.size() - 1; i >= 0; i--) { + ThrustCurveMotorSet set = motorSets.get(i); + if (set.matches(motor)) { + set.addMotor(motor); + return; + } + } + + ThrustCurveMotorSet newSet = new ThrustCurveMotorSet(); + newSet.addMotor(motor); + motorSets.add(newSet); + } + + + + + + /** + * Start loading the motors. If asynchronous + * + * @throws IllegalStateException if this method has already been called. + */ + public void startLoading() { + if (startedLoading) { + throw new IllegalStateException("Already called startLoading"); + } + startedLoading = true; + if (asynchronous) { + new LoadingThread().start(); + } else { + performMotorLoading(); + } + } + + + /** + * Return whether loading the database has ended. + * + * @return whether background loading has ended. + */ + public boolean isLoaded() { + return endedLoading; + } + + + /** + * Mark that this database is in use or a place is waiting for the database to + * become loaded. This can be used in conjunction with {@link #isLoaded()} to load + * the database without blocking. + */ + public void setInUse() { + inUse = true; + } + + + /** + * Block the current thread until loading of the motors has been completed. + * + * @throws IllegalStateException if startLoading() has not been called. + */ + public void blockUntilLoaded() { + inUse = true; + if (!startedLoading) { + throw new IllegalStateException("startLoading() has not been called"); + } + if (!endedLoading) { + synchronized (this) { + while (!endedLoading) { + try { + this.wait(); + } catch (InterruptedException e) { + logger.warn("InterruptedException occurred, ignoring", e); + } + } + } + } + } + + + /** + * Used for loading the motor database. This method will be called in a background + * thread to load the motors asynchronously. This method should call + * {@link #addMotor(ThrustCurveMotor)} to add the motors to the database. + */ + protected abstract void loadMotors(); + + + + /** + * Creates the motor list, calls {@link #loadMotors()}, sorts the list and marks + * the motors as loaded. This method is called either synchronously or from the + * background thread. + */ + private void performMotorLoading() { + motorSets = new ArrayList(); + try { + loadMotors(); + } catch (Exception e) { + logger.error("Loading motors failed", e); + } + Collections.sort(motorSets); + motorSets = Collections.unmodifiableList(motorSets); + synchronized (ThrustCurveMotorSetDatabase.this) { + endedLoading = true; + ThrustCurveMotorSetDatabase.this.notifyAll(); + } + } + + + /** + * Background thread for loading the motors. This creates the motor list, + * calls loadMotors(), sorts the database, makes it unmodifiable, and finally + * marks the database as loaded and notifies any blocked threads. + */ + private class LoadingThread extends Thread { + @Override + public void run() { + performMotorLoading(); + } + } + +} diff --git a/core/src/net/sf/openrocket/document/OpenRocketDocument.java b/core/src/net/sf/openrocket/document/OpenRocketDocument.java new file mode 100644 index 00000000..cafc98f3 --- /dev/null +++ b/core/src/net/sf/openrocket/document/OpenRocketDocument.java @@ -0,0 +1,531 @@ +package net.sf.openrocket.document; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +import net.sf.openrocket.document.events.DocumentChangeEvent; +import net.sf.openrocket.document.events.DocumentChangeListener; +import net.sf.openrocket.document.events.SimulationChangeEvent; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayList; + +/** + * Class describing an entire OpenRocket document, including a rocket and + * simulations. The document contains: + *

+ * - the rocket definition + * - a default Configuration + * - Simulation instances + * - the stored file and file save information + * - undo/redo information + * + * @author Sampo Niskanen + */ +public class OpenRocketDocument implements ComponentChangeListener { + private static final LogHelper log = Application.getLogger(); + + /** + * The minimum number of undo levels that are stored. + */ + public static final int UNDO_LEVELS = 50; + /** + * The margin of the undo levels. After the number of undo levels exceeds + * UNDO_LEVELS by this amount the undo is purged to that length. + */ + public static final int UNDO_MARGIN = 10; + + public static final String SIMULATION_NAME_PREFIX = "Simulation "; + + /** Whether an undo error has already been reported to the user */ + private static boolean undoErrorReported = false; + + private final Rocket rocket; + private final Configuration configuration; + + private final ArrayList simulations = new ArrayList(); + + + /* + * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.* + */ + + /** + * The undo history of the rocket. Whenever a new undo position is created while the + * rocket is in "dirty" state, the rocket is copied here. + */ + private LinkedList undoHistory = new LinkedList(); + private LinkedList undoDescription = new LinkedList(); + + /** + * The position in the undoHistory we are currently at. If modifications have been + * made to the rocket, the rocket is in "dirty" state and this points to the previous + * "clean" state. + */ + private int undoPosition = -1; // Illegal position, init in constructor + + /** + * The description of the next action that modifies this rocket. + */ + private String nextDescription = null; + private String storedDescription = null; + + + private ArrayList undoRedoListeners = new ArrayList(2); + + private File file = null; + private int savedID = -1; + + private final StorageOptions storageOptions = new StorageOptions(); + + + private final List listeners = + new ArrayList(); + + public OpenRocketDocument(Rocket rocket) { + this(rocket.getDefaultConfiguration()); + } + + + private OpenRocketDocument(Configuration configuration) { + this.configuration = configuration; + this.rocket = configuration.getRocket(); + + clearUndo(); + + rocket.addComponentChangeListener(this); + } + + + + + public Rocket getRocket() { + return rocket; + } + + + public Configuration getDefaultConfiguration() { + return configuration; + } + + + public File getFile() { + return file; + } + + public void setFile(File file) { + this.file = file; + } + + + public boolean isSaved() { + return rocket.getModID() == savedID; + } + + public void setSaved(boolean saved) { + if (saved == false) + this.savedID = -1; + else + this.savedID = rocket.getModID(); + } + + /** + * Retrieve the default storage options for this document. + * + * @return the storage options. + */ + public StorageOptions getDefaultStorageOptions() { + return storageOptions; + } + + + + + + public List getSimulations() { + return simulations.clone(); + } + + public int getSimulationCount() { + return simulations.size(); + } + + public Simulation getSimulation(int n) { + return simulations.get(n); + } + + public int getSimulationIndex(Simulation simulation) { + return simulations.indexOf(simulation); + } + + public void addSimulation(Simulation simulation) { + simulations.add(simulation); + fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); + } + + public void addSimulation(Simulation simulation, int n) { + simulations.add(n, simulation); + fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); + } + + public void removeSimulation(Simulation simulation) { + simulations.remove(simulation); + fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); + } + + public Simulation removeSimulation(int n) { + Simulation simulation = simulations.remove(n); + fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); + return simulation; + } + + /** + * Return a unique name suitable for the next simulation. The name begins + * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any + * previous simulation. + * + * @return the new name. + */ + public String getNextSimulationName() { + // Generate unique name for the simulation + int maxValue = 0; + for (Simulation s : simulations) { + String name = s.getName(); + if (name.startsWith(SIMULATION_NAME_PREFIX)) { + try { + maxValue = Math.max(maxValue, + Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length()))); + } catch (NumberFormatException ignore) { + } + } + } + return SIMULATION_NAME_PREFIX + (maxValue + 1); + } + + + /** + * Adds an undo point at this position. This method should be called *before* any + * action that is to be undoable. All actions after the call will be undone by a + * single "Undo" action. + *

+ * The description should be a short, descriptive string of the actions that will + * follow. This is shown to the user e.g. in the Edit-menu, for example + * "Undo (Modify body tube)". If the actions are not known (in general should not + * be the case!) description may be null. + *

+ * If this method is called successively without any change events occurring between the + * calls, only the last call will have any effect. + * + * @param description A short description of the following actions. + */ + public void addUndoPosition(String description) { + + if (storedDescription != null) { + logUndoError("addUndoPosition called while storedDescription=" + storedDescription + + " description=" + description); + } + + // Check whether modifications have been done since last call + if (isCleanState()) { + // No modifications + log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state"); + nextDescription = description; + return; + } + + log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state"); + + /* + * Modifications have been made to the rocket. We should be at the end of the + * undo history, but check for consistency and try to recover. + */ + if (undoPosition != undoHistory.size() - 1) { + logUndoError("undo position inconsistency"); + } + while (undoPosition < undoHistory.size() - 1) { + undoHistory.removeLast(); + undoDescription.removeLast(); + } + + + // Add the current state to the undo history + undoHistory.add(rocket.copyWithOriginalID()); + undoDescription.add(null); + nextDescription = description; + undoPosition++; + + + // Maintain maximum undo size + if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) { + for (int i = 0; i < UNDO_MARGIN; i++) { + undoHistory.removeFirst(); + undoDescription.removeFirst(); + undoPosition--; + } + } + } + + + /** + * Start a time-limited undoable operation. After the operation {@link #stopUndo()} + * must be called, which will restore the previous undo description into effect. + * Only one level of start-stop undo descriptions is supported, i.e. start-stop + * undo cannot be nested, and no other undo operations may be called between + * the start and stop calls. + * + * @param description Description of the following undoable operations. + */ + public void startUndo(String description) { + if (storedDescription != null) { + logUndoError("startUndo called while storedDescription=" + storedDescription + + " description=" + description); + } + log.info("Starting time-limited undoable operation '" + description + "' for " + this); + String store = nextDescription; + addUndoPosition(description); + storedDescription = store; + } + + /** + * End the previous time-limited undoable operation. This must be called after + * {@link #startUndo(String)} has been called before any other undo operations are + * performed. + */ + public void stopUndo() { + log.info("Ending time-limited undoable operation for " + this + " nextDescription=" + + nextDescription + " storedDescription=" + storedDescription); + String stored = storedDescription; + storedDescription = null; + addUndoPosition(stored); + } + + + /** + * Clear the undo history. + */ + public void clearUndo() { + log.info("Clearing undo history of " + this); + undoHistory.clear(); + undoDescription.clear(); + + undoHistory.add(rocket.copyWithOriginalID()); + undoDescription.add(null); + undoPosition = 0; + + fireUndoRedoChangeEvent(); + } + + + @Override + public void componentChanged(ComponentChangeEvent e) { + + if (!e.isUndoChange()) { + if (undoPosition < undoHistory.size() - 1) { + log.info("Rocket changed while in undo history, removing redo information for " + this + + " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() + + " isClean=" + isCleanState()); + } + // Remove any redo information if available + while (undoPosition < undoHistory.size() - 1) { + undoHistory.removeLast(); + undoDescription.removeLast(); + } + + // Set the latest description + undoDescription.set(undoPosition, nextDescription); + } + + fireUndoRedoChangeEvent(); + } + + + /** + * Return whether undo action is available. + * @return true if undo can be performed + */ + public boolean isUndoAvailable() { + if (undoPosition > 0) + return true; + + return !isCleanState(); + } + + /** + * Return the description of what action would be undone if undo is called. + * @return the description what would be undone, or null if description unavailable. + */ + public String getUndoDescription() { + if (!isUndoAvailable()) + return null; + + if (isCleanState()) { + return undoDescription.get(undoPosition - 1); + } else { + return undoDescription.get(undoPosition); + } + } + + + /** + * Return whether redo action is available. + * @return true if redo can be performed + */ + public boolean isRedoAvailable() { + return undoPosition < undoHistory.size() - 1; + } + + /** + * Return the description of what action would be redone if redo is called. + * @return the description of what would be redone, or null if description unavailable. + */ + public String getRedoDescription() { + if (!isRedoAvailable()) + return null; + + return undoDescription.get(undoPosition); + } + + + /** + * Perform undo operation on the rocket. + */ + public void undo() { + log.info("Performing undo for " + this + " undoPosition=" + undoPosition + + " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState()); + if (!isUndoAvailable()) { + logUndoError("Undo not available"); + fireUndoRedoChangeEvent(); + return; + } + if (storedDescription != null) { + logUndoError("undo() called with storedDescription=" + storedDescription); + } + + // Update history position + + if (isCleanState()) { + // We are in a clean state, simply move backwards in history + undoPosition--; + } else { + if (undoPosition != undoHistory.size() - 1) { + logUndoError("undo position inconsistency"); + } + // Modifications have been made, save the state and restore previous state + undoHistory.add(rocket.copyWithOriginalID()); + undoDescription.add(null); + } + + rocket.checkComponentStructure(); + undoHistory.get(undoPosition).checkComponentStructure(); + undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure(); + rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID()); + rocket.checkComponentStructure(); + } + + + /** + * Perform redo operation on the rocket. + */ + public void redo() { + log.info("Performing redo for " + this + " undoPosition=" + undoPosition + + " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState()); + if (!isRedoAvailable()) { + logUndoError("Redo not available"); + fireUndoRedoChangeEvent(); + return; + } + if (storedDescription != null) { + logUndoError("redo() called with storedDescription=" + storedDescription); + } + + undoPosition++; + + rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID()); + } + + + private boolean isCleanState() { + return rocket.getModID() == undoHistory.get(undoPosition).getModID(); + } + + + /** + * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first + * time it occurs, but not on subsequent times. Logs automatically the undo system state. + */ + private void logUndoError(String error) { + log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition + + " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() + + " nextDescription=" + nextDescription + " storedDescription=" + storedDescription, + new TraceException()); + + if (!undoErrorReported) { + undoErrorReported = true; + Application.getExceptionHandler().handleErrorCondition("Undo/Redo error: " + error); + } + } + + + + /** + * Return a copy of this document. The rocket is copied with original ID's, the default + * motor configuration ID is maintained and the simulations are copied to the new rocket. + * No undo/redo information or file storage information is maintained. + * + * @return a copy of this document. + */ + public OpenRocketDocument copy() { + Rocket rocketCopy = rocket.copyWithOriginalID(); + OpenRocketDocument documentCopy = new OpenRocketDocument(rocketCopy); + documentCopy.getDefaultConfiguration().setMotorConfigurationID(configuration.getMotorConfigurationID()); + for (Simulation s : simulations) { + documentCopy.addSimulation(s.duplicateSimulation(rocketCopy)); + } + return documentCopy; + } + + + + /////// Listeners + + public void addUndoRedoListener( UndoRedoListener listener ) { + undoRedoListeners.add(listener); + } + + public void removeUndoRedoListener( UndoRedoListener listener ) { + undoRedoListeners.remove(listener); + } + + private void fireUndoRedoChangeEvent() { + UndoRedoListener[] array = undoRedoListeners.toArray(new UndoRedoListener[0]); + for (UndoRedoListener l : array) { + l.setAllValues(); + } + + } + + public void addDocumentChangeListener(DocumentChangeListener listener) { + listeners.add(listener); + } + + public void removeDocumentChangeListener(DocumentChangeListener listener) { + listeners.remove(listener); + } + + protected void fireDocumentChangeEvent(DocumentChangeEvent event) { + DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]); + for (DocumentChangeListener l : array) { + l.documentChanged(event); + } + } + + + + +} diff --git a/core/src/net/sf/openrocket/document/Simulation.java b/core/src/net/sf/openrocket/document/Simulation.java new file mode 100644 index 00000000..74d3ab28 --- /dev/null +++ b/core/src/net/sf/openrocket/document/Simulation.java @@ -0,0 +1,469 @@ +package net.sf.openrocket.document; + +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; + +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.simulation.BasicEventSimulationEngine; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.RK4SimulationStepper; +import net.sf.openrocket.simulation.SimulationConditions; +import net.sf.openrocket.simulation.SimulationEngine; +import net.sf.openrocket.simulation.SimulationOptions; +import net.sf.openrocket.simulation.SimulationStepper; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationListenerException; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.SafetyMutex; +import net.sf.openrocket.util.StateChangeListener; + +/** + * A class defining a simulation, its conditions and simulated data. + *

+ * This class is not thread-safe and enforces single-threaded access with a + * SafetyMutex. + * + * @author Sampo Niskanen + */ +public class Simulation implements ChangeSource, Cloneable { + private static final LogHelper log = Application.getLogger(); + + public static enum Status { + /** Up-to-date */ + UPTODATE, + + /** Loaded from file, status probably up-to-date */ + LOADED, + + /** Data outdated */ + OUTDATED, + + /** Imported external data */ + EXTERNAL, + + /** Not yet simulated */ + NOT_SIMULATED + } + + private SafetyMutex mutex = SafetyMutex.newInstance(); + + private final Rocket rocket; + + private String name = ""; + + private Status status = Status.NOT_SIMULATED; + + /** The conditions to use */ + // TODO: HIGH: Change to use actual conditions class?? + private SimulationOptions options; + + private ArrayList simulationListeners = new ArrayList(); + + private final Class simulationEngineClass = BasicEventSimulationEngine.class; + private Class simulationStepperClass = RK4SimulationStepper.class; + private Class aerodynamicCalculatorClass = BarrowmanCalculator.class; + private Class massCalculatorClass = BasicMassCalculator.class; + + + + /** Listeners for this object */ + private List listeners = new ArrayList(); + + + /** The conditions actually used in the previous simulation, or null */ + private SimulationOptions simulatedConditions = null; + private String simulatedMotors = null; + private FlightData simulatedData = null; + private int simulatedRocketID = -1; + + + /** + * Create a new simulation for the rocket. The initial motor configuration is + * taken from the default rocket configuration. + * + * @param rocket the rocket associated with the simulation. + */ + public Simulation(Rocket rocket) { + this.rocket = rocket; + this.status = Status.NOT_SIMULATED; + + options = new SimulationOptions(rocket); + options.setMotorConfigurationID( + rocket.getDefaultConfiguration().getMotorConfigurationID()); + options.addChangeListener(new ConditionListener()); + } + + + public Simulation(Rocket rocket, Status status, String name, SimulationOptions options, + List listeners, FlightData data) { + + if (rocket == null) + throw new IllegalArgumentException("rocket cannot be null"); + if (status == null) + throw new IllegalArgumentException("status cannot be null"); + if (name == null) + throw new IllegalArgumentException("name cannot be null"); + if (options == null) + throw new IllegalArgumentException("options cannot be null"); + + this.rocket = rocket; + + if (status == Status.UPTODATE) { + this.status = Status.LOADED; + } else if (data == null) { + this.status = Status.NOT_SIMULATED; + } else { + this.status = status; + } + + this.name = name; + + this.options = options; + options.addChangeListener(new ConditionListener()); + + if (listeners != null) { + this.simulationListeners.addAll(listeners); + } + + + if (data != null && this.status != Status.NOT_SIMULATED) { + simulatedData = data; + if (this.status == Status.LOADED) { + simulatedConditions = options.clone(); + simulatedRocketID = rocket.getModID(); + } + } + + } + + + /** + * Return the rocket associated with this simulation. + * + * @return the rocket. + */ + public Rocket getRocket() { + mutex.verify(); + return rocket; + } + + + /** + * Return a newly created Configuration for this simulation. The configuration + * has the motor ID set and all stages active. + * + * @return a newly created Configuration of the launch conditions. + */ + public Configuration getConfiguration() { + mutex.verify(); + Configuration c = new Configuration(rocket); + c.setMotorConfigurationID(options.getMotorConfigurationID()); + c.setAllStages(); + return c; + } + + /** + * Returns the simulation options attached to this simulation. The options + * may be modified freely, and the status of the simulation will change to reflect + * the changes. + * + * @return the simulation conditions. + */ + public SimulationOptions getOptions() { + mutex.verify(); + return options; + } + + + /** + * Get the list of simulation listeners. The returned list is the one used by + * this object; changes to it will reflect changes in the simulation. + * + * @return the actual list of simulation listeners. + */ + public List getSimulationListeners() { + mutex.verify(); + return simulationListeners; + } + + + /** + * Return the user-defined name of the simulation. + * + * @return the name for the simulation. + */ + public String getName() { + mutex.verify(); + return name; + } + + /** + * Set the user-defined name of the simulation. Setting the name to + * null yields an empty name. + * + * @param name the name of the simulation. + */ + public void setName(String name) { + mutex.lock("setName"); + try { + if (this.name.equals(name)) + return; + + if (name == null) + this.name = ""; + else + this.name = name; + + fireChangeEvent(); + } finally { + mutex.unlock("setName"); + } + } + + + /** + * Returns the status of this simulation. This method examines whether the + * simulation has been outdated and returns {@link Status#OUTDATED} accordingly. + * + * @return the status + * @see Status + */ + public Status getStatus() { + mutex.verify(); + + if (status == Status.UPTODATE || status == Status.LOADED) { + if (rocket.getFunctionalModID() != simulatedRocketID || + !options.equals(simulatedConditions)) + return Status.OUTDATED; + } + + return status; + } + + + + /** + * Simulate the flight. + * + * @param additionalListeners additional simulation listeners (those defined by the simulation are used in any case) + * @throws SimulationException if a problem occurs during simulation + */ + public void simulate(SimulationListener... additionalListeners) + throws SimulationException { + mutex.lock("simulate"); + try { + + if (this.status == Status.EXTERNAL) { + throw new SimulationException("Cannot simulate imported simulation."); + } + + SimulationEngine simulator; + + try { + simulator = simulationEngineClass.newInstance(); + } catch (InstantiationException e) { + throw new IllegalStateException("Cannot instantiate simulator.", e); + } catch (IllegalAccessException e) { + throw new IllegalStateException("Cannot access simulator instance?! BUG!", e); + } + + SimulationConditions simulationConditions = options.toSimulationConditions(); + for (SimulationListener l : additionalListeners) { + simulationConditions.getSimulationListenerList().add(l); + } + + for (String className : simulationListeners) { + SimulationListener l = null; + try { + Class c = Class.forName(className); + l = (SimulationListener) c.newInstance(); + } catch (Exception e) { + throw new SimulationListenerException("Could not instantiate listener of " + + "class: " + className, e); + } + simulationConditions.getSimulationListenerList().add(l); + } + + long t1, t2; + log.debug("Simulation: calling simulator"); + t1 = System.currentTimeMillis(); + simulatedData = simulator.simulate(simulationConditions); + t2 = System.currentTimeMillis(); + log.debug("Simulation: returning from simulator, simulation took " + (t2 - t1) + "ms"); + + // Set simulated info after simulation, will not be set in case of exception + simulatedConditions = options.clone(); + simulatedMotors = getConfiguration().getMotorConfigurationDescription(); + simulatedRocketID = rocket.getFunctionalModID(); + + status = Status.UPTODATE; + fireChangeEvent(); + } finally { + mutex.unlock("simulate"); + } + } + + + /** + * Return the conditions used in the previous simulation, or null + * if this simulation has not been run. + * + * @return the conditions used in the previous simulation, or null. + */ + public SimulationOptions getSimulatedConditions() { + mutex.verify(); + return simulatedConditions; + } + + /** + * Return the warnings generated in the previous simulation, or + * null if this simulation has not been run. This is the same + * warning set as contained in the FlightData object. + * + * @return the warnings during the previous simulation, or null. + * @see FlightData#getWarningSet() + */ + public WarningSet getSimulatedWarnings() { + mutex.verify(); + if (simulatedData == null) + return null; + return simulatedData.getWarningSet(); + } + + + /** + * Return a string describing the motor configuration of the previous simulation, + * or null if this simulation has not been run. + * + * @return a description of the motor configuration of the previous simulation, or + * null. + * @see Rocket#getMotorConfigurationNameOrDescription(String) + */ + public String getSimulatedMotorDescription() { + mutex.verify(); + return simulatedMotors; + } + + /** + * Return the flight data of the previous simulation, or null if + * this simulation has not been run. + * + * @return the flight data of the previous simulation, or null. + */ + public FlightData getSimulatedData() { + mutex.verify(); + return simulatedData; + } + + + + /** + * Returns a copy of this simulation suitable for cut/copy/paste operations. + * The rocket refers to the same instance as the original simulation. + * This excludes any simulated data. + * + * @return a copy of this simulation and its conditions. + */ + public Simulation copy() { + mutex.lock("copy"); + try { + + Simulation copy = (Simulation) super.clone(); + + copy.mutex = SafetyMutex.newInstance(); + copy.status = Status.NOT_SIMULATED; + copy.options = this.options.clone(); + copy.simulationListeners = this.simulationListeners.clone(); + copy.listeners = new ArrayList(); + copy.simulatedConditions = null; + copy.simulatedMotors = null; + copy.simulatedData = null; + copy.simulatedRocketID = -1; + + return copy; + + } catch (CloneNotSupportedException e) { + throw new BugException("Clone not supported, BUG", e); + } finally { + mutex.unlock("copy"); + } + } + + + /** + * Create a duplicate of this simulation with the specified rocket. The new + * simulation is in non-simulated state. + * + * @param newRocket the rocket for the new simulation. + * @return a new simulation with the same conditions and properties. + */ + public Simulation duplicateSimulation(Rocket newRocket) { + mutex.lock("duplicateSimulation"); + try { + Simulation copy = new Simulation(newRocket); + + copy.name = this.name; + copy.options.copyFrom(this.options); + copy.simulationListeners = this.simulationListeners.clone(); + copy.simulationStepperClass = this.simulationStepperClass; + copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass; + + return copy; + } finally { + mutex.unlock("duplicateSimulation"); + } + } + + + + @Override + public void addChangeListener(EventListener listener) { + mutex.verify(); + listeners.add(listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + mutex.verify(); + listeners.remove(listener); + } + + protected void fireChangeEvent() { + EventObject e = new EventObject(this); + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] ls = listeners.toArray(new EventListener[0]); + for (EventListener l : ls) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(e); + } + } + } + + + + + private class ConditionListener implements StateChangeListener { + + private Status oldStatus = null; + + @Override + public void stateChanged(EventObject e) { + if (getStatus() != oldStatus) { + oldStatus = getStatus(); + fireChangeEvent(); + } + } + } +} diff --git a/core/src/net/sf/openrocket/document/StorageOptions.java b/core/src/net/sf/openrocket/document/StorageOptions.java new file mode 100644 index 00000000..7e1186c1 --- /dev/null +++ b/core/src/net/sf/openrocket/document/StorageOptions.java @@ -0,0 +1,53 @@ +package net.sf.openrocket.document; + +import net.sf.openrocket.util.BugException; + +public class StorageOptions implements Cloneable { + + public static final double SIMULATION_DATA_NONE = Double.POSITIVE_INFINITY; + public static final double SIMULATION_DATA_ALL = 0; + + private boolean compressionEnabled = true; + + private double simulationTimeSkip = SIMULATION_DATA_NONE; + + private boolean explicitlySet = false; + + + public boolean isCompressionEnabled() { + return compressionEnabled; + } + + public void setCompressionEnabled(boolean compression) { + this.compressionEnabled = compression; + } + + public double getSimulationTimeSkip() { + return simulationTimeSkip; + } + + public void setSimulationTimeSkip(double simulationTimeSkip) { + this.simulationTimeSkip = simulationTimeSkip; + } + + + + public boolean isExplicitlySet() { + return explicitlySet; + } + + public void setExplicitlySet(boolean explicitlySet) { + this.explicitlySet = explicitlySet; + } + + + + @Override + public StorageOptions clone() { + try { + return (StorageOptions)super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException?!?", e); + } + } +} diff --git a/core/src/net/sf/openrocket/document/UndoRedoListener.java b/core/src/net/sf/openrocket/document/UndoRedoListener.java new file mode 100644 index 00000000..4f7900ea --- /dev/null +++ b/core/src/net/sf/openrocket/document/UndoRedoListener.java @@ -0,0 +1,8 @@ +package net.sf.openrocket.document; + +import java.util.EventListener; + +public interface UndoRedoListener extends EventListener { + + public void setAllValues(); +} diff --git a/core/src/net/sf/openrocket/document/events/DocumentChangeEvent.java b/core/src/net/sf/openrocket/document/events/DocumentChangeEvent.java new file mode 100644 index 00000000..cb5662f2 --- /dev/null +++ b/core/src/net/sf/openrocket/document/events/DocumentChangeEvent.java @@ -0,0 +1,11 @@ +package net.sf.openrocket.document.events; + +import java.util.EventObject; + +public class DocumentChangeEvent extends EventObject { + + public DocumentChangeEvent(Object source) { + super(source); + } + +} diff --git a/core/src/net/sf/openrocket/document/events/DocumentChangeListener.java b/core/src/net/sf/openrocket/document/events/DocumentChangeListener.java new file mode 100644 index 00000000..01d3dbd4 --- /dev/null +++ b/core/src/net/sf/openrocket/document/events/DocumentChangeListener.java @@ -0,0 +1,7 @@ +package net.sf.openrocket.document.events; + +public interface DocumentChangeListener { + + public void documentChanged(DocumentChangeEvent event); + +} diff --git a/core/src/net/sf/openrocket/document/events/SimulationChangeEvent.java b/core/src/net/sf/openrocket/document/events/SimulationChangeEvent.java new file mode 100644 index 00000000..56ed6d1a --- /dev/null +++ b/core/src/net/sf/openrocket/document/events/SimulationChangeEvent.java @@ -0,0 +1,10 @@ +package net.sf.openrocket.document.events; + + +public class SimulationChangeEvent extends DocumentChangeEvent { + + public SimulationChangeEvent(Object source) { + super(source); + } + +} diff --git a/core/src/net/sf/openrocket/file/CSVExport.java b/core/src/net/sf/openrocket/file/CSVExport.java new file mode 100644 index 00000000..f2e8d6fc --- /dev/null +++ b/core/src/net/sf/openrocket/file/CSVExport.java @@ -0,0 +1,213 @@ +package net.sf.openrocket.file; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.util.TextUtil; + +public class CSVExport { + + /** + * Exports the specified flight data branch into a CSV file. + * + * @param stream the stream to write to. + * @param simulation the simulation being exported. + * @param branch the branch to export. + * @param fields the fields to export (in appropriate order). + * @param units the units of the fields. + * @param fieldSeparator the field separator string. + * @param commentStarter the comment starting character(s). + * @param simulationComments whether to output general simulation comments. + * @param fieldComments whether to output field comments. + * @param eventComments whether to output comments for the flight events. + * @throws IOException if an I/O exception occurs. + */ + public static void exportCSV(OutputStream stream, Simulation simulation, + FlightDataBranch branch, FlightDataType[] fields, Unit[] units, + String fieldSeparator, String commentStarter, boolean simulationComments, + boolean fieldComments, boolean eventComments) throws IOException { + + if (fields.length != units.length) { + throw new IllegalArgumentException("fields and units lengths must be equal " + + "(" + fields.length + " vs " + units.length + ")"); + } + + + PrintWriter writer = null; + try { + + writer = new PrintWriter(stream); + + // Write the initial comments + if (simulationComments) { + writeSimulationComments(writer, simulation, branch, fields, commentStarter); + } + + if (simulationComments && fieldComments) { + writer.println(commentStarter); + } + + if (fieldComments) { + writer.print(commentStarter + " "); + for (int i = 0; i < fields.length; i++) { + writer.print(fields[i].getName() + " (" + units[i].getUnit() + ")"); + if (i < fields.length - 1) { + writer.print(fieldSeparator); + } + } + writer.println(); + } + + writeData(writer, branch, fields, units, fieldSeparator, + eventComments, commentStarter); + + + } finally { + if (writer != null) { + try { + writer.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + private static void writeData(PrintWriter writer, FlightDataBranch branch, + FlightDataType[] fields, Unit[] units, String fieldSeparator, boolean eventComments, + String commentStarter) { + + // Number of data points + int n = branch.getLength(); + + // Flight events in occurrance order + List events = branch.getEvents(); + Collections.sort(events); + int eventPosition = 0; + + // List of field values + List> fieldValues = new ArrayList>(); + for (FlightDataType t : fields) { + fieldValues.add(branch.get(t)); + } + + // Time variable + List time = branch.get(FlightDataType.TYPE_TIME); + if (eventComments && time == null) { + // If time information is not available, print events at beginning of file + for (FlightEvent e : events) { + printEvent(writer, e, commentStarter); + } + eventPosition = events.size(); + } + + + // Loop over all data points + for (int pos = 0; pos < n; pos++) { + + // Check for events to store + if (eventComments && time != null) { + double t = time.get(pos); + + while ((eventPosition < events.size()) && + (events.get(eventPosition).getTime() <= t)) { + printEvent(writer, events.get(eventPosition), commentStarter); + eventPosition++; + } + } + + // Store CSV line + for (int i = 0; i < fields.length; i++) { + double value = fieldValues.get(i).get(pos); + writer.print(TextUtil.doubleToString(units[i].toUnit(value))); + if (i < fields.length - 1) { + writer.print(fieldSeparator); + } + } + writer.println(); + + } + + // Store any remaining events + if (eventComments && time != null) { + while (eventPosition < events.size()) { + printEvent(writer, events.get(eventPosition), commentStarter); + eventPosition++; + } + } + + } + + + private static void printEvent(PrintWriter writer, FlightEvent e, + String commentStarter) { + writer.println(commentStarter + " Event " + e.getType().name() + + " occurred at t=" + TextUtil.doubleToString(e.getTime()) + " seconds"); + } + + private static void writeSimulationComments(PrintWriter writer, + Simulation simulation, FlightDataBranch branch, FlightDataType[] fields, + String commentStarter) { + + String line; + + line = simulation.getName(); + + FlightData data = simulation.getSimulatedData(); + + switch (simulation.getStatus()) { + case UPTODATE: + line += " (Up to date)"; + break; + + case LOADED: + line += " (Data loaded from a file)"; + break; + + case OUTDATED: + line += " (Data is out of date)"; + break; + + case EXTERNAL: + line += " (Imported data)"; + break; + + case NOT_SIMULATED: + line += " (Not simulated yet)"; + break; + } + + writer.println(commentStarter + " " + line); + + + writer.println(commentStarter + " " + branch.getLength() + " data points written for " + + fields.length + " variables."); + + + if (data == null) { + writer.println(commentStarter + " No simulation data available."); + return; + } + WarningSet warnings = data.getWarningSet(); + + if (!warnings.isEmpty()) { + writer.println(commentStarter + " Simulation warnings:"); + for (Warning w : warnings) { + writer.println(commentStarter + " " + w.toString()); + } + } + } + +} diff --git a/core/src/net/sf/openrocket/file/GeneralRocketLoader.java b/core/src/net/sf/openrocket/file/GeneralRocketLoader.java new file mode 100644 index 00000000..b6117f78 --- /dev/null +++ b/core/src/net/sf/openrocket/file/GeneralRocketLoader.java @@ -0,0 +1,91 @@ +package net.sf.openrocket.file; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.openrocket.OpenRocketLoader; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.zip.GZIPInputStream; + + +/** + * A rocket loader that auto-detects the document type and uses the appropriate + * loading. Supports loading of GZIPed files as well with transparent + * uncompression. + * + * @author Sampo Niskanen + */ +public class GeneralRocketLoader extends RocketLoader { + + private static final int READ_BYTES = 300; + + private static final byte[] GZIP_SIGNATURE = { 31, -117 }; // 0x1f, 0x8b + private static final byte[] OPENROCKET_SIGNATURE = + " { + + public Collection load(InputStream stream, String filename) throws IOException; + +} diff --git a/core/src/net/sf/openrocket/file/RocketLoadException.java b/core/src/net/sf/openrocket/file/RocketLoadException.java new file mode 100644 index 00000000..2fdd177b --- /dev/null +++ b/core/src/net/sf/openrocket/file/RocketLoadException.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.file; + +public class RocketLoadException extends Exception { + + public RocketLoadException() { + } + + public RocketLoadException(String message) { + super(message); + } + + public RocketLoadException(Throwable cause) { + super(cause); + } + + public RocketLoadException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/file/RocketLoader.java b/core/src/net/sf/openrocket/file/RocketLoader.java new file mode 100644 index 00000000..4fc8dada --- /dev/null +++ b/core/src/net/sf/openrocket/file/RocketLoader.java @@ -0,0 +1,74 @@ +package net.sf.openrocket.file; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; + + +public abstract class RocketLoader { + protected final WarningSet warnings = new WarningSet(); + + + /** + * Loads a rocket from the specified File object. + */ + public final OpenRocketDocument load(File source) throws RocketLoadException { + warnings.clear(); + InputStream stream = null; + + try { + + stream = new BufferedInputStream(new FileInputStream(source)); + return load(stream); + + } catch (FileNotFoundException e) { + throw new RocketLoadException("File not found: " + source); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Loads a rocket from the specified InputStream. + */ + public final OpenRocketDocument load(InputStream source) throws RocketLoadException { + warnings.clear(); + + try { + return loadFromStream(source); + } catch (RocketLoadException e) { + throw e; + } catch (IOException e) { + throw new RocketLoadException("I/O error: " + e.getMessage(), e); + } + } + + + + /** + * This method is called by the default implementations of {@link #load(File)} + * and {@link #load(InputStream)} to load the rocket. + * + * @throws RocketLoadException if an error occurs during loading. + */ + protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException, + RocketLoadException; + + + + public final WarningSet getWarnings() { + return warnings; + } +} diff --git a/core/src/net/sf/openrocket/file/RocketSaver.java b/core/src/net/sf/openrocket/file/RocketSaver.java new file mode 100644 index 00000000..5c18bb8d --- /dev/null +++ b/core/src/net/sf/openrocket/file/RocketSaver.java @@ -0,0 +1,102 @@ +package net.sf.openrocket.file; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.StorageOptions; + + +public abstract class RocketSaver { + + /** + * Save the document to the specified file using the default storage options. + * + * @param dest the destination file. + * @param document the document to save. + * @throws IOException in case of an I/O error. + */ + public final void save(File dest, OpenRocketDocument document) throws IOException { + save(dest, document, document.getDefaultStorageOptions()); + } + + + /** + * Save the document to the specified file using the given storage options. + * + * @param dest the destination file. + * @param document the document to save. + * @param options the storage options. + * @throws IOException in case of an I/O error. + */ + public void save(File dest, OpenRocketDocument document, StorageOptions options) + throws IOException { + OutputStream s = new BufferedOutputStream(new FileOutputStream(dest)); + try { + save(s, document, options); + } finally { + s.close(); + } + } + + + /** + * Save the document to the specified output stream using the default storage options. + * + * @param dest the destination stream. + * @param doc the document to save. + * @throws IOException in case of an I/O error. + */ + public final void save(OutputStream dest, OpenRocketDocument doc) throws IOException { + save(dest, doc, doc.getDefaultStorageOptions()); + } + + + /** + * Save the document to the specified output stream using the given storage options. + * + * @param dest the destination stream. + * @param doc the document to save. + * @param options the storage options. + * @throws IOException in case of an I/O error. + */ + public abstract void save(OutputStream dest, OpenRocketDocument doc, + StorageOptions options) throws IOException; + + + + /** + * Provide an estimate of the file size when saving the document with the + * specified options. This is used as an indication to the user and when estimating + * file save progress. + * + * @param doc the document. + * @param options the save options, compression must be taken into account. + * @return the estimated number of bytes the storage would take. + */ + public abstract long estimateFileSize(OpenRocketDocument doc, StorageOptions options); + + + + + public static String escapeXML(String s) { + + s = s.replace("&", "&"); + s = s.replace("<", "<"); + s = s.replace(">", ">"); + s = s.replace("\"","""); + s = s.replace("'", "'"); + + for (int i=0; i < s.length(); i++) { + char n = s.charAt(i); + if (((n < 32) && (n != 9) && (n != 10) && (n != 13)) || (n == 127)) { + s = s.substring(0,i) + "&#" + ((int)n) + ";" + s.substring(i+1); + } + } + + return s; + } +} diff --git a/core/src/net/sf/openrocket/file/UnknownFileTypeException.java b/core/src/net/sf/openrocket/file/UnknownFileTypeException.java new file mode 100644 index 00000000..bcea2bbb --- /dev/null +++ b/core/src/net/sf/openrocket/file/UnknownFileTypeException.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.file; + +import java.io.IOException; + +/** + * An exception marking that a file type was not supported. + * + * @author Sampo Niskanen + */ +public class UnknownFileTypeException extends IOException { + + public UnknownFileTypeException() { + } + + public UnknownFileTypeException(String message) { + super(message); + } + + public UnknownFileTypeException(Throwable cause) { + super(cause); + } + + public UnknownFileTypeException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/file/configuration/XmlContainerElement.java b/core/src/net/sf/openrocket/file/configuration/XmlContainerElement.java new file mode 100644 index 00000000..c227f6d0 --- /dev/null +++ b/core/src/net/sf/openrocket/file/configuration/XmlContainerElement.java @@ -0,0 +1,24 @@ +package net.sf.openrocket.file.configuration; + +import java.util.ArrayList; +import java.util.List; + +public class XmlContainerElement extends XmlElement { + + private ArrayList subelements = new ArrayList(); + + public XmlContainerElement(String name) { + super(name); + } + + + public void addElement(XmlElement element) { + subelements.add(element); + } + + @SuppressWarnings("unchecked") + public List getElements() { + return (List) subelements.clone(); + } + +} diff --git a/core/src/net/sf/openrocket/file/configuration/XmlContentElement.java b/core/src/net/sf/openrocket/file/configuration/XmlContentElement.java new file mode 100644 index 00000000..bf469271 --- /dev/null +++ b/core/src/net/sf/openrocket/file/configuration/XmlContentElement.java @@ -0,0 +1,28 @@ +package net.sf.openrocket.file.configuration; + +/** + * A simple XML element that contains textual content. + * + * @author Sampo Niskanen + */ +public class XmlContentElement extends XmlElement { + + private String content = ""; + + public XmlContentElement(String name) { + super(name); + } + + + public String getContent() { + return content; + } + + public void setContent(String content) { + if (content == null) { + throw new IllegalArgumentException("XML content cannot be null"); + } + this.content = content; + } + +} diff --git a/core/src/net/sf/openrocket/file/configuration/XmlElement.java b/core/src/net/sf/openrocket/file/configuration/XmlElement.java new file mode 100644 index 00000000..ee094340 --- /dev/null +++ b/core/src/net/sf/openrocket/file/configuration/XmlElement.java @@ -0,0 +1,45 @@ +package net.sf.openrocket.file.configuration; + +import java.util.HashMap; +import java.util.Map; + +/** + * A base simple XML element. A simple XML element can contain either other XML elements + * (XmlContainerElement) or textual content (XmlContentElement), but not both. + * + * @author Sampo Niskanen + */ +public abstract class XmlElement { + + private final String name; + private final HashMap attributes = new HashMap(); + + + + public XmlElement(String name) { + this.name = name; + } + + + public String getName() { + return name; + } + + public void setAttribute(String key, String value) { + attributes.put(key, value); + } + + public void removeAttribute(String key) { + attributes.remove(key); + } + + public String getAttribute(String key) { + return attributes.get(key); + } + + @SuppressWarnings("unchecked") + public Map getAttributes() { + return (Map) attributes.clone(); + } + +} diff --git a/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java b/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java new file mode 100644 index 00000000..c934482c --- /dev/null +++ b/core/src/net/sf/openrocket/file/iterator/DirectoryIterator.java @@ -0,0 +1,187 @@ +package net.sf.openrocket.file.iterator; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.JarUtil; +import net.sf.openrocket.util.Pair; + +/** + * A DirectoryIterator that scans for files within a directory in the file system + * matching a FileFilter. The scan is optionally recursive. + * + * @author Sampo Niskanen + */ +public class DirectoryIterator extends FileIterator { + + private static final LogHelper logger = Application.getLogger(); + + private final FileFilter filter; + private final File[] files; + private final boolean recursive; + private int position = 0; + private DirectoryIterator subIterator = null; + + /** + * Sole constructor. + * + * @param directory the directory to read. + * @param filter the filter for selecting files. + * @throws IOException if the directory cannot be read. + */ + public DirectoryIterator(File directory, FileFilter filter, boolean recursive) + throws IOException { + + this.filter = filter; + this.recursive = recursive; + + this.files = directory.listFiles(new DirSelectionFileFilter(filter, recursive)); + if (this.files == null) { + throw new IOException("not a directory or IOException occurred when listing files " + + "from " + directory); + } + } + + + + + + @Override + protected Pair findNext() { + + // Check if we're recursing + if (subIterator != null) { + if (subIterator.hasNext()) { + return subIterator.next(); + } else { + subIterator.close(); + subIterator = null; + } + } + + // Scan through file entries + while (position < files.length) { + File file = files[position]; + position++; + + try { + if (recursive && file.isDirectory()) { + subIterator = new DirectoryIterator(file, filter, recursive); + if (subIterator.hasNext()) { + return subIterator.next(); + } else { + subIterator.close(); + subIterator = null; + continue; + } + } + + InputStream is = new BufferedInputStream(new FileInputStream(file)); + return new Pair(file.getName(), is); + } catch (IOException e) { + logger.warn("Error opening file/directory " + file, e); + } + } + return null; + } + + + + /** + * Return a DirectoryIterator for a directory that can be located either + * within the containing JAR file, in the classpath or in the current directory + * (searched in this order). The first place that contains matching files + * will be iterated through. + * + * @param directory the directory to search for. + * @param filter the filter for matching files in the directory. + * @return a DirectoryIterator for iterating through the files in the + * directory, or null if no directory containing + * matching files can be found. + */ + public static FileIterator findDirectory(String directory, FileFilter filter) { + FileIterator iterator = null; + + // Try to load from containing JAR file + File jarFile = JarUtil.getCurrentJarFile(); + if (jarFile != null) { + try { + iterator = new ZipDirectoryIterator(jarFile, directory, filter); + if (iterator.hasNext()) { + return iterator; + } + iterator.close(); + } catch (IOException e) { + logger.error("Error opening containing JAR file " + jarFile, e); + } + } + + + // Try to find directory as a system resource + URL url = ClassLoader.getSystemResource(directory); + if (url != null) { + try { + File dir = JarUtil.urlToFile(url); + iterator = new DirectoryIterator(dir, filter, true); + if (iterator.hasNext()) { + return iterator; + } + iterator.close(); + } catch (Exception e1) { + logger.error("Error opening directory from URL " + url); + } + } + + + // Try to open directory as such + try { + iterator = new DirectoryIterator(new File(directory), filter, true); + if (iterator.hasNext()) { + return iterator; + } + iterator.close(); + } catch (IOException e) { + logger.error("Error opening directory " + directory); + } + + return null; + } + + + + /** + * A FileFilter wrapper that accepts or discards directories. + */ + private class DirSelectionFileFilter implements FileFilter { + + private final boolean acceptDirs; + private final FileFilter parentFilter; + + + public DirSelectionFileFilter(FileFilter filter, boolean acceptDirs) { + this.acceptDirs = acceptDirs; + this.parentFilter = filter; + } + + + @Override + public boolean accept(File pathname) { + if (pathname.getName().startsWith(".")) { + return false; + } + if (pathname.isDirectory()) { + return acceptDirs; + } + return parentFilter.accept(pathname); + } + + } + +} diff --git a/core/src/net/sf/openrocket/file/iterator/FileIterator.java b/core/src/net/sf/openrocket/file/iterator/FileIterator.java new file mode 100644 index 00000000..14d220a9 --- /dev/null +++ b/core/src/net/sf/openrocket/file/iterator/FileIterator.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.file.iterator; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Pair; + +/** + * An abstract class for iterating over files fulfilling some condition. The files are + * returned as pairs of open InputStreams and file names. Conditions can be for example + * files in a directory matching a specific FileFilter. + *

+ * Concrete implementations must implement the method {@link #findNext()} and possibly + * {@link #close()}. + * + * @author Sampo Niskanen + */ +public abstract class FileIterator implements Iterator> { + private static final LogHelper logger = Application.getLogger(); + + private Pair next = null; + private int fileCount = 0; + + @Override + public boolean hasNext() { + if (next != null) + return true; + + next = findNext(); + return (next != null); + } + + + @Override + public Pair next() { + if (next == null) { + next = findNext(); + } + if (next == null) { + throw new NoSuchElementException("No more files"); + } + + Pair n = next; + next = null; + fileCount++; + return n; + } + + + @Override + public void remove() { + throw new UnsupportedOperationException("remove() not supported"); + } + + + + /** + * Closes the resources related to this iterator. This method should be + * overridden if the iterator needs to close any resources of its own, but + * must call this method as well. + */ + public void close() { + if (next != null) { + try { + next.getV().close(); + } catch (IOException e) { + logger.error("Error closing file " + next.getU()); + } + next = null; + } + } + + + /** + * Return the number of files that have so far been returned by this iterator. + * + * @return the number of files that this iterator has returned so far. + */ + public int getFileCount() { + return fileCount; + } + + /** + * Return the next pair of file name and InputStream. + * + * @return a pair with the file name and input stream reading the file. + */ + protected abstract Pair findNext(); + +} diff --git a/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java b/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java new file mode 100644 index 00000000..3d3e6df5 --- /dev/null +++ b/core/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java @@ -0,0 +1,105 @@ +package net.sf.openrocket.file.iterator; + +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.util.Enumeration; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Pair; + +/** + * A DirectoryIterator that reads files from the specified directory of a + * ZIP (or JAR) file. + * + * TODO: MEDIUM: This is always a recursive search. + * + * @author Sampo Niskanen + */ +public class ZipDirectoryIterator extends FileIterator { + + private static final LogHelper logger = Application.getLogger(); + + private final File zipFileName; + private final String directory; + private final FileFilter filter; + + private ZipFile zipFile; + private Enumeration entries; + + + /** + * Sole constructor. + * + * @param zipFileName the ZIP file to read. + * @param directory the directory within the ZIP file to read, relative to the + * base (an empty string corresponds to the root directory) + * @param filter the filter for accepted files. + * @throws IOException if the ZIP file could not be read. + */ + public ZipDirectoryIterator(File zipFileName, String directory, FileFilter filter) + throws IOException { + + // Process directory and extension + if (!directory.endsWith("/")) { + directory += "/"; + } + + this.zipFileName = zipFileName; + this.directory = directory; + this.filter = filter; + + + // Loop through ZIP entries searching for files to load + this.zipFile = new ZipFile(zipFileName); + entries = zipFile.entries(); + + } + + + @Override + public void close() { + super.close(); + if (zipFile != null) { + try { + zipFile.close(); + } catch (IOException e) { + logger.error("Closing ZIP file failed", e); + } + zipFile = null; + entries = null; + } + } + + + @Override + protected Pair findNext() { + if (entries == null) { + return null; + } + + while (entries.hasMoreElements()) { + ZipEntry entry = entries.nextElement(); + String name = entry.getName(); + File file = new File(name); + if (name.startsWith(directory) && filter.accept(file)) { + try { + InputStream is = zipFile.getInputStream(entry); + return new Pair(name, is); + } catch (IOException e) { + logger.error("IOException when reading ZIP file " + zipFileName, e); + } + } + } + + // No more elements exist + close(); + return null; + } + + +} diff --git a/core/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java b/core/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java new file mode 100644 index 00000000..e9051f7a --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java @@ -0,0 +1,200 @@ +package net.sf.openrocket.file.motor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.util.MathUtil; + +public abstract class AbstractMotorLoader implements MotorLoader { + + + /** + * {@inheritDoc} + *

+ * This method delegates the reading to the loaded from the Reader using the charset + * returned by {@link #getDefaultCharset()}. + */ + public List load(InputStream stream, String filename) throws IOException { + return load(new InputStreamReader(stream, getDefaultCharset()), filename); + } + + + /** + * Load motors from the specified Reader. + * + * @param reader the source of the motor definitions. + * @param filename the file name of the file, may be null if not + * applicable. + * @return a list of motors contained in the file. + * @throws IOException if an I/O exception occurs of the file format is invalid. + */ + protected abstract List load(Reader reader, String filename) throws IOException; + + + + /** + * Return the default charset to use when loading rocket files of this type. + *

+ * If the method {@link #load(InputStream, String)} is overridden as well, this + * method may return null. + * + * @return the charset to use when loading the rocket file. + */ + protected abstract Charset getDefaultCharset(); + + + + + ////////// Helper methods ////////// + + + /** + * Calculate the mass of a motor at distinct points in time based on the + * initial total mass, propellant weight and thrust. + *

+ * This calculation assumes that the velocity of the exhaust remains constant + * during the burning. This derives from the mass-flow and thrust relation + *

F = m' * v
+ * + * @param time list of time points + * @param thrust thrust at the discrete times + * @param total total weight of the motor + * @param prop propellant amount consumed during burning + * @return a list of the mass at the specified time points + */ + protected static List calculateMass(List time, List thrust, + double total, double prop) { + List mass = new ArrayList(); + List deltam = new ArrayList(); + + double t0, f0; + double totalMassChange = 0; + double scale; + + // First calculate mass change between points + t0 = time.get(0); + f0 = thrust.get(0); + for (int i = 1; i < time.size(); i++) { + double t1 = time.get(i); + double f1 = thrust.get(i); + + double dm = 0.5 * (f0 + f1) * (t1 - t0); + deltam.add(dm); + totalMassChange += dm; + t0 = t1; + f0 = f1; + } + + // Scale mass change and calculate mass + mass.add(total); + scale = prop / totalMassChange; + for (double dm : deltam) { + total -= dm * scale; + mass.add(total); + } + + return mass; + } + + + /** + * Helper method to remove a delay (or plugged) from the end of a motor designation, + * if present. + * + * @param designation the motor designation. + * @return the designation with a possible delay removed. + */ + protected static String removeDelay(String designation) { + if (designation.matches(".*-([0-9]+|[pP])$")) { + designation = designation.substring(0, designation.lastIndexOf('-')); + } + return designation; + } + + + + /** + * Helper method to tokenize a string using whitespace as the delimiter. + */ + protected static String[] split(String str) { + return split(str, "\\s+"); + } + + + /** + * Helper method to tokenize a string using the given delimiter. + */ + protected static String[] split(String str, String delim) { + String[] pieces = str.split(delim); + if (pieces.length == 0 || !pieces[0].equals("")) + return pieces; + return Arrays.copyOfRange(pieces, 1, pieces.length); + } + + + /** + * Sort the primary list and other lists in that order. + * + * @param primary the list to order. + * @param lists lists to order in the same permutation. + */ + protected static void sortLists(List primary, List... lists) { + + // TODO: LOW: Very idiotic sort algorithm, but should be fast enough + // since the time should be sorted already + + int index; + + do { + for (index = 0; index < primary.size() - 1; index++) { + if (primary.get(index + 1) < primary.get(index)) { + Collections.swap(primary, index, index + 1); + for (List l : lists) { + Collections.swap(l, index, index + 1); + } + break; + } + } + } while (index < primary.size() - 1); + } + + + + @SuppressWarnings("unchecked") + protected static void finalizeThrustCurve(List time, List thrust, + List... lists) { + + if (time.size() == 0) + return; + + // Start + if (!MathUtil.equals(time.get(0), 0) || !MathUtil.equals(thrust.get(0), 0)) { + time.add(0, 0.0); + thrust.add(0, 0.0); + for (List l : lists) { + Object o = l.get(0); + l.add(0, o); + } + } + + // End + int n = time.size() - 1; + if (!MathUtil.equals(thrust.get(n), 0)) { + time.add(time.get(n)); + thrust.add(0.0); + for (List l : lists) { + Object o = l.get(n); + l.add(o); + } + } + } + +} diff --git a/core/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java b/core/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java new file mode 100644 index 00000000..937c6790 --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java @@ -0,0 +1,80 @@ +package net.sf.openrocket.file.motor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import net.sf.openrocket.file.UnknownFileTypeException; +import net.sf.openrocket.motor.Motor; + +/** + * A motor loader class that detects the file type based on the file name extension. + * + * @author Sampo Niskanen + */ +public class GeneralMotorLoader implements MotorLoader { + + private final MotorLoader RASP_LOADER = new RASPMotorLoader(); + private final MotorLoader ROCKSIM_LOADER = new RockSimMotorLoader(); + private final MotorLoader ZIP_LOADER; + + + public GeneralMotorLoader() { + // Must use this loader in order to avoid recursive instantiation + ZIP_LOADER = new ZipFileMotorLoader(this); + } + + + + /** + * {@inheritDoc} + * + * @throws UnknownFileTypeException if the file format is not supported + */ + @Override + public List load(InputStream stream, String filename) throws IOException { + return selectLoader(filename).load(stream, filename); + } + + + + /** + * Return an array containing the supported file extensions. + * + * @return an array of the supported file extensions. + */ + public String[] getSupportedExtensions() { + return new String[] { "rse", "eng", "zip" }; + } + + + /** + * Return the appropriate motor loader based on the file name. + * + * @param filename the file name (may be null). + * @return the appropriate motor loader to use for the file. + * @throws UnknownFileTypeException if the file type cannot be detected from the file name. + */ + private MotorLoader selectLoader(String filename) throws IOException { + if (filename == null) { + throw new UnknownFileTypeException("Unknown file type, filename=null"); + } + + String ext = ""; + int point = filename.lastIndexOf('.'); + + if (point > 0) + ext = filename.substring(point + 1); + + if (ext.equalsIgnoreCase("eng")) { + return RASP_LOADER; + } else if (ext.equalsIgnoreCase("rse")) { + return ROCKSIM_LOADER; + } else if (ext.equalsIgnoreCase("zip")) { + return ZIP_LOADER; + } + + throw new UnknownFileTypeException("Unknown file type, filename=" + filename); + } + +} diff --git a/core/src/net/sf/openrocket/file/motor/MotorLoader.java b/core/src/net/sf/openrocket/file/motor/MotorLoader.java new file mode 100644 index 00000000..73e14039 --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/MotorLoader.java @@ -0,0 +1,24 @@ +package net.sf.openrocket.file.motor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import net.sf.openrocket.file.Loader; +import net.sf.openrocket.motor.Motor; + + +public interface MotorLoader extends Loader { + + /** + * Load motors from the specified InputStream. + * + * @param stream the source of the motor definitions. + * @param filename the file name of the file, may be null if not + * applicable. + * @return a list of motors contained in the file. + * @throws IOException if an I/O exception occurs of the file format is invalid. + */ + public List load(InputStream stream, String filename) throws IOException; + +} diff --git a/core/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java b/core/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java new file mode 100644 index 00000000..049738a4 --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java @@ -0,0 +1,110 @@ +package net.sf.openrocket.file.motor; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.file.iterator.DirectoryIterator; +import net.sf.openrocket.file.iterator.FileIterator; +import net.sf.openrocket.gui.util.SimpleFileFilter; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Pair; + +public final class MotorLoaderHelper { + + private static final LogHelper log = Application.getLogger(); + + private MotorLoaderHelper() { + // Prevent construction + } + + /** + * Load a file or directory of thrust curves. Directories are loaded + * recursively. Any errors during loading are logged, but otherwise ignored. + * + * @param target the file or directory to load. + * @return a list of all motors in the file/directory. + */ + public static List load(File target) { + GeneralMotorLoader loader = new GeneralMotorLoader(); + + if (target.isDirectory()) { + + try { + return load(new DirectoryIterator(target, new SimpleFileFilter("", loader.getSupportedExtensions()), true)); + } catch (IOException e) { + log.warn("Could not read directory " + target, e); + return Collections.emptyList(); + } + + } else { + + InputStream is = null; + try { + is = new FileInputStream(target); + return loader.load(new BufferedInputStream(is), target.getName()); + } catch (IOException e) { + log.warn("Could not load file " + target, e); + return Collections.emptyList(); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + log.error("Could not close file " + target, e); + } + } + } + + } + } + + + /** + * Load motors from files iterated over by a FileIterator. Any errors during + * loading are logged, but otherwise ignored. + *

+ * The iterator is closed at the end of the operation. + * + * @param iterator the FileIterator that iterates of the files to load. + * @return a list of all motors loaded. + */ + public static List load(FileIterator iterator) { + GeneralMotorLoader loader = new GeneralMotorLoader(); + List list = new ArrayList(); + + while (iterator.hasNext()) { + final Pair input = iterator.next(); + log.debug("Loading motors from file " + input.getU()); + try { + List motors = loader.load(input.getV(), input.getU()); + if (motors.size() == 0) { + log.warn("No motors found in file " + input.getU()); + } + for (Motor m : motors) { + list.add((ThrustCurveMotor) m); + } + } catch (IOException e) { + log.warn("IOException when loading motor file " + input.getU(), e); + } finally { + try { + input.getV().close(); + } catch (IOException e) { + log.error("IOException when closing InputStream", e); + } + } + } + iterator.close(); + + return list; + } + +} diff --git a/core/src/net/sf/openrocket/file/motor/RASPMotorLoader.java b/core/src/net/sf/openrocket/file/motor/RASPMotorLoader.java new file mode 100644 index 00000000..ae653e66 --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/RASPMotorLoader.java @@ -0,0 +1,216 @@ +package net.sf.openrocket.file.motor; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.MotorDigest.DataType; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.util.Coordinate; + +public class RASPMotorLoader extends AbstractMotorLoader { + + public static final String CHARSET_NAME = "ISO-8859-1"; + + public static final Charset CHARSET = Charset.forName(CHARSET_NAME); + + + + + @Override + protected Charset getDefaultCharset() { + return CHARSET; + } + + + /** + * Load a Motor from a RASP file specified by the Reader. + * The Reader is responsible for using the correct charset. + *

+ * The CG is assumed to be located at the center of the motor casing and the mass + * is calculated from the thrust curve by assuming a constant exhaust velocity. + * + * @param reader the source of the file. + * @return a list of the {@link Motor} objects defined in the file. + * @throws IOException if an I/O error occurs or if the file format is illegal. + */ + @Override + public List load(Reader reader, String filename) throws IOException { + List motors = new ArrayList(); + BufferedReader in = new BufferedReader(reader); + + String manufacturer = ""; + String designation = ""; + String comment = ""; + + double length = 0; + double diameter = 0; + ArrayList delays = null; + + List time = new ArrayList(); + List thrust = new ArrayList(); + + double propW = 0; + double totalW = 0; + + try { + String line; + String[] pieces, buf; + + line = in.readLine(); + main: while (line != null) { // Until EOF + + manufacturer = ""; + designation = ""; + comment = ""; + length = 0; + diameter = 0; + delays = new ArrayList(); + propW = 0; + totalW = 0; + time.clear(); + thrust.clear(); + + // Read comment + while (line.length() == 0 || line.charAt(0) == ';') { + if (line.length() > 0) { + comment += line.substring(1).trim() + "\n"; + } + line = in.readLine(); + if (line == null) + break main; + } + comment = comment.trim(); + + // Parse header line, example: + // F32 24 124 5-10-15-P .0377 .0695 RV + // desig diam len delays prop.w tot.w manufacturer + pieces = split(line); + if (pieces.length != 7) { + throw new IOException("Illegal file format."); + } + + designation = pieces[0]; + diameter = Double.parseDouble(pieces[1]) / 1000.0; + length = Double.parseDouble(pieces[2]) / 1000.0; + + if (pieces[3].equalsIgnoreCase("None")) { + + } else { + buf = split(pieces[3], "[-,]+"); + for (int i = 0; i < buf.length; i++) { + if (buf[i].equalsIgnoreCase("P") || + buf[i].equalsIgnoreCase("plugged")) { + delays.add(Motor.PLUGGED); + } else { + // Many RASP files have "100" as an only delay + double d = Double.parseDouble(buf[i]); + if (d < 99) + delays.add(d); + } + } + Collections.sort(delays); + } + + propW = Double.parseDouble(pieces[4]); + totalW = Double.parseDouble(pieces[5]); + manufacturer = pieces[6]; + + if (propW > totalW) { + throw new IOException("Propellant weight exceeds total weight in " + + "RASP file " + filename); + } + + // Read the data + for (line = in.readLine(); (line != null) && (line.length() == 0 || line.charAt(0) != ';'); line = in.readLine()) { + + buf = split(line); + if (buf.length == 0) { + continue; + } else if (buf.length == 2) { + + time.add(Double.parseDouble(buf[0])); + thrust.add(Double.parseDouble(buf[1])); + + } else { + throw new IOException("Illegal file format."); + } + } + + // Comment of EOF encountered, marks the start of the next motor + if (time.size() < 2) { + throw new IOException("Illegal file format, too short thrust-curve."); + } + double[] delayArray = new double[delays.size()]; + for (int i = 0; i < delays.size(); i++) { + delayArray[i] = delays.get(i); + } + motors.add(createRASPMotor(manufacturer, designation, comment, + length, diameter, delayArray, propW, totalW, time, thrust)); + } + + } catch (NumberFormatException e) { + + throw new IOException("Illegal file format."); + + } + + return motors; + } + + + /** + * Create a motor from RASP file data. + * @throws IOException if the data is illegal for a thrust curve + */ + private static Motor createRASPMotor(String manufacturer, String designation, + String comment, double length, double diameter, double[] delays, + double propW, double totalW, List time, List thrust) + throws IOException { + + // Add zero time/thrust if necessary + sortLists(time, thrust); + finalizeThrustCurve(time, thrust); + List mass = calculateMass(time, thrust, totalW, propW); + + double[] timeArray = new double[time.size()]; + double[] thrustArray = new double[time.size()]; + Coordinate[] cgArray = new Coordinate[time.size()]; + for (int i = 0; i < time.size(); i++) { + timeArray[i] = time.get(i); + thrustArray[i] = thrust.get(i); + cgArray[i] = new Coordinate(length / 2, 0, 0, mass.get(i)); + } + + designation = removeDelay(designation); + + // Create the motor digest from data available in RASP files + MotorDigest motorDigest = new MotorDigest(); + motorDigest.update(DataType.TIME_ARRAY, timeArray); + motorDigest.update(DataType.MASS_SPECIFIC, totalW, totalW - propW); + motorDigest.update(DataType.FORCE_PER_TIME, thrustArray); + // TODO: HIGH: Motor digest? + // final String digest = motorDigest.getDigest(); + + + try { + + Manufacturer m = Manufacturer.getManufacturer(manufacturer); + return new ThrustCurveMotor(m, designation, comment, m.getMotorType(), + delays, diameter, length, timeArray, thrustArray, cgArray); + + } catch (IllegalArgumentException e) { + + // Bad data read from file. + throw new IOException("Illegal file format.", e); + + } + } +} diff --git a/core/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java b/core/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java new file mode 100644 index 00000000..d2bbcd26 --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java @@ -0,0 +1,481 @@ +package net.sf.openrocket.file.motor; + +import java.io.IOException; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.NullElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.file.simplesax.SimpleSAX; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.MotorDigest.DataType; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +public class RockSimMotorLoader extends AbstractMotorLoader { + + private static final LogHelper log = Application.getLogger(); + + public static final String CHARSET_NAME = "UTF-8"; + + public static final Charset CHARSET = Charset.forName(CHARSET_NAME); + + + /** Any delay longer than this will be interpreted as a plugged motor. */ + private static final int DELAY_LIMIT = 90; + + + + @Override + protected Charset getDefaultCharset() { + return CHARSET; + } + + + + /** + * Load a Motor from a RockSim motor definition file specified by the + * Reader. The Reader is responsible for using the correct + * charset. + *

+ * If automatic CG/mass calculation is used, then the CG is assumed to be located at + * the center of the motor casing and the mass is calculated from the thrust curve + * by assuming a constant exhaust velocity. + * + * @param reader the source of the file. + * @return a list of the {@link Motor} objects defined in the file. + * @throws IOException if an I/O error occurs or if the file format is invalid. + */ + @Override + public List load(Reader reader, String filename) throws IOException { + InputSource source = new InputSource(reader); + RSEHandler handler = new RSEHandler(); + WarningSet warnings = new WarningSet(); + + try { + SimpleSAX.readXML(source, handler, warnings); + return handler.getMotors(); + } catch (SAXException e) { + throw new IOException(e.getMessage(), e); + } + } + + + + /** + * Initial handler for the RockSim engine files. + */ + private static class RSEHandler extends ElementHandler { + private final List motors = new ArrayList(); + + private RSEMotorHandler motorHandler; + + public List getMotors() { + return motors; + } + + @Override + public ElementHandler openElement(String element, + HashMap attributes, WarningSet warnings) throws SAXException { + + if (element.equals("engine-database") || + element.equals("engine-list")) { + // Ignore and elements + return this; + } + + if (element.equals("version")) { + // Ignore elements completely + return null; + } + + if (element.equals("engine")) { + motorHandler = new RSEMotorHandler(attributes); + return motorHandler; + } + + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + if (element.equals("engine")) { + Motor motor = motorHandler.getMotor(); + motors.add(motor); + } + } + } + + + /** + * Handler for a RockSim engine file element. + */ + private static class RSEMotorHandler extends ElementHandler { + + private final String manufacturer; + private final String designation; + private final double[] delays; + private final double diameter; + private final double length; + private final double initMass; + private final double propMass; + private final Motor.Type type; + private boolean calculateMass; + private boolean calculateCG; + + private String description = ""; + + private List time; + private List force; + private List mass; + private List cg; + + private RSEMotorDataHandler dataHandler = null; + + + public RSEMotorHandler(HashMap attributes) throws SAXException { + String str; + + // Manufacturer + str = attributes.get("mfg"); + if (str == null) + throw new SAXException("Manufacturer missing"); + manufacturer = str; + + // Designation + str = attributes.get("code"); + if (str == null) + throw new SAXException("Designation missing"); + designation = removeDelay(str); + + // Delays + ArrayList delayList = new ArrayList(); + str = attributes.get("delays"); + if (str != null) { + String[] split = str.split(","); + for (String delay : split) { + try { + + double d = Double.parseDouble(delay); + if (d >= DELAY_LIMIT) + d = Motor.PLUGGED; + delayList.add(d); + + } catch (NumberFormatException e) { + if (str.equalsIgnoreCase("P") || str.equalsIgnoreCase("plugged")) { + delayList.add(Motor.PLUGGED); + } + } + } + } + delays = new double[delayList.size()]; + for (int i = 0; i < delayList.size(); i++) { + delays[i] = delayList.get(i); + } + + // Diameter + str = attributes.get("dia"); + if (str == null) + throw new SAXException("Diameter missing"); + try { + diameter = Double.parseDouble(str) / 1000.0; + } catch (NumberFormatException e) { + throw new SAXException("Invalid diameter " + str); + } + + // Length + str = attributes.get("len"); + if (str == null) + throw new SAXException("Length missing"); + try { + length = Double.parseDouble(str) / 1000.0; + } catch (NumberFormatException e) { + throw new SAXException("Invalid length " + str); + } + + // Initial mass + str = attributes.get("initWt"); + if (str == null) + throw new SAXException("Initial mass missing"); + try { + initMass = Double.parseDouble(str) / 1000.0; + } catch (NumberFormatException e) { + throw new SAXException("Invalid initial mass " + str); + } + + // Propellant mass + str = attributes.get("propWt"); + if (str == null) + throw new SAXException("Propellant mass missing"); + try { + propMass = Double.parseDouble(str) / 1000.0; + } catch (NumberFormatException e) { + throw new SAXException("Invalid propellant mass " + str); + } + + if (propMass > initMass) { + throw new SAXException("Propellant weight exceeds total weight in " + + "RockSim engine format"); + } + + // Motor type + str = attributes.get("Type"); + if (str != null && str.equalsIgnoreCase("single-use")) { + type = Motor.Type.SINGLE; + } else if (str != null && str.equalsIgnoreCase("hybrid")) { + type = Motor.Type.HYBRID; + } else if (str != null && str.equalsIgnoreCase("reloadable")) { + type = Motor.Type.RELOAD; + } else { + type = Motor.Type.UNKNOWN; + } + + // Calculate mass + str = attributes.get("auto-calc-mass"); + if ("0".equals(str) || "false".equalsIgnoreCase(str)) { + calculateMass = false; + } else { + calculateMass = true; + } + + // Calculate CG + str = attributes.get("auto-calc-cg"); + if ("0".equals(str) || "false".equalsIgnoreCase(str)) { + calculateCG = false; + } else { + calculateCG = true; + } + } + + @Override + public ElementHandler openElement(String element, + HashMap attributes, WarningSet warnings) throws SAXException { + + if (element.equals("comments")) { + return PlainTextHandler.INSTANCE; + } + + if (element.equals("data")) { + if (dataHandler != null) { + throw new SAXException("Multiple data elements encountered in motor " + + "definition"); + } + dataHandler = new RSEMotorDataHandler(); + return dataHandler; + } + + warnings.add("Unknown element '" + element + "' encountered, ignoring."); + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + + if (element.equals("comments")) { + if (description.length() > 0) { + description = description + "\n\n" + content.trim(); + } else { + description = content.trim(); + } + return; + } + + if (element.equals("data")) { + time = dataHandler.getTime(); + force = dataHandler.getForce(); + mass = dataHandler.getMass(); + cg = dataHandler.getCG(); + + sortLists(time, force, mass, cg); + + for (double d : mass) { + if (Double.isNaN(d)) { + calculateMass = true; + break; + } + } + for (double d : cg) { + if (Double.isNaN(d)) { + calculateCG = true; + break; + } + } + return; + } + } + + public Motor getMotor() throws SAXException { + if (time == null || time.size() == 0) + throw new SAXException("Illegal motor data"); + + + finalizeThrustCurve(time, force, mass, cg); + final int n = time.size(); + + if (hasIllegalValue(mass)) + calculateMass = true; + if (hasIllegalValue(cg)) + calculateCG = true; + + if (calculateMass) { + mass = calculateMass(time, force, initMass, propMass); + } + if (calculateCG) { + for (int i = 0; i < n; i++) { + cg.set(i, length / 2); + } + } + + double[] timeArray = toArray(time); + double[] thrustArray = toArray(force); + Coordinate[] cgArray = new Coordinate[n]; + for (int i = 0; i < n; i++) { + cgArray[i] = new Coordinate(cg.get(i), 0, 0, mass.get(i)); + } + + + // Create the motor digest from all data available in the file + MotorDigest motorDigest = new MotorDigest(); + motorDigest.update(DataType.TIME_ARRAY, timeArray); + if (!calculateMass) { + motorDigest.update(DataType.MASS_PER_TIME, toArray(mass)); + } else { + motorDigest.update(DataType.MASS_SPECIFIC, initMass, initMass - propMass); + } + if (!calculateCG) { + motorDigest.update(DataType.CG_PER_TIME, toArray(cg)); + } + motorDigest.update(DataType.FORCE_PER_TIME, thrustArray); + // TODO: HIGH: Motor digest? + // final String digest = motorDigest.getDigest(); + + + try { + Manufacturer m = Manufacturer.getManufacturer(manufacturer); + Motor.Type t = type; + if (t == Motor.Type.UNKNOWN) { + t = m.getMotorType(); + } else { + if (m.getMotorType() != Motor.Type.UNKNOWN && m.getMotorType() != t) { + log.warn("Loaded motor type inconsistent with manufacturer," + + " loaded type=" + t + " manufacturer=" + m + + " manufacturer type=" + m.getMotorType() + + " designation=" + designation); + } + } + + return new ThrustCurveMotor(m, designation, description, t, + delays, diameter, length, timeArray, thrustArray, cgArray); + } catch (IllegalArgumentException e) { + throw new SAXException("Illegal motor data", e); + } + } + } + + + /** + * Handler for the element in a RockSim engine file motor definition. + */ + private static class RSEMotorDataHandler extends ElementHandler { + + private final List time = new ArrayList(); + private final List force = new ArrayList(); + private final List mass = new ArrayList(); + private final List cg = new ArrayList(); + + + public List getTime() { + return time; + } + + public List getForce() { + return force; + } + + public List getMass() { + return mass; + } + + public List getCG() { + return cg; + } + + + @Override + public ElementHandler openElement(String element, + HashMap attributes, WarningSet warnings) { + + if (element.equals("eng-data")) { + return NullElementHandler.INSTANCE; + } + + warnings.add("Unknown element '" + element + "' encountered, ignoring."); + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + double t = parseDouble(attributes.get("t")); + double f = parseDouble(attributes.get("f")); + double m = parseDouble(attributes.get("m")) / 1000.0; + double g = parseDouble(attributes.get("cg")) / 1000.0; + + if (Double.isNaN(t) || Double.isNaN(f)) { + throw new SAXException("Illegal motor data point encountered"); + } + + time.add(t); + force.add(f); + mass.add(m); + cg.add(g); + } + + + private double parseDouble(String str) { + if (str == null) + return Double.NaN; + try { + return Double.parseDouble(str); + } catch (NumberFormatException e) { + return Double.NaN; + } + } + } + + + + private static boolean hasIllegalValue(List list) { + for (Double d : list) { + if (d == null || d.isNaN() || d.isInfinite()) { + return true; + } + } + return false; + } + + private static double[] toArray(List list) { + final int n = list.size(); + double[] array = new double[n]; + for (int i = 0; i < n; i++) { + array[i] = list.get(i); + } + return array; + } +} diff --git a/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java b/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java new file mode 100644 index 00000000..804226f0 --- /dev/null +++ b/core/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java @@ -0,0 +1,85 @@ +package net.sf.openrocket.file.motor; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import net.sf.openrocket.file.UnknownFileTypeException; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.UncloseableInputStream; + +/** + * A motor loader that loads motors from a ZIP file. + * + * @author Sampo Niskanen + */ +public class ZipFileMotorLoader implements MotorLoader { + private static final LogHelper log = Application.getLogger(); + + private final MotorLoader loader; + + + /** + * Construct a ZipFileMotorLoader that loads files using a + * {@link GeneralMotorLoader}. + */ + public ZipFileMotorLoader() { + this(new GeneralMotorLoader()); + } + + /** + * Constructs a ZipFileMotorLoader that loads files using the provided motor loader. + * + * @param loader the motor loader to use when loading. + */ + public ZipFileMotorLoader(MotorLoader loader) { + this.loader = loader; + } + + + @Override + public List load(InputStream stream, String filename) throws IOException { + List motors = new ArrayList(); + + ZipInputStream is = new ZipInputStream(stream); + + // SAX seems to close the input stream, prevent it + InputStream uncloseable = new UncloseableInputStream(is); + + while (true) { + ZipEntry entry = is.getNextEntry(); + if (entry == null) + break; + + if (entry.isDirectory()) + continue; + + // Get the file name of the entry + String name = entry.getName(); + int index = name.lastIndexOf('/'); + if (index < 0) { + index = name.lastIndexOf('\\'); + } + if (index >= 0) { + name = name.substring(index + 1); + } + + try { + List m = loader.load(uncloseable, entry.getName()); + motors.addAll(m); + log.info("Loaded " + m.size() + " motors from ZIP entry " + entry.getName()); + } catch (UnknownFileTypeException e) { + log.info("Could not read ZIP entry " + entry.getName() + ": " + e.getMessage()); + } + + } + + return motors; + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java new file mode 100644 index 00000000..c4fc7d73 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java @@ -0,0 +1,2074 @@ +package net.sf.openrocket.file.openrocket; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.database.Databases; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.document.Simulation.Status; +import net.sf.openrocket.document.StorageOptions; +import net.sf.openrocket.file.RocketLoadException; +import net.sf.openrocket.file.RocketLoader; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.file.simplesax.SimpleSAX; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.BodyComponent; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.ClusterConfiguration; +import net.sf.openrocket.rocketcomponent.Clusterable; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.IllegalFinPointException; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.InternalComponent; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RadiusRingComponent; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.ReferenceType; +import net.sf.openrocket.rocketcomponent.RingComponent; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent.Position; +import net.sf.openrocket.rocketcomponent.ShockCord; +import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.StructuralComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.FlightEvent.Type; +import net.sf.openrocket.simulation.SimulationOptions; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Color; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.GeodeticComputationStrategy; +import net.sf.openrocket.util.LineStyle; +import net.sf.openrocket.util.Reflection; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + + +/** + * Class that loads a rocket definition from an OpenRocket rocket file. + *

+ * This class uses SAX to read the XML file format. The + * {@link #loadFromStream(InputStream)} method simply sets the system up and + * starts the parsing, while the actual logic is in the private inner class + * OpenRocketHandler. + * + * @author Sampo Niskanen + */ +public class OpenRocketLoader extends RocketLoader { + private static final LogHelper log = Application.getLogger(); + + + @Override + public OpenRocketDocument loadFromStream(InputStream source) throws RocketLoadException, + IOException { + log.info("Loading .ork file"); + + InputSource xmlSource = new InputSource(source); + OpenRocketHandler handler = new OpenRocketHandler(); + + + try { + SimpleSAX.readXML(xmlSource, handler, warnings); + } catch (SAXException e) { + log.warn("Malformed XML in input"); + throw new RocketLoadException("Malformed XML in input.", e); + } + + + OpenRocketDocument doc = handler.getDocument(); + doc.getDefaultConfiguration().setAllStages(); + + // Deduce suitable time skip + double timeSkip = StorageOptions.SIMULATION_DATA_NONE; + for (Simulation s : doc.getSimulations()) { + if (s.getStatus() == Simulation.Status.EXTERNAL || + s.getStatus() == Simulation.Status.NOT_SIMULATED) + continue; + if (s.getSimulatedData() == null) + continue; + if (s.getSimulatedData().getBranchCount() == 0) + continue; + FlightDataBranch branch = s.getSimulatedData().getBranch(0); + if (branch == null) + continue; + List list = branch.get(FlightDataType.TYPE_TIME); + if (list == null) + continue; + + double previousTime = Double.NaN; + for (double time : list) { + if (time - previousTime < timeSkip) + timeSkip = time - previousTime; + previousTime = time; + } + } + // Round value + timeSkip = Math.rint(timeSkip * 100) / 100; + + doc.getDefaultStorageOptions().setSimulationTimeSkip(timeSkip); + doc.getDefaultStorageOptions().setCompressionEnabled(false); // Set by caller if compressed + doc.getDefaultStorageOptions().setExplicitlySet(false); + + doc.clearUndo(); + log.info("Loading done"); + return doc; + } + +} + + + +class DocumentConfig { + + /* Remember to update OpenRocketSaver as well! */ + public static final String[] SUPPORTED_VERSIONS = { "0.9", "1.0", "1.1", "1.2", "1.3" }; + + + //////// Component constructors + static final HashMap> constructors = new HashMap>(); + static { + try { + // External components + constructors.put("bodytube", BodyTube.class.getConstructor(new Class[0])); + constructors.put("transition", Transition.class.getConstructor(new Class[0])); + constructors.put("nosecone", NoseCone.class.getConstructor(new Class[0])); + constructors.put("trapezoidfinset", TrapezoidFinSet.class.getConstructor(new Class[0])); + constructors.put("ellipticalfinset", EllipticalFinSet.class.getConstructor(new Class[0])); + constructors.put("freeformfinset", FreeformFinSet.class.getConstructor(new Class[0])); + constructors.put("launchlug", LaunchLug.class.getConstructor(new Class[0])); + + // Internal components + constructors.put("engineblock", EngineBlock.class.getConstructor(new Class[0])); + constructors.put("innertube", InnerTube.class.getConstructor(new Class[0])); + constructors.put("tubecoupler", TubeCoupler.class.getConstructor(new Class[0])); + constructors.put("bulkhead", Bulkhead.class.getConstructor(new Class[0])); + constructors.put("centeringring", CenteringRing.class.getConstructor(new Class[0])); + + constructors.put("masscomponent", MassComponent.class.getConstructor(new Class[0])); + constructors.put("shockcord", ShockCord.class.getConstructor(new Class[0])); + constructors.put("parachute", Parachute.class.getConstructor(new Class[0])); + constructors.put("streamer", Streamer.class.getConstructor(new Class[0])); + + // Other + constructors.put("stage", Stage.class.getConstructor(new Class[0])); + + } catch (NoSuchMethodException e) { + throw new BugException( + "Error in constructing the 'constructors' HashMap."); + } + } + + + //////// Parameter setters + /* + * The keys are of the form Class:param, where Class is the class name and param + * the element name. Setters are searched for in descending class order. + * A setter of null means setting the parameter is not allowed. + */ + static final HashMap setters = new HashMap(); + static { + // RocketComponent + setters.put("RocketComponent:name", new StringSetter( + Reflection.findMethod(RocketComponent.class, "setName", String.class))); + setters.put("RocketComponent:color", new ColorSetter( + Reflection.findMethod(RocketComponent.class, "setColor", Color.class))); + setters.put("RocketComponent:linestyle", new EnumSetter( + Reflection.findMethod(RocketComponent.class, "setLineStyle", LineStyle.class), + LineStyle.class)); + setters.put("RocketComponent:position", new PositionSetter()); + setters.put("RocketComponent:overridemass", new OverrideSetter( + Reflection.findMethod(RocketComponent.class, "setOverrideMass", double.class), + Reflection.findMethod(RocketComponent.class, "setMassOverridden", boolean.class))); + setters.put("RocketComponent:overridecg", new OverrideSetter( + Reflection.findMethod(RocketComponent.class, "setOverrideCGX", double.class), + Reflection.findMethod(RocketComponent.class, "setCGOverridden", boolean.class))); + setters.put("RocketComponent:overridesubcomponents", new BooleanSetter( + Reflection.findMethod(RocketComponent.class, "setOverrideSubcomponents", boolean.class))); + setters.put("RocketComponent:comment", new StringSetter( + Reflection.findMethod(RocketComponent.class, "setComment", String.class))); + + // ExternalComponent + setters.put("ExternalComponent:finish", new EnumSetter( + Reflection.findMethod(ExternalComponent.class, "setFinish", Finish.class), + Finish.class)); + setters.put("ExternalComponent:material", new MaterialSetter( + Reflection.findMethod(ExternalComponent.class, "setMaterial", Material.class), + Material.Type.BULK)); + + // BodyComponent + setters.put("BodyComponent:length", new DoubleSetter( + Reflection.findMethod(BodyComponent.class, "setLength", double.class))); + + // SymmetricComponent + setters.put("SymmetricComponent:thickness", new DoubleSetter( + Reflection.findMethod(SymmetricComponent.class, "setThickness", double.class), + "filled", + Reflection.findMethod(SymmetricComponent.class, "setFilled", boolean.class))); + + // BodyTube + setters.put("BodyTube:radius", new DoubleSetter( + Reflection.findMethod(BodyTube.class, "setOuterRadius", double.class), + "auto", + Reflection.findMethod(BodyTube.class, "setOuterRadiusAutomatic", boolean.class))); + + // Transition + setters.put("Transition:shape", new EnumSetter( + Reflection.findMethod(Transition.class, "setType", Transition.Shape.class), + Transition.Shape.class)); + setters.put("Transition:shapeclipped", new BooleanSetter( + Reflection.findMethod(Transition.class, "setClipped", boolean.class))); + setters.put("Transition:shapeparameter", new DoubleSetter( + Reflection.findMethod(Transition.class, "setShapeParameter", double.class))); + + setters.put("Transition:foreradius", new DoubleSetter( + Reflection.findMethod(Transition.class, "setForeRadius", double.class), + "auto", + Reflection.findMethod(Transition.class, "setForeRadiusAutomatic", boolean.class))); + setters.put("Transition:aftradius", new DoubleSetter( + Reflection.findMethod(Transition.class, "setAftRadius", double.class), + "auto", + Reflection.findMethod(Transition.class, "setAftRadiusAutomatic", boolean.class))); + + setters.put("Transition:foreshoulderradius", new DoubleSetter( + Reflection.findMethod(Transition.class, "setForeShoulderRadius", double.class))); + setters.put("Transition:foreshoulderlength", new DoubleSetter( + Reflection.findMethod(Transition.class, "setForeShoulderLength", double.class))); + setters.put("Transition:foreshoulderthickness", new DoubleSetter( + Reflection.findMethod(Transition.class, "setForeShoulderThickness", double.class))); + setters.put("Transition:foreshouldercapped", new BooleanSetter( + Reflection.findMethod(Transition.class, "setForeShoulderCapped", boolean.class))); + + setters.put("Transition:aftshoulderradius", new DoubleSetter( + Reflection.findMethod(Transition.class, "setAftShoulderRadius", double.class))); + setters.put("Transition:aftshoulderlength", new DoubleSetter( + Reflection.findMethod(Transition.class, "setAftShoulderLength", double.class))); + setters.put("Transition:aftshoulderthickness", new DoubleSetter( + Reflection.findMethod(Transition.class, "setAftShoulderThickness", double.class))); + setters.put("Transition:aftshouldercapped", new BooleanSetter( + Reflection.findMethod(Transition.class, "setAftShoulderCapped", boolean.class))); + + // NoseCone - disable disallowed elements + setters.put("NoseCone:foreradius", null); + setters.put("NoseCone:foreshoulderradius", null); + setters.put("NoseCone:foreshoulderlength", null); + setters.put("NoseCone:foreshoulderthickness", null); + setters.put("NoseCone:foreshouldercapped", null); + + // FinSet + setters.put("FinSet:fincount", new IntSetter( + Reflection.findMethod(FinSet.class, "setFinCount", int.class))); + setters.put("FinSet:rotation", new DoubleSetter( + Reflection.findMethod(FinSet.class, "setBaseRotation", double.class), Math.PI / 180.0)); + setters.put("FinSet:thickness", new DoubleSetter( + Reflection.findMethod(FinSet.class, "setThickness", double.class))); + setters.put("FinSet:crosssection", new EnumSetter( + Reflection.findMethod(FinSet.class, "setCrossSection", FinSet.CrossSection.class), + FinSet.CrossSection.class)); + setters.put("FinSet:cant", new DoubleSetter( + Reflection.findMethod(FinSet.class, "setCantAngle", double.class), Math.PI / 180.0)); + setters.put("FinSet:tabheight", new DoubleSetter( + Reflection.findMethod(FinSet.class, "setTabHeight", double.class))); + setters.put("FinSet:tablength", new DoubleSetter( + Reflection.findMethod(FinSet.class, "setTabLength", double.class))); + setters.put("FinSet:tabposition", new FinTabPositionSetter()); + + // TrapezoidFinSet + setters.put("TrapezoidFinSet:rootchord", new DoubleSetter( + Reflection.findMethod(TrapezoidFinSet.class, "setRootChord", double.class))); + setters.put("TrapezoidFinSet:tipchord", new DoubleSetter( + Reflection.findMethod(TrapezoidFinSet.class, "setTipChord", double.class))); + setters.put("TrapezoidFinSet:sweeplength", new DoubleSetter( + Reflection.findMethod(TrapezoidFinSet.class, "setSweep", double.class))); + setters.put("TrapezoidFinSet:height", new DoubleSetter( + Reflection.findMethod(TrapezoidFinSet.class, "setHeight", double.class))); + + // EllipticalFinSet + setters.put("EllipticalFinSet:rootchord", new DoubleSetter( + Reflection.findMethod(EllipticalFinSet.class, "setLength", double.class))); + setters.put("EllipticalFinSet:height", new DoubleSetter( + Reflection.findMethod(EllipticalFinSet.class, "setHeight", double.class))); + + // FreeformFinSet points handled as a special handler + + // LaunchLug + setters.put("LaunchLug:radius", new DoubleSetter( + Reflection.findMethod(LaunchLug.class, "setOuterRadius", double.class))); + setters.put("LaunchLug:length", new DoubleSetter( + Reflection.findMethod(LaunchLug.class, "setLength", double.class))); + setters.put("LaunchLug:thickness", new DoubleSetter( + Reflection.findMethod(LaunchLug.class, "setThickness", double.class))); + setters.put("LaunchLug:radialdirection", new DoubleSetter( + Reflection.findMethod(LaunchLug.class, "setRadialDirection", double.class), + Math.PI / 180.0)); + + // InternalComponent - nothing + + // StructuralComponent + setters.put("StructuralComponent:material", new MaterialSetter( + Reflection.findMethod(StructuralComponent.class, "setMaterial", Material.class), + Material.Type.BULK)); + + // RingComponent + setters.put("RingComponent:length", new DoubleSetter( + Reflection.findMethod(RingComponent.class, "setLength", double.class))); + setters.put("RingComponent:radialposition", new DoubleSetter( + Reflection.findMethod(RingComponent.class, "setRadialPosition", double.class))); + setters.put("RingComponent:radialdirection", new DoubleSetter( + Reflection.findMethod(RingComponent.class, "setRadialDirection", double.class), + Math.PI / 180.0)); + + // ThicknessRingComponent - radius on separate components due to differing automatics + setters.put("ThicknessRingComponent:thickness", new DoubleSetter( + Reflection.findMethod(ThicknessRingComponent.class, "setThickness", double.class))); + + // EngineBlock + setters.put("EngineBlock:outerradius", new DoubleSetter( + Reflection.findMethod(EngineBlock.class, "setOuterRadius", double.class), + "auto", + Reflection.findMethod(EngineBlock.class, "setOuterRadiusAutomatic", boolean.class))); + + // TubeCoupler + setters.put("TubeCoupler:outerradius", new DoubleSetter( + Reflection.findMethod(TubeCoupler.class, "setOuterRadius", double.class), + "auto", + Reflection.findMethod(TubeCoupler.class, "setOuterRadiusAutomatic", boolean.class))); + + // InnerTube + setters.put("InnerTube:outerradius", new DoubleSetter( + Reflection.findMethod(InnerTube.class, "setOuterRadius", double.class))); + setters.put("InnerTube:clusterconfiguration", new ClusterConfigurationSetter()); + setters.put("InnerTube:clusterscale", new DoubleSetter( + Reflection.findMethod(InnerTube.class, "setClusterScale", double.class))); + setters.put("InnerTube:clusterrotation", new DoubleSetter( + Reflection.findMethod(InnerTube.class, "setClusterRotation", double.class), + Math.PI / 180.0)); + + // RadiusRingComponent + + // Bulkhead + setters.put("RadiusRingComponent:innerradius", new DoubleSetter( + Reflection.findMethod(RadiusRingComponent.class, "setInnerRadius", double.class))); + setters.put("Bulkhead:outerradius", new DoubleSetter( + Reflection.findMethod(Bulkhead.class, "setOuterRadius", double.class), + "auto", + Reflection.findMethod(Bulkhead.class, "setOuterRadiusAutomatic", boolean.class))); + + // CenteringRing + setters.put("CenteringRing:innerradius", new DoubleSetter( + Reflection.findMethod(CenteringRing.class, "setInnerRadius", double.class), + "auto", + Reflection.findMethod(CenteringRing.class, "setInnerRadiusAutomatic", boolean.class))); + setters.put("CenteringRing:outerradius", new DoubleSetter( + Reflection.findMethod(CenteringRing.class, "setOuterRadius", double.class), + "auto", + Reflection.findMethod(CenteringRing.class, "setOuterRadiusAutomatic", boolean.class))); + + + // MassObject + setters.put("MassObject:packedlength", new DoubleSetter( + Reflection.findMethod(MassObject.class, "setLength", double.class))); + setters.put("MassObject:packedradius", new DoubleSetter( + Reflection.findMethod(MassObject.class, "setRadius", double.class))); + setters.put("MassObject:radialposition", new DoubleSetter( + Reflection.findMethod(MassObject.class, "setRadialPosition", double.class))); + setters.put("MassObject:radialdirection", new DoubleSetter( + Reflection.findMethod(MassObject.class, "setRadialDirection", double.class), + Math.PI / 180.0)); + + // MassComponent + setters.put("MassComponent:mass", new DoubleSetter( + Reflection.findMethod(MassComponent.class, "setComponentMass", double.class))); + + // ShockCord + setters.put("ShockCord:cordlength", new DoubleSetter( + Reflection.findMethod(ShockCord.class, "setCordLength", double.class))); + setters.put("ShockCord:material", new MaterialSetter( + Reflection.findMethod(ShockCord.class, "setMaterial", Material.class), + Material.Type.LINE)); + + // RecoveryDevice + setters.put("RecoveryDevice:cd", new DoubleSetter( + Reflection.findMethod(RecoveryDevice.class, "setCD", double.class), + "auto", + Reflection.findMethod(RecoveryDevice.class, "setCDAutomatic", boolean.class))); + setters.put("RecoveryDevice:deployevent", new EnumSetter( + Reflection.findMethod(RecoveryDevice.class, "setDeployEvent", RecoveryDevice.DeployEvent.class), + RecoveryDevice.DeployEvent.class)); + setters.put("RecoveryDevice:deployaltitude", new DoubleSetter( + Reflection.findMethod(RecoveryDevice.class, "setDeployAltitude", double.class))); + setters.put("RecoveryDevice:deploydelay", new DoubleSetter( + Reflection.findMethod(RecoveryDevice.class, "setDeployDelay", double.class))); + setters.put("RecoveryDevice:material", new MaterialSetter( + Reflection.findMethod(RecoveryDevice.class, "setMaterial", Material.class), + Material.Type.SURFACE)); + + // Parachute + setters.put("Parachute:diameter", new DoubleSetter( + Reflection.findMethod(Parachute.class, "setDiameter", double.class))); + setters.put("Parachute:linecount", new IntSetter( + Reflection.findMethod(Parachute.class, "setLineCount", int.class))); + setters.put("Parachute:linelength", new DoubleSetter( + Reflection.findMethod(Parachute.class, "setLineLength", double.class))); + setters.put("Parachute:linematerial", new MaterialSetter( + Reflection.findMethod(Parachute.class, "setLineMaterial", Material.class), + Material.Type.LINE)); + + // Streamer + setters.put("Streamer:striplength", new DoubleSetter( + Reflection.findMethod(Streamer.class, "setStripLength", double.class))); + setters.put("Streamer:stripwidth", new DoubleSetter( + Reflection.findMethod(Streamer.class, "setStripWidth", double.class))); + + // Rocket + // handled by separate handler + setters.put("Rocket:referencetype", new EnumSetter( + Reflection.findMethod(Rocket.class, "setReferenceType", ReferenceType.class), + ReferenceType.class)); + setters.put("Rocket:customreference", new DoubleSetter( + Reflection.findMethod(Rocket.class, "setCustomReferenceLength", double.class))); + setters.put("Rocket:designer", new StringSetter( + Reflection.findMethod(Rocket.class, "setDesigner", String.class))); + setters.put("Rocket:revision", new StringSetter( + Reflection.findMethod(Rocket.class, "setRevision", String.class))); + } + + + /** + * Search for a enum value that has the corresponding name as an XML value. The current + * conversion from enum name to XML value is to lowercase the name and strip out all + * underscore characters. This method returns a match to these criteria, or null + * if no such enum exists. + * + * @param then enum type. + * @param name the XML value, null ok. + * @param enumClass the class of the enum. + * @return the found enum value, or null. + */ + public static > Enum findEnum(String name, + Class> enumClass) { + + if (name == null) + return null; + name = name.trim(); + for (Enum e : enumClass.getEnumConstants()) { + if (e.name().toLowerCase().replace("_", "").equals(name)) { + return e; + } + } + return null; + } + + + /** + * Convert a string to a double including formatting specifications of the OpenRocket + * file format. This accepts all formatting that is valid for + * Double.parseDouble(s) and a few others as well ("Inf", "-Inf"). + * + * @param s the string to parse. + * @return the numerical value. + * @throws NumberFormatException the the string cannot be parsed. + */ + public static double stringToDouble(String s) throws NumberFormatException { + if (s == null) + throw new NumberFormatException("null string"); + if (s.equalsIgnoreCase("NaN")) + return Double.NaN; + if (s.equalsIgnoreCase("Inf")) + return Double.POSITIVE_INFINITY; + if (s.equalsIgnoreCase("-Inf")) + return Double.NEGATIVE_INFINITY; + return Double.parseDouble(s); + } +} + + + + + +/** + * The starting point of the handlers. Accepts a single element and hands + * the contents to be read by a OpenRocketContentsHandler. + */ +class OpenRocketHandler extends ElementHandler { + private OpenRocketContentHandler handler = null; + + /** + * Return the OpenRocketDocument read from the file, or null if a document + * has not been read yet. + * + * @return the document read, or null. + */ + public OpenRocketDocument getDocument() { + return handler.getDocument(); + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + // Check for unknown elements + if (!element.equals("openrocket")) { + warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); + return null; + } + + // Check for first call + if (handler != null) { + warnings.add(Warning.fromString("Multiple document elements found, ignoring later " + + "ones.")); + return null; + } + + // Check version number + String version = null; + String creator = attributes.remove("creator"); + String docVersion = attributes.remove("version"); + for (String v : DocumentConfig.SUPPORTED_VERSIONS) { + if (v.equals(docVersion)) { + version = v; + break; + } + } + if (version == null) { + String str = "Unsupported document version"; + if (docVersion != null) + str += " " + docVersion; + if (creator != null && !creator.trim().equals("")) + str += " (written using '" + creator.trim() + "')"; + str += ", attempting to read file anyway."; + warnings.add(str); + } + + handler = new OpenRocketContentHandler(); + return handler; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + attributes.remove("version"); + attributes.remove("creator"); + super.closeElement(element, attributes, content, warnings); + } + + +} + + +/** + * Handles the content of the tag. + */ +class OpenRocketContentHandler extends ElementHandler { + private final OpenRocketDocument doc; + private final Rocket rocket; + + private boolean rocketDefined = false; + private boolean simulationsDefined = false; + + public OpenRocketContentHandler() { + this.rocket = new Rocket(); + this.doc = new OpenRocketDocument(rocket); + } + + + public OpenRocketDocument getDocument() { + if (!rocketDefined) + return null; + return doc; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (element.equals("rocket")) { + if (rocketDefined) { + warnings.add(Warning + .fromString("Multiple rocket designs within one document, " + + "ignoring later ones.")); + return null; + } + rocketDefined = true; + return new ComponentParameterHandler(rocket); + } + + if (element.equals("simulations")) { + if (simulationsDefined) { + warnings.add(Warning + .fromString("Multiple simulation definitions within one document, " + + "ignoring later ones.")); + return null; + } + simulationsDefined = true; + return new SimulationsHandler(doc); + } + + warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); + + return null; + } +} + + + + +/** + * A handler that creates components from the corresponding elements. The control of the + * contents is passed on to ComponentParameterHandler. + */ +class ComponentHandler extends ElementHandler { + private final RocketComponent parent; + + public ComponentHandler(RocketComponent parent) { + this.parent = parent; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + // Attempt to construct new component + Constructor constructor = DocumentConfig.constructors + .get(element); + if (constructor == null) { + warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); + return null; + } + + RocketComponent c; + try { + c = constructor.newInstance(); + } catch (InstantiationException e) { + throw new BugException("Error constructing component.", e); + } catch (IllegalAccessException e) { + throw new BugException("Error constructing component.", e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + + parent.addChild(c); + + return new ComponentParameterHandler(c); + } +} + + +/** + * A handler that populates the parameters of a previously constructed rocket component. + * This uses the setters, or delegates the handling to another handler for specific + * elements. + */ +class ComponentParameterHandler extends ElementHandler { + private final RocketComponent component; + + public ComponentParameterHandler(RocketComponent c) { + this.component = c; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + // Check for specific elements that contain other elements + if (element.equals("subcomponents")) { + return new ComponentHandler(component); + } + if (element.equals("motormount")) { + if (!(component instanceof MotorMount)) { + warnings.add(Warning.fromString("Illegal component defined as motor mount.")); + return null; + } + return new MotorMountHandler((MotorMount) component); + } + if (element.equals("finpoints")) { + if (!(component instanceof FreeformFinSet)) { + warnings.add(Warning.fromString("Illegal component defined for fin points.")); + return null; + } + return new FinSetPointHandler((FreeformFinSet) component); + } + if (element.equals("motorconfiguration")) { + if (!(component instanceof Rocket)) { + warnings.add(Warning.fromString("Illegal component defined for motor configuration.")); + return null; + } + return new MotorConfigurationHandler((Rocket) component); + } + + + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + + if (element.equals("subcomponents") || element.equals("motormount") || + element.equals("finpoints") || element.equals("motorconfiguration")) { + return; + } + + // Search for the correct setter class + + Class c; + for (c = component.getClass(); c != null; c = c.getSuperclass()) { + String setterKey = c.getSimpleName() + ":" + element; + Setter s = DocumentConfig.setters.get(setterKey); + if (s != null) { + // Setter found + s.set(component, content, attributes, warnings); + break; + } + if (DocumentConfig.setters.containsKey(setterKey)) { + // Key exists but is null -> invalid parameter + c = null; + break; + } + } + if (c == null) { + warnings.add(Warning.fromString("Unknown parameter type '" + element + "' for " + + component.getComponentName() + ", ignoring.")); + } + } +} + + +/** + * A handler that reads the specifications within the freeformfinset's + * elements. + */ +class FinSetPointHandler extends ElementHandler { + private final FreeformFinSet finset; + private final ArrayList coordinates = new ArrayList(); + + public FinSetPointHandler(FreeformFinSet finset) { + this.finset = finset; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + String strx = attributes.remove("x"); + String stry = attributes.remove("y"); + if (strx == null || stry == null) { + warnings.add(Warning.fromString("Illegal fin points specification, ignoring.")); + return; + } + try { + double x = Double.parseDouble(strx); + double y = Double.parseDouble(stry); + coordinates.add(new Coordinate(x, y)); + } catch (NumberFormatException e) { + warnings.add(Warning.fromString("Illegal fin points specification, ignoring.")); + return; + } + + super.closeElement(element, attributes, content, warnings); + } + + @Override + public void endHandler(String element, HashMap attributes, + String content, WarningSet warnings) { + try { + finset.setPoints(coordinates.toArray(new Coordinate[0])); + } catch (IllegalFinPointException e) { + warnings.add(Warning.fromString("Freeform fin set point definitions illegal, ignoring.")); + } + } +} + + +class MotorMountHandler extends ElementHandler { + private final MotorMount mount; + private MotorHandler motorHandler; + + public MotorMountHandler(MotorMount mount) { + this.mount = mount; + mount.setMotorMount(true); + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (element.equals("motor")) { + motorHandler = new MotorHandler(); + return motorHandler; + } + + if (element.equals("ignitionevent") || + element.equals("ignitiondelay") || + element.equals("overhang")) { + return PlainTextHandler.INSTANCE; + } + + warnings.add(Warning.fromString("Unknown element '" + element + "' encountered, ignoring.")); + return null; + } + + + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + if (element.equals("motor")) { + String id = attributes.get("configid"); + if (id == null || id.equals("")) { + warnings.add(Warning.fromString("Illegal motor specification, ignoring.")); + return; + } + + Motor motor = motorHandler.getMotor(warnings); + mount.setMotor(id, motor); + mount.setMotorDelay(id, motorHandler.getDelay(warnings)); + return; + } + + if (element.equals("ignitionevent")) { + MotorMount.IgnitionEvent event = null; + for (MotorMount.IgnitionEvent e : MotorMount.IgnitionEvent.values()) { + if (e.name().toLowerCase().replaceAll("_", "").equals(content)) { + event = e; + break; + } + } + if (event == null) { + warnings.add(Warning.fromString("Unknown ignition event type '" + content + "', ignoring.")); + return; + } + mount.setIgnitionEvent(event); + return; + } + + if (element.equals("ignitiondelay")) { + double d; + try { + d = Double.parseDouble(content); + } catch (NumberFormatException nfe) { + warnings.add(Warning.fromString("Illegal ignition delay specified, ignoring.")); + return; + } + mount.setIgnitionDelay(d); + return; + } + + if (element.equals("overhang")) { + double d; + try { + d = Double.parseDouble(content); + } catch (NumberFormatException nfe) { + warnings.add(Warning.fromString("Illegal overhang specified, ignoring.")); + return; + } + mount.setMotorOverhang(d); + return; + } + + super.closeElement(element, attributes, content, warnings); + } +} + + + + +class MotorConfigurationHandler extends ElementHandler { + private final Rocket rocket; + private String name = null; + private boolean inNameElement = false; + + public MotorConfigurationHandler(Rocket rocket) { + this.rocket = rocket; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (inNameElement || !element.equals("name")) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return null; + } + inNameElement = true; + + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + name = content; + } + + @Override + public void endHandler(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + String configid = attributes.remove("configid"); + if (configid == null || configid.equals("")) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + if (!rocket.addMotorConfigurationID(configid)) { + warnings.add("Duplicate motor configuration ID used."); + return; + } + + if (name != null && name.trim().length() > 0) { + rocket.setMotorConfigurationName(configid, name); + } + + if ("true".equals(attributes.remove("default"))) { + rocket.getDefaultConfiguration().setMotorConfigurationID(configid); + } + + super.closeElement(element, attributes, content, warnings); + } +} + + +class MotorHandler extends ElementHandler { + private Motor.Type type = null; + private String manufacturer = null; + private String designation = null; + private String digest = null; + private double diameter = Double.NaN; + private double length = Double.NaN; + private double delay = Double.NaN; + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + + /** + * Return the motor to use, or null. + */ + public Motor getMotor(WarningSet warnings) { + if (designation == null) { + warnings.add(Warning.fromString("No motor specified, ignoring.")); + return null; + } + + List motors = Application.getMotorSetDatabase().findMotors(type, manufacturer, + designation, diameter, length); + + // No motors + if (motors.size() == 0) { + String str = "No motor with designation '" + designation + "'"; + if (manufacturer != null) + str += " for manufacturer '" + manufacturer + "'"; + str += " found."; + warnings.add(str); + return null; + } + + // One motor + if (motors.size() == 1) { + Motor m = motors.get(0); + if (digest != null && !MotorDigest.digestMotor(m).equals(digest)) { + String str = "Motor with designation '" + designation + "'"; + if (manufacturer != null) + str += " for manufacturer '" + manufacturer + "'"; + str += " has differing thrust curve than the original."; + warnings.add(str); + } + return m; + } + + // Multiple motors, check digest for which one to use + if (digest != null) { + + // Check for motor with correct digest + for (Motor m : motors) { + if (MotorDigest.digestMotor(m).equals(digest)) { + return m; + } + } + String str = "Motor with designation '" + designation + "'"; + if (manufacturer != null) + str += " for manufacturer '" + manufacturer + "'"; + str += " has differing thrust curve than the original."; + warnings.add(str); + + } else { + + // No digest, check for preferred digest (OpenRocket <= 1.1.0) + // TODO: MEDIUM: This should only be done for document versions 1.1 and below + for (Motor m : motors) { + if (PreferredMotorDigests.DIGESTS.contains(MotorDigest.digestMotor(m))) { + return m; + } + } + + String str = "Multiple motors with designation '" + designation + "'"; + if (manufacturer != null) + str += " for manufacturer '" + manufacturer + "'"; + str += " found, one chosen arbitrarily."; + warnings.add(str); + + } + return motors.get(0); + } + + /** + * Return the delay to use for the motor. + */ + public double getDelay(WarningSet warnings) { + if (Double.isNaN(delay)) { + warnings.add(Warning.fromString("Motor delay not specified, assuming no ejection charge.")); + return Motor.PLUGGED; + } + return delay; + } + + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + content = content.trim(); + + if (element.equals("type")) { + + // Motor type + type = null; + for (Motor.Type t : Motor.Type.values()) { + if (t.name().toLowerCase().equals(content.trim())) { + type = t; + break; + } + } + if (type == null) { + warnings.add(Warning.fromString("Unknown motor type '" + content + "', ignoring.")); + } + + } else if (element.equals("manufacturer")) { + + // Manufacturer + manufacturer = content.trim(); + + } else if (element.equals("designation")) { + + // Designation + designation = content.trim(); + + } else if (element.equals("digest")) { + + // Digest + digest = content.trim(); + + } else if (element.equals("diameter")) { + + // Diameter + diameter = Double.NaN; + try { + diameter = Double.parseDouble(content.trim()); + } catch (NumberFormatException e) { + // Ignore + } + if (Double.isNaN(diameter)) { + warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring.")); + } + + } else if (element.equals("length")) { + + // Length + length = Double.NaN; + try { + length = Double.parseDouble(content.trim()); + } catch (NumberFormatException ignore) { + } + + if (Double.isNaN(length)) { + warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring.")); + } + + } else if (element.equals("delay")) { + + // Delay + delay = Double.NaN; + if (content.equals("none")) { + delay = Motor.PLUGGED; + } else { + try { + delay = Double.parseDouble(content.trim()); + } catch (NumberFormatException ignore) { + } + + if (Double.isNaN(delay)) { + warnings.add(Warning.fromString("Illegal motor delay specified, ignoring.")); + } + + } + + } else { + super.closeElement(element, attributes, content, warnings); + } + } + +} + + + +class SimulationsHandler extends ElementHandler { + private final OpenRocketDocument doc; + private SingleSimulationHandler handler; + + public SimulationsHandler(OpenRocketDocument doc) { + this.doc = doc; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (!element.equals("simulation")) { + warnings.add("Unknown element '" + element + "', ignoring."); + return null; + } + + handler = new SingleSimulationHandler(doc); + return handler; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + attributes.remove("status"); + super.closeElement(element, attributes, content, warnings); + } + + +} + +class SingleSimulationHandler extends ElementHandler { + + private final OpenRocketDocument doc; + + private String name; + + private SimulationConditionsHandler conditionHandler; + private FlightDataHandler dataHandler; + + private final List listeners = new ArrayList(); + + public SingleSimulationHandler(OpenRocketDocument doc) { + this.doc = doc; + } + + + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (element.equals("name") || element.equals("simulator") || + element.equals("calculator") || element.equals("listener")) { + return PlainTextHandler.INSTANCE; + } else if (element.equals("conditions")) { + conditionHandler = new SimulationConditionsHandler(doc.getRocket()); + return conditionHandler; + } else if (element.equals("flightdata")) { + dataHandler = new FlightDataHandler(); + return dataHandler; + } else { + warnings.add("Unknown element '" + element + "', ignoring."); + return null; + } + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + + if (element.equals("name")) { + name = content; + } else if (element.equals("simulator")) { + if (!content.trim().equals("RK4Simulator")) { + warnings.add("Unknown simulator '" + content.trim() + "' specified, ignoring."); + } + } else if (element.equals("calculator")) { + if (!content.trim().equals("BarrowmanCalculator")) { + warnings.add("Unknown calculator '" + content.trim() + "' specified, ignoring."); + } + } else if (element.equals("listener") && content.trim().length() > 0) { + listeners.add(content.trim()); + } + + } + + @Override + public void endHandler(String element, HashMap attributes, + String content, WarningSet warnings) { + + String s = attributes.get("status"); + Simulation.Status status = (Status) DocumentConfig.findEnum(s, Simulation.Status.class); + if (status == null) { + warnings.add("Simulation status unknown, assuming outdated."); + status = Simulation.Status.OUTDATED; + } + + SimulationOptions conditions; + if (conditionHandler != null) { + conditions = conditionHandler.getConditions(); + } else { + warnings.add("Simulation conditions not defined, using defaults."); + conditions = new SimulationOptions(doc.getRocket()); + } + + if (name == null) + name = "Simulation"; + + FlightData data; + if (dataHandler == null) + data = null; + else + data = dataHandler.getFlightData(); + + Simulation simulation = new Simulation(doc.getRocket(), status, name, + conditions, listeners, data); + + doc.addSimulation(simulation); + } +} + + + +class SimulationConditionsHandler extends ElementHandler { + private SimulationOptions conditions; + private AtmosphereHandler atmosphereHandler; + + public SimulationConditionsHandler(Rocket rocket) { + conditions = new SimulationOptions(rocket); + // Set up default loading settings (which may differ from the new defaults) + conditions.setGeodeticComputation(GeodeticComputationStrategy.FLAT); + } + + public SimulationOptions getConditions() { + return conditions; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + if (element.equals("atmosphere")) { + atmosphereHandler = new AtmosphereHandler(attributes.get("model")); + return atmosphereHandler; + } + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + + double d = Double.NaN; + try { + d = Double.parseDouble(content); + } catch (NumberFormatException ignore) { + } + + + if (element.equals("configid")) { + if (content.equals("")) { + conditions.setMotorConfigurationID(null); + } else { + conditions.setMotorConfigurationID(content); + } + } else if (element.equals("launchrodlength")) { + if (Double.isNaN(d)) { + warnings.add("Illegal launch rod length defined, ignoring."); + } else { + conditions.setLaunchRodLength(d); + } + } else if (element.equals("launchrodangle")) { + if (Double.isNaN(d)) { + warnings.add("Illegal launch rod angle defined, ignoring."); + } else { + conditions.setLaunchRodAngle(d * Math.PI / 180); + } + } else if (element.equals("launchroddirection")) { + if (Double.isNaN(d)) { + warnings.add("Illegal launch rod direction defined, ignoring."); + } else { + conditions.setLaunchRodDirection(d * Math.PI / 180); + } + } else if (element.equals("windaverage")) { + if (Double.isNaN(d)) { + warnings.add("Illegal average windspeed defined, ignoring."); + } else { + conditions.setWindSpeedAverage(d); + } + } else if (element.equals("windturbulence")) { + if (Double.isNaN(d)) { + warnings.add("Illegal wind turbulence intensity defined, ignoring."); + } else { + conditions.setWindTurbulenceIntensity(d); + } + } else if (element.equals("launchaltitude")) { + if (Double.isNaN(d)) { + warnings.add("Illegal launch altitude defined, ignoring."); + } else { + conditions.setLaunchAltitude(d); + } + } else if (element.equals("launchlatitude")) { + if (Double.isNaN(d)) { + warnings.add("Illegal launch latitude defined, ignoring."); + } else { + conditions.setLaunchLatitude(d); + } + } else if (element.equals("launchlongitude")) { + if (Double.isNaN(d)) { + warnings.add("Illegal launch longitude."); + } else { + conditions.setLaunchLongitude(d); + } + } else if (element.equals("geodeticmethod")) { + GeodeticComputationStrategy gcs = + (GeodeticComputationStrategy) DocumentConfig.findEnum(content, GeodeticComputationStrategy.class); + if (gcs != null) { + conditions.setGeodeticComputation(gcs); + } else { + warnings.add("Unknown geodetic computation method '" + content + "'"); + } + } else if (element.equals("atmosphere")) { + atmosphereHandler.storeSettings(conditions, warnings); + } else if (element.equals("timestep")) { + if (Double.isNaN(d)) { + warnings.add("Illegal time step defined, ignoring."); + } else { + conditions.setTimeStep(d); + } + } + } +} + + +class AtmosphereHandler extends ElementHandler { + private final String model; + private double temperature = Double.NaN; + private double pressure = Double.NaN; + + public AtmosphereHandler(String model) { + this.model = model; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + double d = Double.NaN; + try { + d = Double.parseDouble(content); + } catch (NumberFormatException ignore) { + } + + if (element.equals("basetemperature")) { + if (Double.isNaN(d)) { + warnings.add("Illegal base temperature specified, ignoring."); + } + temperature = d; + } else if (element.equals("basepressure")) { + if (Double.isNaN(d)) { + warnings.add("Illegal base pressure specified, ignoring."); + } + pressure = d; + } else { + super.closeElement(element, attributes, content, warnings); + } + } + + + public void storeSettings(SimulationOptions cond, WarningSet warnings) { + if (!Double.isNaN(pressure)) { + cond.setLaunchPressure(pressure); + } + if (!Double.isNaN(temperature)) { + cond.setLaunchTemperature(temperature); + } + + if ("isa".equals(model)) { + cond.setISAAtmosphere(true); + } else if ("extendedisa".equals(model)) { + cond.setISAAtmosphere(false); + } else { + cond.setISAAtmosphere(true); + warnings.add("Unknown atmospheric model, using ISA."); + } + } + +} + + +class FlightDataHandler extends ElementHandler { + + private FlightDataBranchHandler dataHandler; + private WarningSet warningSet = new WarningSet(); + private List branches = new ArrayList(); + + private FlightData data; + + public FlightData getFlightData() { + return data; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (element.equals("warning")) { + return PlainTextHandler.INSTANCE; + } + if (element.equals("databranch")) { + if (attributes.get("name") == null || attributes.get("types") == null) { + warnings.add("Illegal flight data definition, ignoring."); + return null; + } + dataHandler = new FlightDataBranchHandler(attributes.get("name"), + attributes.get("types")); + return dataHandler; + } + + warnings.add("Unknown element '" + element + "' encountered, ignoring."); + return null; + } + + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + + if (element.equals("databranch")) { + FlightDataBranch branch = dataHandler.getBranch(); + if (branch.getLength() > 0) { + branches.add(branch); + } + } else if (element.equals("warning")) { + warningSet.add(Warning.fromString(content)); + } + } + + + @Override + public void endHandler(String element, HashMap attributes, + String content, WarningSet warnings) { + + if (branches.size() > 0) { + data = new FlightData(branches.toArray(new FlightDataBranch[0])); + } else { + double maxAltitude = Double.NaN; + double maxVelocity = Double.NaN; + double maxAcceleration = Double.NaN; + double maxMach = Double.NaN; + double timeToApogee = Double.NaN; + double flightTime = Double.NaN; + double groundHitVelocity = Double.NaN; + + try { + maxAltitude = DocumentConfig.stringToDouble(attributes.get("maxaltitude")); + } catch (NumberFormatException ignore) { + } + try { + maxVelocity = DocumentConfig.stringToDouble(attributes.get("maxvelocity")); + } catch (NumberFormatException ignore) { + } + try { + maxAcceleration = DocumentConfig.stringToDouble(attributes.get("maxacceleration")); + } catch (NumberFormatException ignore) { + } + try { + maxMach = DocumentConfig.stringToDouble(attributes.get("maxmach")); + } catch (NumberFormatException ignore) { + } + try { + timeToApogee = DocumentConfig.stringToDouble(attributes.get("timetoapogee")); + } catch (NumberFormatException ignore) { + } + try { + flightTime = DocumentConfig.stringToDouble(attributes.get("flighttime")); + } catch (NumberFormatException ignore) { + } + try { + groundHitVelocity = + DocumentConfig.stringToDouble(attributes.get("groundhitvelocity")); + } catch (NumberFormatException ignore) { + } + + // TODO: HIGH: Store and load launchRodVelocity + data = new FlightData(maxAltitude, maxVelocity, maxAcceleration, maxMach, + timeToApogee, flightTime, groundHitVelocity, Double.NaN); + } + + data.getWarningSet().addAll(warningSet); + data.immute(); + } + + +} + + +class FlightDataBranchHandler extends ElementHandler { + private final FlightDataType[] types; + private final FlightDataBranch branch; + + public FlightDataBranchHandler(String name, String typeList) { + String[] split = typeList.split(","); + types = new FlightDataType[split.length]; + for (int i = 0; i < split.length; i++) { + types[i] = FlightDataType.getType(split[i], UnitGroup.UNITS_NONE); + } + + // TODO: LOW: May throw an IllegalArgumentException + branch = new FlightDataBranch(name, types); + } + + public FlightDataBranch getBranch() { + branch.immute(); + return branch; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + if (element.equals("datapoint")) + return PlainTextHandler.INSTANCE; + if (element.equals("event")) + return PlainTextHandler.INSTANCE; + + warnings.add("Unknown element '" + element + "' encountered, ignoring."); + return null; + } + + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + + if (element.equals("event")) { + double time; + FlightEvent.Type type; + + try { + time = DocumentConfig.stringToDouble(attributes.get("time")); + } catch (NumberFormatException e) { + warnings.add("Illegal event specification, ignoring."); + return; + } + + type = (Type) DocumentConfig.findEnum(attributes.get("type"), FlightEvent.Type.class); + if (type == null) { + warnings.add("Illegal event specification, ignoring."); + return; + } + + branch.addEvent(new FlightEvent(type, time)); + return; + } + + if (!element.equals("datapoint")) { + warnings.add("Unknown element '" + element + "' encountered, ignoring."); + return; + } + + // element == "datapoint" + + + // Check line format + String[] split = content.split(","); + if (split.length != types.length) { + warnings.add("Data point did not contain correct amount of values, ignoring point."); + return; + } + + // Parse the doubles + double[] values = new double[split.length]; + for (int i = 0; i < values.length; i++) { + try { + values[i] = DocumentConfig.stringToDouble(split[i]); + } catch (NumberFormatException e) { + warnings.add("Data point format error, ignoring point."); + return; + } + } + + // Add point to branch + branch.addPoint(); + for (int i = 0; i < types.length; i++) { + branch.setValue(types[i], values[i]); + } + } +} + + + + + +///////////////// Setters implementation + + +//// Interface +interface Setter { + /** + * Set the specified value to the given component. + * + * @param component the component to which to set. + * @param value the value within the element. + * @param attributes attributes for the element. + * @param warnings the warning set to use. + */ + public void set(RocketComponent component, String value, + HashMap attributes, WarningSet warnings); +} + + +//// StringSetter - sets the value to the contained String +class StringSetter implements Setter { + private final Reflection.Method setMethod; + + public StringSetter(Reflection.Method set) { + setMethod = set; + } + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + setMethod.invoke(c, s); + } +} + +//// IntSetter - set an integer value +class IntSetter implements Setter { + private final Reflection.Method setMethod; + + public IntSetter(Reflection.Method set) { + setMethod = set; + } + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + try { + int n = Integer.parseInt(s); + setMethod.invoke(c, n); + } catch (NumberFormatException e) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + } + } +} + + +//// BooleanSetter - set a boolean value +class BooleanSetter implements Setter { + private final Reflection.Method setMethod; + + public BooleanSetter(Reflection.Method set) { + setMethod = set; + } + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + + s = s.trim(); + if (s.equalsIgnoreCase("true")) { + setMethod.invoke(c, true); + } else if (s.equalsIgnoreCase("false")) { + setMethod.invoke(c, false); + } else { + warnings.add(Warning.FILE_INVALID_PARAMETER); + } + } +} + + + +//// DoubleSetter - sets a double value or (alternatively) if a specific string is encountered +//// calls a setXXX(boolean) method. +class DoubleSetter implements Setter { + private final Reflection.Method setMethod; + private final String specialString; + private final Reflection.Method specialMethod; + private final double multiplier; + + /** + * Set only the double value. + * @param set set method for the double value. + */ + public DoubleSetter(Reflection.Method set) { + this.setMethod = set; + this.specialString = null; + this.specialMethod = null; + this.multiplier = 1.0; + } + + /** + * Multiply with the given multiplier and set the double value. + * @param set set method for the double value. + * @param mul multiplier. + */ + public DoubleSetter(Reflection.Method set, double mul) { + this.setMethod = set; + this.specialString = null; + this.specialMethod = null; + this.multiplier = mul; + } + + /** + * Set the double value, or if the value equals the special string, use the + * special setter and set it to true. + * + * @param set double setter. + * @param special special string + * @param specialMethod boolean setter. + */ + public DoubleSetter(Reflection.Method set, String special, + Reflection.Method specialMethod) { + this.setMethod = set; + this.specialString = special; + this.specialMethod = specialMethod; + this.multiplier = 1.0; + } + + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + + s = s.trim(); + + // Check for special case + if (specialMethod != null && s.equalsIgnoreCase(specialString)) { + specialMethod.invoke(c, true); + return; + } + + // Normal case + try { + double d = Double.parseDouble(s); + setMethod.invoke(c, d * multiplier); + } catch (NumberFormatException e) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + } + } +} + + +class OverrideSetter implements Setter { + private final Reflection.Method setMethod; + private final Reflection.Method enabledMethod; + + public OverrideSetter(Reflection.Method set, Reflection.Method enabledMethod) { + this.setMethod = set; + this.enabledMethod = enabledMethod; + } + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + + try { + double d = Double.parseDouble(s); + setMethod.invoke(c, d); + enabledMethod.invoke(c, true); + } catch (NumberFormatException e) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + } + } +} + +//// EnumSetter - sets a generic enum type +class EnumSetter> implements Setter { + private final Reflection.Method setter; + private final Class enumClass; + + public EnumSetter(Reflection.Method set, Class enumClass) { + this.setter = set; + this.enumClass = enumClass; + } + + @Override + public void set(RocketComponent c, String name, HashMap attributes, + WarningSet warnings) { + + Enum setEnum = DocumentConfig.findEnum(name, enumClass); + if (setEnum == null) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + setter.invoke(c, setEnum); + } +} + + +//// ColorSetter - sets a Color value +class ColorSetter implements Setter { + private final Reflection.Method setMethod; + + public ColorSetter(Reflection.Method set) { + setMethod = set; + } + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + + String red = attributes.get("red"); + String green = attributes.get("green"); + String blue = attributes.get("blue"); + + if (red == null || green == null || blue == null) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + int r, g, b; + try { + r = Integer.parseInt(red); + g = Integer.parseInt(green); + b = Integer.parseInt(blue); + } catch (NumberFormatException e) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + if (r < 0 || g < 0 || b < 0 || r > 255 || g > 255 || b > 255) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + Color color = new Color(r, g, b); + setMethod.invoke(c, color); + + if (!s.trim().equals("")) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + } + } +} + + + +class MaterialSetter implements Setter { + private final Reflection.Method setMethod; + private final Material.Type type; + + public MaterialSetter(Reflection.Method set, Material.Type type) { + this.setMethod = set; + this.type = type; + } + + @Override + public void set(RocketComponent c, String name, HashMap attributes, + WarningSet warnings) { + + Material mat; + + // Check name != "" + name = name.trim(); + if (name.equals("")) { + warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + return; + } + + // Parse density + double density; + String str; + str = attributes.remove("density"); + if (str == null) { + warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + return; + } + try { + density = Double.parseDouble(str); + } catch (NumberFormatException e) { + warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + return; + } + + // Parse thickness + // double thickness = 0; + // str = attributes.remove("thickness"); + // try { + // if (str != null) + // thickness = Double.parseDouble(str); + // } catch (NumberFormatException e){ + // warnings.add(Warning.fromString("Illegal material specification, ignoring.")); + // return; + // } + + // Check type if specified + str = attributes.remove("type"); + if (str != null && !type.name().toLowerCase().equals(str)) { + warnings.add(Warning.fromString("Illegal material type specified, ignoring.")); + return; + } + + mat = Databases.findMaterial(type, name, density, false); + + setMethod.invoke(c, mat); + } +} + + + + +class PositionSetter implements Setter { + + @Override + public void set(RocketComponent c, String value, HashMap attributes, + WarningSet warnings) { + + RocketComponent.Position type = (Position) DocumentConfig.findEnum(attributes.get("type"), + RocketComponent.Position.class); + if (type == null) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + double pos; + try { + pos = Double.parseDouble(value); + } catch (NumberFormatException e) { + warnings.add(Warning.FILE_INVALID_PARAMETER); + return; + } + + if (c instanceof FinSet) { + ((FinSet) c).setRelativePosition(type); + c.setPositionValue(pos); + } else if (c instanceof LaunchLug) { + ((LaunchLug) c).setRelativePosition(type); + c.setPositionValue(pos); + } else if (c instanceof InternalComponent) { + ((InternalComponent) c).setRelativePosition(type); + c.setPositionValue(pos); + } else { + warnings.add(Warning.FILE_INVALID_PARAMETER); + } + + } +} + + +class FinTabPositionSetter extends DoubleSetter { + + public FinTabPositionSetter() { + super(Reflection.findMethod(FinSet.class, "setTabShift", double.class)); + } + + @Override + public void set(RocketComponent c, String s, HashMap attributes, + WarningSet warnings) { + + if (!(c instanceof FinSet)) { + throw new IllegalStateException("FinTabPositionSetter called for component " + c); + } + + String relative = attributes.get("relativeto"); + FinSet.TabRelativePosition position = + (TabRelativePosition) DocumentConfig.findEnum(relative, + FinSet.TabRelativePosition.class); + + if (position != null) { + + ((FinSet) c).setTabRelativePosition(position); + + } else { + if (relative == null) { + warnings.add("Required attribute 'relativeto' not found for fin tab position."); + } else { + warnings.add("Illegal attribute value '" + relative + "' encountered."); + } + } + + super.set(c, s, attributes, warnings); + } + + +} + + +class ClusterConfigurationSetter implements Setter { + + @Override + public void set(RocketComponent component, String value, HashMap attributes, + WarningSet warnings) { + + if (!(component instanceof Clusterable)) { + warnings.add("Illegal component defined as cluster."); + return; + } + + ClusterConfiguration config = null; + for (ClusterConfiguration c : ClusterConfiguration.CONFIGURATIONS) { + if (c.getXMLName().equals(value)) { + config = c; + break; + } + } + + if (config == null) { + warnings.add("Illegal cluster configuration specified."); + return; + } + + ((Clusterable) component).setClusterConfiguration(config); + } +} diff --git a/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java new file mode 100644 index 00000000..7dbb2fce --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java @@ -0,0 +1,554 @@ +package net.sf.openrocket.file.openrocket; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.zip.GZIPOutputStream; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.document.StorageOptions; +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationOptions; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.TextUtil; + +public class OpenRocketSaver extends RocketSaver { + private static final LogHelper log = Application.getLogger(); + + + /** + * Divisor used in converting an integer version to the point-represented version. + * The integer version divided by this value is the major version and the remainder is + * the minor version. For example 101 corresponds to file version "1.1". + */ + public static final int FILE_VERSION_DIVISOR = 100; + + + private static final String OPENROCKET_CHARSET = "UTF-8"; + + private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers"; + private static final String METHOD_SUFFIX = "Saver"; + + + // Estimated storage used by different portions + // These have been hand-estimated from saved files + private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590; + private static final int BYTES_PER_COMPONENT_COMPRESSED = 80; + private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000; + private static final int BYTES_PER_SIMULATION_COMPRESSED = 100; + private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350; + private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100; + + + private int indent; + private Writer dest; + + @Override + public void save(OutputStream output, OpenRocketDocument document, StorageOptions options) + throws IOException { + + log.info("Saving .ork file"); + + if (options.isCompressionEnabled()) { + log.debug("Enabling compression"); + output = new GZIPOutputStream(output); + } + + dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET)); + + // Select file version number + final int fileVersion = calculateNecessaryFileVersion(document, options); + final String fileVersionString = + (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR); + log.debug("Storing file version " + fileVersionString); + + + this.indent = 0; + + + writeln(""); + writeln(""); + indent++; + + // Recursively save the rocket structure + saveComponent(document.getRocket()); + + writeln(""); + + // Save all simulations + writeln(""); + indent++; + boolean first = true; + for (Simulation s : document.getSimulations()) { + if (!first) + writeln(""); + first = false; + saveSimulation(s, options.getSimulationTimeSkip()); + } + indent--; + writeln(""); + + indent--; + writeln(""); + + log.debug("Writing complete, flushing buffers"); + dest.flush(); + if (options.isCompressionEnabled()) { + ((GZIPOutputStream) output).finish(); + } + } + + + + @Override + public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) { + + long size = 0; + + // Size per component + int componentCount = 0; + Rocket rocket = doc.getRocket(); + Iterator iterator = rocket.iterator(true); + while (iterator.hasNext()) { + iterator.next(); + componentCount++; + } + + if (options.isCompressionEnabled()) + size += componentCount * BYTES_PER_COMPONENT_COMPRESSED; + else + size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED; + + + // Size per simulation + if (options.isCompressionEnabled()) + size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED; + else + size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED; + + + // Size per flight data point + int pointCount = 0; + double timeSkip = options.getSimulationTimeSkip(); + if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) { + for (Simulation s : doc.getSimulations()) { + FlightData data = s.getSimulatedData(); + if (data != null) { + for (int i = 0; i < data.getBranchCount(); i++) { + pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip); + } + } + } + } + + if (options.isCompressionEnabled()) + size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED; + else + size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED; + + return size; + } + + + /** + * Determine which file version is required in order to store all the features of the + * current design. By default the oldest version that supports all the necessary features + * will be used. + * + * @param document the document to output. + * @param opts the storage options. + * @return the integer file version to use. + */ + private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) { + /* + * File version 1.2 is required for: + * - saving motor data + * + * File version 1.1 is required for: + * - fin tabs + * - components attached to tube coupler + * + * Otherwise use version 1.0. + */ + + // Check if design has simulations defined (version 1.3) + if (document.getSimulationCount() > 0) { + return FILE_VERSION_DIVISOR + 3; + } + + // Check for motor definitions (version 1.2) + Iterator iterator = document.getRocket().iterator(); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + if (!(c instanceof MotorMount)) + continue; + + MotorMount mount = (MotorMount) c; + for (String id : document.getRocket().getMotorConfigurationIDs()) { + if (mount.getMotor(id) != null) { + return FILE_VERSION_DIVISOR + 2; + } + } + } + + // Check for fin tabs (version 1.1) + iterator = document.getRocket().iterator(); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + + // Check for fin tabs + if (c instanceof FinSet) { + FinSet fin = (FinSet) c; + if (!MathUtil.equals(fin.getTabHeight(), 0) && + !MathUtil.equals(fin.getTabLength(), 0)) { + return FILE_VERSION_DIVISOR + 1; + } + } + + // Check for components attached to tube coupler + if (c instanceof TubeCoupler) { + if (c.getChildCount() > 0) { + return FILE_VERSION_DIVISOR + 1; + } + } + } + + // Default (version 1.0) + return FILE_VERSION_DIVISOR + 0; + } + + + + @SuppressWarnings("unchecked") + private void saveComponent(RocketComponent component) throws IOException { + + log.debug("Saving component " + component.getComponentName()); + + Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX, + "getElements", RocketComponent.class); + if (m == null) { + throw new BugException("Unable to find saving class for component " + + component.getComponentName()); + } + + // Get the strings to save + List list = (List) m.invokeStatic(component); + int length = list.size(); + + if (length == 0) // Nothing to do + return; + + if (length < 2) { + throw new RuntimeException("BUG, component data length less than two lines."); + } + + // Open element + writeln(list.get(0)); + indent++; + + // Write parameters + for (int i = 1; i < length - 1; i++) { + writeln(list.get(i)); + } + + // Recursively write subcomponents + if (component.getChildCount() > 0) { + writeln(""); + writeln(""); + indent++; + boolean emptyline = false; + for (RocketComponent subcomponent : component.getChildren()) { + if (emptyline) + writeln(""); + emptyline = true; + saveComponent(subcomponent); + } + indent--; + writeln(""); + } + + // Close element + indent--; + writeln(list.get(length - 1)); + } + + + private void saveSimulation(Simulation simulation, double timeSkip) throws IOException { + SimulationOptions cond = simulation.getOptions(); + + writeln(""); + indent++; + + writeln("" + escapeXML(simulation.getName()) + ""); + // TODO: MEDIUM: Other simulators/calculators + writeln("RK4Simulator"); + writeln("BarrowmanCalculator"); + writeln(""); + indent++; + + writeElement("configid", cond.getMotorConfigurationID()); + writeElement("launchrodlength", cond.getLaunchRodLength()); + writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI); + writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI); + writeElement("windaverage", cond.getWindSpeedAverage()); + writeElement("windturbulence", cond.getWindTurbulenceIntensity()); + writeElement("launchaltitude", cond.getLaunchAltitude()); + writeElement("launchlatitude", cond.getLaunchLatitude()); + writeElement("launchlongitude", cond.getLaunchLongitude()); + writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase()); + + if (cond.isISAAtmosphere()) { + writeln(""); + } else { + writeln(""); + indent++; + writeElement("basetemperature", cond.getLaunchTemperature()); + writeElement("basepressure", cond.getLaunchPressure()); + indent--; + writeln(""); + } + + writeElement("timestep", cond.getTimeStep()); + + indent--; + writeln(""); + + + for (String s : simulation.getSimulationListeners()) { + writeElement("listener", escapeXML(s)); + } + + + // Write basic simulation data + + FlightData data = simulation.getSimulatedData(); + if (data != null) { + String str = ""); + } + + indent--; + writeln(""); + + } + + + + private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) + throws IOException { + double previousTime = -100000; + + if (branch == null) + return; + + // Retrieve the types from the branch + FlightDataType[] types = branch.getTypes(); + + if (types.length == 0) + return; + + // Retrieve the data from the branch + List> data = new ArrayList>(types.length); + for (int i = 0; i < types.length; i++) { + data.add(branch.get(types[i])); + } + List timeData = branch.get(FlightDataType.TYPE_TIME); + + // Build the tag + StringBuilder sb = new StringBuilder(); + sb.append(" 0) + sb.append(","); + sb.append(escapeXML(types[i].getName())); + } + sb.append("\">"); + writeln(sb.toString()); + indent++; + + // Write events + for (FlightEvent event : branch.getEvents()) { + writeln(""); + } + + // Write the data + int length = branch.getLength(); + if (length > 0) { + writeDataPointString(data, 0, sb); + previousTime = timeData.get(0); + } + + for (int i = 1; i < length - 1; i++) { + if (timeData != null) { + if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) { + writeDataPointString(data, i, sb); + previousTime = timeData.get(i); + } + } else { + // If time data is not available, write all points + writeDataPointString(data, i, sb); + } + } + + if (length > 1) { + writeDataPointString(data, length - 1, sb); + } + + indent--; + writeln(""); + } + + + + /* TODO: LOW: This is largely duplicated from above! */ + private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) { + int count = 0; + + double previousTime = -100000; + + if (branch == null) + return 0; + + // Retrieve the types from the branch + FlightDataType[] types = branch.getTypes(); + + if (types.length == 0) + return 0; + + List timeData = branch.get(FlightDataType.TYPE_TIME); + if (timeData == null) { + // If time data not available, store all points + return branch.getLength(); + } + + // Write the data + int length = branch.getLength(); + if (length > 0) { + count++; + previousTime = timeData.get(0); + } + + for (int i = 1; i < length - 1; i++) { + if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) { + count++; + previousTime = timeData.get(i); + } + } + + if (length > 1) { + count++; + } + + return count; + } + + + + private void writeDataPointString(List> data, int index, StringBuilder sb) + throws IOException { + sb.setLength(0); + sb.append(""); + for (int j = 0; j < data.size(); j++) { + if (j > 0) + sb.append(","); + sb.append(TextUtil.doubleToString(data.get(j).get(index))); + } + sb.append(""); + writeln(sb.toString()); + } + + + + private void writeElement(String element, Object content) throws IOException { + if (content == null) + content = ""; + writeln("<" + element + ">" + content + ""); + } + + + + private void writeln(String str) throws IOException { + if (str.length() == 0) { + dest.write("\n"); + return; + } + String s = ""; + for (int i = 0; i < indent; i++) + s = s + " "; + s = s + str + "\n"; + dest.write(s); + } + + + + + /** + * Return the XML equivalent of an enum name. + * + * @param e the enum to save. + * @return the corresponding XML name. + */ + public static String enumToXMLName(Enum e) { + return e.name().toLowerCase().replace("_", ""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/PreferredMotorDigests.java b/core/src/net/sf/openrocket/file/openrocket/PreferredMotorDigests.java new file mode 100644 index 00000000..d05cccbb --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/PreferredMotorDigests.java @@ -0,0 +1,885 @@ +package net.sf.openrocket.file.openrocket; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * This class contains the motor digests of motors included in OpenRocket versions prior to 1.1.1. + * Before this the motor digest was not included in the design files, and therefore if the motor + * digest is missing when loading a file, the loader should prefer the motors with a digest defined + * in this class. + *

+ * This is not a requirement for supporting the OpenRocket format, but allows opening older OpenRocket + * design files accurately without warnings of multiple motors. + * + * @author Sampo Niskanen + */ +final class PreferredMotorDigests { + + /** + * A set containing the preferred motor digests. + */ + public static final Set DIGESTS; + static { + /* + * The list contains 845 digests, set initial parameters suitably to + * prevent any rehashing operations and to minimize size (power of two). + * 845/1024 = 0.825 + */ + Set set = new HashSet(1024, 0.85f); + + set.add("000ffb4c8e49ae47b2ab9a659da9e59b"); + set.add("0039ed088e61360d934d9bd8503fad92"); + set.add("003eeba358de7ebf9293b0e4c4ca9e66"); + set.add("00e1a0576a93101d458c1c3d68d3eee0"); + set.add("0111b89926277a6ea3f6075052343105"); + set.add("0142c270a670ffff41c43268b0f129b9"); + set.add("01be1f9100e05fb15df4c13395f7181c"); + set.add("026f5924c48693077f2b11cdcdeb7452"); + set.add("029082f7acda395568ca7f7df40764e1"); + set.add("02dd1b3e2df7daf48b763f5ace35345e"); + set.add("036e124dce42859ff08efa79e1f202e8"); + set.add("03b88e64af521b03803247922801c996"); + set.add("0468d7dc3dca25ac073dac1bd674e271"); + set.add("048cfb7c2477c6e957d501c5ed3bc252"); + set.add("049dda2ad1a709321734f393dc8a115b"); + set.add("056d61b6a268283411e9dc9731dbb5e6"); + set.add("05b85612f288726b02cdc47af7026aac"); + set.add("05e205dc5dbd95db25305aa5c77b1192"); + set.add("0601c6944d02e8736c09c2a8bb7cba49"); + set.add("0622884d0a0954b1df6694ead24868bf"); + set.add("063e7748d9a96508a70b1a2a1887aa3d"); + set.add("06634321a8c5d533eb5efcbb40143257"); + set.add("069a54372ed2776286160384ca0cac4f"); + set.add("075539867b13c2afcc5198e00d7f4b5c"); + set.add("076d9374af5fb0f2469083f9b57b7b96"); + set.add("07c44b615a67060bca83c6faed56c0c6"); + set.add("0825628215a980eed5fb4bed4eaec3b8"); + set.add("082bad018f6d1e5622c371c1fe3148d6"); + set.add("0837c3014078c8c8e79961b939be83cb"); + set.add("08abceec22c5f6be5e9864be38df8ad5"); + set.add("08c3b40a4bcf7a33256e5543e484f995"); + set.add("08ca5be1a598772a8683016db619de36"); + set.add("0a80cecafb53ae0ac73e6aec1a4567dd"); + set.add("0add7ca688bcd32df8c3367a094e7847"); + set.add("0b175b4beb0057db1b169d61061208a7"); + set.add("0b955870dc2007c2b5f07eea57609420"); + set.add("0c60a51d36ee5118fe29173aff2f6e49"); + set.add("0c96cd95432d8e2ce6a6463ebf50beb1"); + set.add("0d06d195c29a7f6fde6d002171922f2e"); + set.add("0d642d7cb1544d19ec471124db97b92e"); + set.add("0dd49968e2b1c4b1077e3c7ade056a79"); + set.add("0e0d93ee28216440a5fa9452c9082351"); + set.add("0e6774068b61579e20b89771b8a8f273"); + set.add("0eac15679d3ae2fbd41083492b356b03"); + set.add("0eca4c015dd1de561c2bbc47eaa4daf6"); + set.add("0f0e1d09c7dec3a05b870b399ddbf6ee"); + set.add("0f3c31b26b5768b3202f02f9d6bcc71c"); + set.add("0f47293601d59fbad2076012090665dc"); + set.add("0f5a1b31c333b722e4a72acbeba3a189"); + set.add("0f6a55aca8a317f4d3d3236e4944343d"); + set.add("0ffaa291ee52495d7dfec03b3a845636"); + set.add("1092f5c5b48c2dcd4ac0bb16fa5383a8"); + set.add("10a1689703dc533d435bef7265dd9ac0"); + set.add("11bcc433b82e714f59809d76eed0ba05"); + set.add("11ce2ec12b216f3e8d71fd9c53782b23"); + set.add("11d11cdff93f18d10a1286cc3485c8c7"); + set.add("11eac86852e12b9c3a2d0a4b183c3b79"); + set.add("120eab6dd03b9bee7f5eb717e4e9d491"); + set.add("1272d1a6979ea20a2efee3eb04657915"); + set.add("12f6c5360c83656356c902ece3c0ff1b"); + set.add("138a603a483dcf4127f1dcf208843e67"); + set.add("140276d009fde1357ecdcb5d9ddc8a80"); + set.add("1491fae1c7ce940915dd8296c74320f3"); + set.add("14955ccec83043f3b1ef92f8524b3e67"); + set.add("150b1279bc8b7f509a030274ee8e3f35"); + set.add("153374d45687af1e96d5b8b1b03a2515"); + set.add("1536a1389a9cd4ecf5bfaac9f4333852"); + set.add("1539231d9952fdbe0533df405c46356c"); + set.add("15d6a88bb656579740291df01297aa5c"); + set.add("15fbf68a7c02161beb6cad00325752c3"); + set.add("161cd37f60e13b9850e881bac61c840f"); + set.add("161ed36663b694184f7f4131d1d5f3df"); + set.add("167df7bf13809a19da8ff90a27f4b522"); + set.add("170e81af0371550ea20c827669fbf0fd"); + set.add("177c0df08cb85a4e13bf7412dacf2699"); + set.add("179b9694bca64255ce9c0b06a08f46e6"); + set.add("17d55e2cd3df50ef07aff9be6b160915"); + set.add("1835337dfceafa20029fe6e472e7c7f0"); + set.add("185820cacfb62e34c1f6c2e1feb42d27"); + set.add("18981fde366efeca850bdf490253f0ec"); + set.add("18b7f1dce04bc7838f3d2b234923de27"); + set.add("18c2d213b8de15fc11ef66f7a7ad04a4"); + set.add("1914ab609416b8559eeefda814867b9b"); + set.add("19ae231357c49b6bf9427fa178dc58ab"); + set.add("19b0b447800ba36f2d4ce76264009e2d"); + set.add("19c9120e2fb7bbf6d21d71659f77439c"); + set.add("19c9753bd99d7f0328792a434625f8a5"); + set.add("1a508ce5b461be1998750bdad26764a3"); + set.add("1a77681a4646cd21461df84c49074fe3"); + set.add("1aa169a73004fc66a932576ac2732b15"); + set.add("1aa1f3cc21a0f6a6eadb6166d468284c"); + set.add("1ac8dac1b547a064a306bf42e568b5bc"); + set.add("1af11d2e99f06b69ab5103731592ae8e"); + set.add("1af30f73640ac1f9f3c8ef32fd04bfb8"); + set.add("1b337a115a491abfc3abcd62399704d2"); + set.add("1bb9c002f22ccd24bfcec36957ac0367"); + set.add("1cbb12c9b58adc33642e1165b77c2e58"); + set.add("1d30457aa2af0f212a26b9d2c203a216"); + set.add("1d390d2ede88fb2f77ad7e7432155466"); + set.add("1d920d4ee2bef0c7ffb28a91b9e325f6"); + set.add("1e09cd01462e6d4728efafc4a550a5a6"); + set.add("1e26c7969adb5bfc507da22802f17053"); + set.add("1e5378337317821ffa4f53e9ecf47fbd"); + set.add("1e68b1ce7eb224dc65b39546d0892299"); + set.add("1e757827e2e03a781905e9f983c89933"); + set.add("1f2564b3f0d78751b4e1d5836d54f7b1"); + set.add("210bd4536d1c1872d213995420cf9513"); + set.add("21bdc48d9411ffc8e811e32c45640f58"); + set.add("21d4e53c3308cf3a1e916ef6cafff873"); + set.add("21db7fea27e33cbab6fa2984017c241c"); + set.add("221ab691a72a6f8b65792233b7bdf884"); + set.add("222b7613b7a9a85d45051fb263b511bf"); + set.add("224c062a8213f22058c0479c91ce470a"); + set.add("22777fde378d9610258e4223fb5563f5"); + set.add("22929b4849129644531a722397786513"); + set.add("22c31705c3948c39721ced4ca04b2e65"); + set.add("22e355a9e573b7f6f86c7e0791647ba7"); + set.add("2320f4b15fb78448ce16a5a625f6f8f2"); + set.add("234467bcf00a15e7377ceca46b7302f8"); + set.add("23e140b2126af53487781f63082615e5"); + set.add("245d147c568faf00dfb47d9c9080871c"); + set.add("24a5102957c91107a092704f4f166e77"); + set.add("24b7b0f55cea9329f981f00d922cfe61"); + set.add("24d9308fa5d88f89365760a6e54f557f"); + set.add("24fe3f1922a6d16b21f57b9925558296"); + set.add("2571d40a353d275cdd8a4ba6e80a32fd"); + set.add("259a0325a52acf54184fd439d1b2521d"); + set.add("259d90773b3387c58aecb648c2c3812e"); + set.add("25fd0f44fbbadfb70cee0467f9b53d3e"); + set.add("26331fa38f2af84b18df5dd1db0244f0"); + set.add("26a5e7834018943090396d419ca64662"); + set.add("271f29d0b199d0d3f036e8f99ce76975"); + set.add("2762f40ffacbc78b4c949cd38101a02a"); + set.add("2769033a0acfff04e1f427027643c03a"); + set.add("27b1601bb3a33c7cd2745caa651f0705"); + set.add("27e522bd25f54343584ae89e90e64ee3"); + set.add("2815e68ed1683663820682c8e00fd795"); + set.add("285e598a676de414661a022f72967d29"); + set.add("2886ee93f5dd4f02b331089928520e4f"); + set.add("28f53f74ab45da2ab83072143f0d01d0"); + set.add("2967cd7a160b396ef96f09695429d8e9"); + set.add("29e99fbfab8c9771f4b5a86195db0c46"); + set.add("2a1f5f5a829badfd64e2c20cd17bd38b"); + set.add("2a941643d418880e0e7337aaaa00c555"); + set.add("2a9d2a64b4601046774c9d27202de593"); + set.add("2ad8de03de84415f1397cb2d4c77fb84"); + set.add("2af7bcae566ada617d8888f34a5f70a3"); + set.add("2bb2cea5465ab43f9b7e83cb44851223"); + set.add("2bc22736450a8d0efb8d898bdcf52d43"); + set.add("2c19c0cd4c005877798821dd65a2ff2e"); + set.add("2c39985a5a49fa07759dc880e3722203"); + set.add("2c58d5382b8d5fdbe1800e013f200f38"); + set.add("2c8f6954ba9842ad9fc9bb367d14cf72"); + set.add("2d13c151bbf6e1d7d7378c86d191d2d8"); + set.add("2df4ee3f8a2c3611b267936e47bd3d3f"); + set.add("2e6c8ecf50ee9ea82f407a8b0acd4f85"); + set.add("2e97a2f015b1247b01b5e022bf6109cc"); + set.add("2eae476e3eb97e2a1ad54c5b8fa48208"); + set.add("2f44b9347e986c91ab886dc2e508885f"); + set.add("2f478d2efa82571d5c3e49fde16c936e"); + set.add("2f7460de6e7581a6775f739f894d86c6"); + set.add("2fa429a16950f6c3f19a051b3417aac7"); + set.add("2fa4545430dae966dce186984c98d0b7"); + set.add("3027d63763f7aca58b41d52689f38dbd"); + set.add("302b34ea5ec261fd74a4901d041d3f82"); + set.add("30b5952157345beb00d753425a728757"); + set.add("3136fef31b6d0e1d9a0dbbbdac05b0a3"); + set.add("321377ccf346be4efa1fb8658032298a"); + set.add("325e3898dc252f6c936301412be06505"); + set.add("32fe6eecb5e97a6ff9c4f1c005857435"); + set.add("33197b8e7194af401da6150c68004d7b"); + set.add("3393a92e46a045c4eaf6b9e18f7044e3"); + set.add("33a89133876e91fccc4058627b34d617"); + set.add("3466c5940034ddb1371c4f86dabce964"); + set.add("348abf304c63a702e4a229db28feee16"); + set.add("349260e7bc0291ba2e4c26d4db00bee9"); + set.add("3507c7d2b11a693620235ea3872dce66"); + set.add("353236b8cb07eef73d80f25e240acddb"); + set.add("35aeed248d254fbc3542b5cd3aa9842d"); + set.add("36218bbb51e99aed53ea822ebaa2c873"); + set.add("3666b06f839465adc5d36a6e75066a47"); + set.add("36fb9fb79c253ee61e498e459f0cf395"); + set.add("3703dd15904a118a05d771e7ee6e3f11"); + set.add("370b98cc77577db1a07021e46c21cd3b"); + set.add("3719475cc57cf3b5312f21b1efd228ef"); + set.add("3738564e4327367bc2f359cdbb442304"); + set.add("37bf1e76b05f333eefc0495e4f725838"); + set.add("38715f11bf91b5ce06494e1ddd94c444"); + set.add("387eea945f83c9567fa42c6e150b7ba9"); + set.add("389687548b9f05e6c99d93a2ecf76307"); + set.add("38b1e93cc1910ecc5301502fbb9bd8a3"); + set.add("3a0b2ffd2d4593581c52bdc1094d92d8"); + set.add("3a99a5318497e7108995a08675fa70d5"); + set.add("3b4573f1f11db1ffedd14e10d539aad3"); + set.add("3bc526028cf0be42fcbb75936810d41c"); + set.add("3bc5834ec0726b10465b67f17b77044e"); + set.add("3bf858e6c91e0292259a886b8bf793c3"); + set.add("3c4eea57e65806dc59dd4c206bef79e1"); + set.add("3c7b9e1836fe07f7a4ffaea90e7f33fc"); + set.add("3c8aee818229c48b9a882caa6de58c18"); + set.add("3cf831629486be08e747671a14d113f5"); + set.add("3d6b990aaee7dff0be939343711dfa74"); + set.add("3e2d355d7fd39e54ceead835d14df7e9"); + set.add("3e8697fe46496d41528387e2d37d734a"); + set.add("3ea538f54677ecaffbed1ae5f2e12d28"); + set.add("3f654d824783b4392396b34ad2b44974"); + set.add("3fc4889ea40aea23fedc994704ba4708"); + set.add("41145e8204991f0b644b831cd859c4e2"); + set.add("415fecbed32f8de16ffbab8e97edb4cb"); + set.add("41633604778611785d7453c23823b0b3"); + set.add("41d37971a99bb08d0f5f4fdcfcd87e8d"); + set.add("428c0aeb520fe9e77d3b464543620716"); + set.add("42cc2865a6fc399e689d2d569c58de2a"); + set.add("43a6db581840e3645459ce51953ca9a5"); + set.add("43a72eab1f3f874da7d68092e83357ec"); + set.add("44255564acd68eca32ffab8e6130c5cc"); + set.add("4448ff245bfd8d2606b418f33797571f"); + set.add("44a4e02e520657221706cd6d69bcfb13"); + set.add("44b12361fee8a2385a9b90e44fd079f3"); + set.add("44b7c1c17e8e4728fadeecb6ba797af0"); + set.add("44d734a18b45937c3035a047f9063dfd"); + set.add("44edf41dd7624a6e2259d8e451622527"); + set.add("4528bda7194c6dfafada95d68c2faa3a"); + set.add("45a8a995a3614f823c04f3c157effe97"); + set.add("45d2f014e70681483d6bc5864cf94b20"); + set.add("46232174087bfb178ad7cc35bfb387a8"); + set.add("46401106d96b729d330375a63e655374"); + set.add("46ac2356b12ed7519ae2dd5f199b7c10"); + set.add("4790684e33d48e3dfe99b6ff7712be8a"); + set.add("479a2848353fef692063ec37e7d556dc"); + set.add("47a649cae70a90e7d1ae2b2ab10465f0"); + set.add("47bc150e2585e61cf9380ed540de4465"); + set.add("4863872b48ecad3005e7b60d114c0fde"); + set.add("487c3163ebf25cd3b4479e13e30cba5b"); + set.add("48c5d84e56a982689f4268ed9b50cded"); + set.add("493a84bde424f5946290238998d64873"); + set.add("499e8c7c38dd4d8068eefc4eb58d4cf5"); + set.add("4a03d963b887b4ade3d574e87d111e9d"); + set.add("4a5509929d6991507c6e136178942a2d"); + set.add("4a933f8824eba082555515e69d3bfe43"); + set.add("4abc93cb926e33fbb97aa0d2ffe7885a"); + set.add("4ad536d6aee9fffe1e84c9e75698f5cf"); + set.add("4af14f94870a2e3d47dbd78cc05e58a8"); + set.add("4b0a7961ee650f518533f03c38ca8320"); + set.add("4b166cec69dc9ace3a9f598674c35b3c"); + set.add("4b5a632e55f4dbea5435e151145337a7"); + set.add("4b797a7d23faae4daf8b2946d6cb24dd"); + set.add("4b9e8ea91d6bd67b22be67dd40b871a7"); + set.add("4bd7e46dd429e45ddee5736f86d494cc"); + set.add("4beec7064114b6e49cc76cc2f71594ec"); + set.add("4c3f47c263ea5b933ac5184466573f6d"); + set.add("4c9b11094fa43b8a9aaf1a1568bf60c2"); + set.add("4ca44906c21909f396774827017e007e"); + set.add("4ca7dd633f56f76f794faf958416f4c1"); + set.add("4d6956c8d3db98528dfbdafa4a3316b6"); + set.add("4d84b18416836b7297b990a408a6eda3"); + set.add("4e13b8d5d4a77393b2fbfbaebe9ea1ca"); + set.add("4e3b029d124b898f1d11a8d15d2a6688"); + set.add("4e9723863a06235d9339cd00912871ed"); + set.add("4efdf67cd98424e7cb008dd09b169942"); + set.add("4f25dd1fcb4aedb512f24758961d87f9"); + set.add("4f86907e557c00d13b42a2393b834d8d"); + set.add("4fdb3ba6ebc9b3e9ab133c15312f995a"); + set.add("504bbb5991ad94728e6b73c6ddc6c476"); + set.add("515f449c1e9fd279dbdadf3cc38fd008"); + set.add("51d9a0c78486de462af6a490acea1fcb"); + set.add("52032bb8c1acb8bf7320aa73babd2e50"); + set.add("5203feb9b0d422a38052d9df4103e3ab"); + set.add("5222c37a7e8618d4cb43ce8c4a188129"); + set.add("52731882ea73ad5b1d39c25e2969d9aa"); + set.add("536af35745c20e4ee25486a31c2fb57c"); + set.add("5379086fb93464cbdad4459101ed4d07"); + set.add("542f3505366c2d6575e73064aacf536a"); + set.add("54350b63fafc31af32bdf49cf0bbfda2"); + set.add("5498ead583ab6cd6900a533b1cb69df8"); + set.add("553eb9e396b2f304f963b717bb3a9059"); + set.add("55c5181d0e1b170cfd05c3a9271b3bc6"); + set.add("566ff170b185c5cfd893030c97080451"); + set.add("568c906117202c4d4451dfb3be697921"); + set.add("56a9926b91222c8943640da0b642d617"); + set.add("56fcddb2fc61ab068e1ce98a226fd34d"); + set.add("573f9b1aa16e771da95df44fe3a62167"); + set.add("5805ae3e1c5fa9f7a604152c40c9d06d"); + set.add("5844ffd995e179e21476fe41a72c7e85"); + set.add("5866a0ca3348c1b406e4a4a869b183ae"); + set.add("5922a04c19e52d4a3844b61b559a89d4"); + set.add("5957b399b3380e097b70cfc09bae1bd3"); + set.add("59785d3feccf91b7a9fcd96fe5e686de"); + set.add("59cc15fde8f2bab7beac6a9542662df3"); + set.add("59ef8fd572ad56b7c00092f185512c0a"); + set.add("5a26e5d6effb9634941bbdaecf1cc4ce"); + set.add("5a94fedb054c29331d22c4442ad619a6"); + set.add("5b1a41ab325cdfb925f500868f77e058"); + set.add("5b20fd5088ed40d65a52c38fbe314373"); + set.add("5b3510c0aa53405e1fbd7a67b3af34fd"); + set.add("5b96ce711afb37fb511e70ac12cb717f"); + set.add("5bb8c694f0d7e39aceaa7fe7a885a6e1"); + set.add("5bc7dae98ed248bc85f4782783c7a383"); + set.add("5c1e091a898470db28aaddc968071a00"); + set.add("5c603c37c8ae3b7441a49bfdd93a2426"); + set.add("5ca4eac1f0b43f03288c19c42e1ccb2b"); + set.add("5ced682df2330890f2239e8da8d33061"); + set.add("5d437ac21a6da33b77c980abef5af0ac"); + set.add("5d4f136bcd4f5f71e0402819f2f666ea"); + set.add("5d9d43530d78a07475063de4f6b82578"); + set.add("5e8973f53dfe0e36537e7d88ac84cfaa"); + set.add("5e8b973df438112549cbd73b38240094"); + set.add("5ec17176ac8ca3ffe5c7707d4b33aba0"); + set.add("5ecdf016b2374b2029c58dce498548cf"); + set.add("5f4d8576e9299aecd4ece33f8b4ffb3d"); + set.add("5f5bc13ecb72cde7c4c6e89279e836f0"); + set.add("5fc7b23ca79086fde585ac92b8ddfa61"); + set.add("5fc8dfad0c6503b16fcbdaf2140f5bd6"); + set.add("610b21fa92e08d26b0ebbd27ac406558"); + set.add("618c82b1f690b74209a68062f0b7f50e"); + set.add("6214548d7b510154960ca3d23da4f38d"); + set.add("6244a9533207c9f3d89bd48d2c946d72"); + set.add("628475d3f98ce21920983b2335d29771"); + set.add("62b5f08d8f9087672b45485f5177b688"); + set.add("62c0ca2c1be43447418c673a27f69a53"); + set.add("62dd2d23b56d1991111028754f7d5718"); + set.add("62fe634a6ec154d4b675a8944ab98a7b"); + set.add("638e84fef470490263300ed27293aca9"); + set.add("643181e6ca3418a86b5dac6858805839"); + set.add("6431d2ee351952b0ca1c8e24aee89d9a"); + set.add("64cf9d529b625818f06f910fd5a51ebc"); + set.add("64f5901476b159bd9c4f5ed9aa2b4cc7"); + set.add("651c5d94aa8b742ea6bf89eb4760d88b"); + set.add("6535664e59493ee6581d3ec49d666a05"); + set.add("659bd0331a1348d14e9cd72006008d5b"); + set.add("659d8f3f58c60862ec21306475d5b86c"); + set.add("65ed980ed9e129e301e3c219be11999c"); + set.add("661c50f934f8b021101df91c063c2662"); + set.add("66289df1c8d41414638696d9847188a7"); + set.add("667f3707995e365e210a1bb9e1926455"); + set.add("66ff6174d6a5b1c8a40637c8a3a8a7b9"); + set.add("673d52c40a3153d07e7a81ad3bf2027c"); + set.add("67c803799e8e1d877eb3f713dc775444"); + set.add("680708ce5f383f0c7511eb3d7b7209d9"); + set.add("68ee06fe147e143b6e1486d292fbc9b4"); + set.add("690da28e475ebd2dec079e7de4c60719"); + set.add("693db94b6ffb0c1afa7b82499d17b25f"); + set.add("6961f9a08f066d0318669a9c9c94017d"); + set.add("69a38fb26f994ccc04e84e66441e0287"); + set.add("69f5b82d6acf05cee8615ff5d05f7921"); + set.add("6a1e040ce59176bcbe4c47654dcf83a7"); + set.add("6a26a773a6223c79697e12838582f122"); + set.add("6a6e0e4350ef8d4a3489aa4398bd674b"); + set.add("6a8abe4a6fe236bf93d9b85681db2c0e"); + set.add("6aaddb50ae45f1006852479932dfbd54"); + set.add("6adb0778b8930a8e6a2f1e99de2ef074"); + set.add("6b54ec7203070bb29e514eb7d684e380"); + set.add("6b598530a066271059bc94c1fa0cd7a1"); + set.add("6be4f1c5af0ff30131481d009e87133b"); + set.add("6be8cb8a938f1ecef2b36c823e8e6ade"); + set.add("6bfe9b78813cfa4014e630454f7a06a5"); + set.add("6cb4d52135f005a2c7ba8ccc3b8781e3"); + set.add("6cd5f8dd36648fcafcfecc8d5b990e9b"); + set.add("6cfdb07efc0d3c1f36b2d992263253f9"); + set.add("6d95c9c12fe5498af055b013bf7ceb7d"); + set.add("6e8f160f1b2b54c3c89b81c4f9a98283"); + set.add("6eadec5ff4cb05c8ef1a64d2c94d627b"); + set.add("6eceba3c0a19666f5a9adbc13ceb1ae7"); + set.add("6f47bff8d62f5bd51cee156f78e8cfcb"); + set.add("6f484725ba3abcadfe8fbfb4e6e83db6"); + set.add("7031b89c62590b6a41e7ad524bb0a308"); + set.add("7058fc792efe7aaddf8a0323bf998f72"); + set.add("706e502b5a6db25407c2565457944633"); + set.add("70cfd491765d3e4e6a4e4b1ccaf9c343"); + set.add("711e7a11c4be36563cae1b71234dc414"); + set.add("71794c9ad0e60b6d0dcd44b51c3704f0"); + set.add("7193a4c6f286f7b86b18cc7908498d06"); + set.add("71b17eeb05fd473e42aa5c4e51e47a15"); + set.add("71e0014aeaebda1113a12cecb51fd20c"); + set.add("71e2d06eaa0ab3ae59d0f7b7ef20fc31"); + set.add("71fe7c7f2a54587c2278b3e630faee56"); + set.add("729ba9dde60740e6d5e8140fae54f4c6"); + set.add("72b5be92417a4a6a09f5e01c106cf96a"); + set.add("72c0c6f5a653bb57a1aba44e7edb202b"); + set.add("72f6580c0aa3b101acffce59adf7809b"); + set.add("730ac94082f25931179a313af186b335"); + set.add("73243d82b8157406558562b5eb70818b"); + set.add("73371ae751751a5998c3bc8de577b83e"); + set.add("733c6d4df3b6781473ba0a3a169ca74a"); + set.add("7376d2568492e6f6c0fadab86e22416b"); + set.add("737b791d27930ccba061fa36c4046208"); + set.add("73a47e531e9c0ddf5a334c40508f6361"); + set.add("73b2859aedfe1bf317841bbc150e0491"); + set.add("7413c1de6d5f286055e8244101da306c"); + set.add("741bfaabd99301c5503fd36d17912627"); + set.add("7423f1b74c8367863a1071bcd0864d42"); + set.add("747ddec4bc30cbde2ebefac7b8df466c"); + set.add("7494df6c5102bbfb6e48c9b30281325b"); + set.add("74c5cb4f540e2c9b27ae60dcc8247eae"); + set.add("74f6a218f8877fb76f74eacc9df32fc6"); + set.add("751ff3c7f3e77c8d3ba24c568fd11845"); + set.add("757c05f3194d6d4b848b83c0e3e5f1a3"); + set.add("75f724e20c3f2e26d2de13927fbf78f1"); + set.add("761c6d190a411231fccfeef67f08eacf"); + set.add("763eddbcb1f6787b3350f65c64b44ba4"); + set.add("765a1a5d8f09ddffec749d3a6920c4a7"); + set.add("76acd0d7e4d38152771480bedacba209"); + set.add("76dc5d4b4f65aacb1dfc1a5a8f61b023"); + set.add("771cc4503662f2fc2c5f15f46e7e58b6"); + set.add("772a3193a3cf3e18fd2058c3f45c35f8"); + set.add("7823311e8d60374d4b37c50d927507c8"); + set.add("78282abebd03a40f2dd21f1505a5f789"); + set.add("782e12a60bbef943f13f2fa1af1e39f1"); + set.add("782e6df5b10a60b26117e0648e76c6c4"); + set.add("7875a865fbaf33ba617cdb7c7f0f7140"); + set.add("78ee37b2f7acb3d892e54f0e9d2e0c14"); + set.add("791f7d21ea119ccd49df31b2f614a0d6"); + set.add("795036eafd006f62ee5a68ba1c309865"); + set.add("795fcaf2d6d9e1c37a4c283876f26cec"); + set.add("79688fa15c424b73454d9cd0a070472f"); + set.add("796c759040e72b8efd4630754bd3f30b"); + set.add("798ad9ae186a9d89e6f69e065bc22a86"); + set.add("7a2a604d923e5bd87846902e47acc438"); + set.add("7a39ea82b6b2bb75f9f6a7b817dab9cb"); + set.add("7a62872422cf100af636b434f4a0b307"); + set.add("7acda66f5d077fa708de7d153882b97c"); + set.add("7b1aca3caab3a61147d4ebf5f7971d42"); + set.add("7b5bc0bfd0de5126b4d278aa5775abd7"); + set.add("7bd0735d3b5d579f0c97e11309a12451"); + set.add("7be2fb055d29d5c8e42c559295ee8113"); + set.add("7c14e11e0126b310217db2488f898127"); + set.add("7c4ab23d9b1db15ea1f49fe017edf346"); + set.add("7c6080928783078289d9a473efecc134"); + set.add("7ccde35451846731eff4ae16e40f661f"); + set.add("7cce66eec1313c11f5b9005db8f2823d"); + set.add("7d40723bc0200f13675626309559ce6d"); + set.add("7da7fa494528cd0836f9988f3e7ada96"); + set.add("7e3bc2bc33f34ad791573e94021381d5"); + set.add("7fb1e485fa41841779a0a1f95a2b7cd8"); + set.add("809b63d7a153ee60272ffc224766fd72"); + set.add("80fc9ff72737286ad64fe7de1524c799"); + set.add("82b602bacfe681cee58d5530ac9e8b99"); + set.add("82f69b66499f2bc375467ee933fe4577"); + set.add("83243e87941f4ec7f8841571dd90b3b2"); + set.add("836481fe9bfd7612506e0545bdcf279d"); + set.add("83a498353a59dea68538962eb2642ba8"); + set.add("83eafb190276935630f43cddf3d78143"); + set.add("845c54809778f5b838e32443a7d44072"); + set.add("849b5885cbf965447675610ee1d0dca2"); + set.add("84a895acdcd487244b6803082036fad7"); + set.add("84bdf63a67691194e37082e3f7f6d712"); + set.add("84c99be383e4ada00f4e6bd335774655"); + set.add("84ed2fb163b5b2525a9a731056ffd144"); + set.add("8517e14d6f657f1244c0344d4f1a828b"); + set.add("8541aca6dd06f2dc6984d5e1b664900c"); + set.add("85cc38b178bd721bf7225144dd301b0f"); + set.add("85d00ae1ce88ace2bc6918750a97502f"); + set.add("868af0eab556081846fdbff18df40b28"); + set.add("871f7fe309f61ec7e45e4b29833349d9"); + set.add("878e7848ab58bf9271fc04766e969c8f"); + set.add("87b872efe9433147c61d5d2c3dcca14f"); + set.add("87cd3518578a2ef76341b33f9c95198f"); + set.add("87cd3a0a86f398ba1026cdb969e55090"); + set.add("87cdeb3fcaa050209091a1600ce1df11"); + set.add("88008ed2e9b600fa2e453390688aaa7e"); + set.add("8833c25743e0f9725ca22dbc6e54d1bf"); + set.add("88693556ff80aacd354c1948675c0674"); + set.add("888664c26a3296f9571d561723b44255"); + set.add("88ed07b96422ec99767fb35bf6a51966"); + set.add("88ed43ef6f483b9a7e34c34b46335dea"); + set.add("8a2e4445364c3c9151dcf4622c6add51"); + set.add("8a73ce2e18dacf250a961dac49338407"); + set.add("8ba75b207cc0bee8ec624e9f33019161"); + set.add("8bc592cc7aaa336637b9c82c43cbb081"); + set.add("8c1bdef25d6a6df69c303be204748da9"); + set.add("8c8b182ec0845de4a5fed3245e5601ea"); + set.add("8c8d724fba729940b1234e58e64125b8"); + set.add("8ce47ac01efd8c0ab75ae5371ff0f7ba"); + set.add("8e1600a04363c86199d660ccb7085735"); + set.add("8eb548ee8bf98a6426f0b5a11e32c88a"); + set.add("8ec54a8bd1ab30f324eb0f03ef79d097"); + set.add("8ede1653debc5617deae6a7632c18502"); + set.add("903594c774fd5be01132f559c00778b4"); + set.add("9079d8f7488bca4504d58be0bc76deea"); + set.add("909a1f7458c8f1f1138dff9ce019fb6c"); + set.add("90b8dd2817509c2374b20a1975ca1a54"); + set.add("90d0f3d40769a6219608964628d40e55"); + set.add("9104737f888d85480d0cc9aef8587e77"); + set.add("9118a19b2abc5d1d624b10f2bceb18bb"); + set.add("912e499f9a4a55f11133e01b33542ad1"); + set.add("915fcc373ba5d8a13814f236c1a9e4e5"); + set.add("918ca652867678984ae1149a3b5467bd"); + set.add("91fbebd600bbd88370994b739ae8e1f8"); + set.add("92fc949a982c897ca4a17750e2ee4afd"); + set.add("93c0446ee508efe75a176035986627cc"); + set.add("93d4329e22ed50d3502b2d0bc117baa6"); + set.add("93f33bcfa6201057376a3fe53eb29959"); + set.add("944b74b5ff9c617312ca2b0868e8cbc2"); + set.add("94bacf4caccc653a74a68698be0da4bc"); + set.add("9572f2ed73f01766b0ede9ec3d52715a"); + set.add("965e3d6087eec8e991175aada15a550a"); + set.add("967119411833b80c9ea55f0e64dacad6"); + set.add("968c5025a59e782d96682b09c4e94746"); + set.add("97824aa7746b63a32ea6d0dedb3e3f84"); + set.add("97aa914f28281f67ae3ac5222566c2a0"); + set.add("97f5a198489144a2f306456c1a346f9b"); + set.add("98a7e979d454d7f46ceb4a4283794d3c"); + set.add("98ff8ee9107e864d7c52d631070fff3b"); + set.add("993739fad4a47f34eb65e3ee48d15c09"); + set.add("99bb411f89eb34ebfa59900de36581fc"); + set.add("9a13940746bcf4cbe210150833b2a58b"); + set.add("9a3d7af6ccb7d277e3ed582d8932b5db"); + set.add("9a76e86b4275099983c5ede78626e0dd"); + set.add("9a9caad4a9c674daf41b5cb616092501"); + set.add("9ae6b0ad5010301ea610f49e008adf8c"); + set.add("9b6033bd4470408ecf2949d88531d6a1"); + set.add("9bfc7853ff00c7ea0e2f8972dc2595d4"); + set.add("9c8bdd485912f9d9eaaba3d5872be726"); + set.add("9cba07b76b4e79c0265deda5df2e2381"); + set.add("9e082b9bb6c1361965c0f0e06f051acb"); + set.add("9e24dbadcadc67447af65d571ffaee55"); + set.add("9e6a5f03a8b524ffa3264a3f32818e1c"); + set.add("9ead837b9e4f8c922f74ddbff0d2b88a"); + set.add("9fb7aa659c0475d5dc72bb35567247c9"); + set.add("a0006978c9a542518b425c0caa67042b"); + set.add("a01bdd6575c3cad9f9a4cb8aac9c716a"); + set.add("a02500e28eeb7e56e343607a822e2a7e"); + set.add("a05c1799e061712459e6c46f533263a6"); + set.add("a0799831bfb3f9b77b63c03fad39cce0"); + set.add("a0d4911294ccb20c0920a3cc6705f326"); + set.add("a11dfa1b02b1671d42b1abc858de2f2e"); + set.add("a11e237bd6d3c4a4ee8a7ee791444ad3"); + set.add("a148d83d50cf0852f6c08ceacbea0296"); + set.add("a1d8b81c03860585fb40613e828c1b2e"); + set.add("a20c867fdbb93bbe1d1231d9a8ea76c5"); + set.add("a21e0795fe0977d50a4496ba60e685e1"); + set.add("a260bb11468a2252a8dedff81f5672fd"); + set.add("a2b01bf43bc1d403e6ab3a9b71f32484"); + set.add("a2c15ded3e16d9aa12b9c07f3383c098"); + set.add("a360659a62e2e34f4edc89ce5e8b8a0c"); + set.add("a3a985e0ae5a9c96c74b8ee647101439"); + set.add("a3bf05e31288667a43b4c39cc0604c97"); + set.add("a427397e35b28706c5b8aa50e8f92a1c"); + set.add("a432e1b27b7d9814368d8f4071cf2dd0"); + set.add("a4b4800082feb6fcaf1cd56dda3a28c6"); + set.add("a4b83742cb45f1dd9130250cd72c460e"); + set.add("a5a8b20a222bd51b707e50303fdae33a"); + set.add("a5cf16d12d611ddc5ae1b010083690ad"); + set.add("a67b1720a7f123bb943c3f1ee74b8f00"); + set.add("a6b31c2e971254f85e231653abdc3a06"); + set.add("a6f9fe8c867cbef07730db4d1d461960"); + set.add("a706de20cf1a31e379d98807d1cb2060"); + set.add("a7b5467023706646a6b8ce2602bba226"); + set.add("a7bb7f7f68b538fb778856d4fbda50b7"); + set.add("a7fee39f2151056370c57d383e486348"); + set.add("a84a5f90f1083075081f135c74314bff"); + set.add("a8a6b73342c6a92638e41b86e6838958"); + set.add("a8f1c8b28c017186778e3074637e52ef"); + set.add("a90e513d9b2d0f55b875c3229e2d9816"); + set.add("a9e697026e08d1a8765497a9569b04e6"); + set.add("aa3218984177ce38bfdf06e38fbaa64b"); + set.add("aaa0291aa11c304b3a2300d4025db74d"); + set.add("aad63a3685d9f906a7c6c8716d626c0b"); + set.add("aafee591c7a3ae5f3c4f44f2d0f8a70f"); + set.add("ab85503c9acb730fcb9ed2a4dd74e2d7"); + set.add("ab8ad454409604808d1b444b068e602d"); + set.add("ac4c8af4d29676c8c79ac9ef180fc5df"); + set.add("ac4cd34387b04786cc5315b464006ec8"); + set.add("ac9c443698ac77bcb3a73a959f6ca0f0"); + set.add("acde934989eba2c7fef7cce095ce85c7"); + set.add("ad053830e5d0bb7e736ab98a8f3f1612"); + set.add("ad08d0d2d84298deb08b4c4a1cf62f39"); + set.add("ad0a1f2424a1b831f9777e638e8f829a"); + set.add("add039636134cb123908df5053304f3e"); + set.add("adf89cbcb01a2ec6d4afb24914790a67"); + set.add("ae1ae7c31f46325ce6a28104fa7070e6"); + set.add("af8c83664fd6eec8089ef1992aec463f"); + set.add("afcd59e32572ecb7ebe2d9c993d5fa9d"); + set.add("b012115b4276791c5049dace174933f7"); + set.add("b218489d2d4d7ddbfee2f073e238ff30"); + set.add("b251290e1d8690953d9cc0c1ea3bac6f"); + set.add("b2843a551894de318b80f790565dcfe3"); + set.add("b2a414aeb8800edfa8945863ffa5fbc9"); + set.add("b2d68ad2619bbb15a827b7baca6677b0"); + set.add("b2fe203ee319ae28b9ccdad26a8f21de"); + set.add("b33afd95fbd9aae903bbe7cb853cbbf3"); + set.add("b385f0f86168fea4f5f456b7700a8ffe"); + set.add("b3bd462a51847d58ed348f17c8718dca"); + set.add("b3d1befe2272f354b85f0ca5a3162dc8"); + set.add("b3f50d0da11772487101b44ae8aeb4ac"); + set.add("b42625f51295803ae1f99daf241c0bd0"); + set.add("b49cdae29a3394a25058e94b4eb5573c"); + set.add("b4ce8f683ec172aecf22cf8e516cce05"); + set.add("b4ffd04e41c1b8757149777a894f33f2"); + set.add("b5a1510fcf6dd596e87f497bfd5317bb"); + set.add("b5a75d8c18db0a96a3423e06554122c8"); + set.add("b5d312d32267bd15ee43f36077feefe9"); + set.add("b6645bb07f58754b8838d54e24562c06"); + set.add("b69831350ae6a3dfc18c0c05db0c25a8"); + set.add("b6b70e569be8de2fdecf285730319735"); + set.add("b6ee0ea7d82d3d7e0ab8bc62057c0385"); + set.add("b707a076a44ca9b3e6b8dc1dcde7d877"); + set.add("b77df6081bbeb4da02c075befb4beb9b"); + set.add("b7bdcedd416cccc742643e8e244f6282"); + set.add("b7ea4565381c6dc534cf0af8305f27ac"); + set.add("b7f3fb01d8c41525b103fc5faba23362"); + set.add("b80bf674f28284a3614596800ec02b3a"); + set.add("b81ab08e53854aba9462ebbaee1ff248"); + set.add("b87e12381d354360f7945299ad92a4d2"); + set.add("b8bd5737f81fddbaf120ebe43a0010e4"); + set.add("b92f1e45fdb62c1fd6d7f3e774b4b610"); + set.add("b9769bfc0d93a570d94fa130de750b1f"); + set.add("b980c7a501ce63ebb6e20b04348298b7"); + set.add("b9e4b006db3e1597b21fb7aba13f79c2"); + set.add("ba031cf2febc03ddbff278200dca45a0"); + set.add("bb0f54037f5ab70e96c7d8ba7f57ca4b"); + set.add("bb2eb6b3f7383d7ef521409fa7710385"); + set.add("bb3eb6a5dbe0582b31f35e5dc2b457a7"); + set.add("bbc22cc7f6851e06fadfac40a6225419"); + set.add("bc4d886813fe2eba9ccd7bef0b4d05ca"); + set.add("bc8162e261658ece13f8bc61aa43ab74"); + set.add("bc89ec14f4629c3afe1e286b07e588f6"); + set.add("bccdb576cb50b44ae34806c2a2781c52"); + set.add("bd10772f1554ccd6e245d6869d84efe8"); + set.add("bd969e90ff4b1362e2f24553124a66cc"); + set.add("bde145f138ed13399b8a747da20c1826"); + set.add("be11a726b56813c4b1aea0574c8302b2"); + set.add("be1cfa6a82eb4fbf7186efd6ddbb0161"); + set.add("be5f3dcf0badef84475741cc47e2ddc0"); + set.add("bf316e6ad7e72f7dc4033207dd033c76"); + set.add("bfadbf9c4cde6071e54e43a5da64aca9"); + set.add("c029503ea85e7a7742d826bc184d65ce"); + set.add("c049499ca03fd69115352e5d4be72de7"); + set.add("c0524ddd036991b4718f6ab0ab4e4f42"); + set.add("c056cf25df6751f7bb8a94bc4f64750f"); + set.add("c0a9c2fd4f0ed2b8f2bdc7f2c0a7a7ce"); + set.add("c165e480073bcdccb3fad1c5e507159f"); + set.add("c24ac1ab205eb3fbd1c64841f0f288d6"); + set.add("c26987c1c7e95810bbb6f2e284861494"); + set.add("c295b3b2493aff880daac1532a063b72"); + set.add("c2b18390691697037d5698b683eee361"); + set.add("c2cd680e3032ce3a70d3bffdb7d0582f"); + set.add("c2defcfb93d217c4be28aa27ec62978b"); + set.add("c332adf9d689dcbbb38fead7098781b3"); + set.add("c4d8f1baafe99501b0d80e8a9c8c3086"); + set.add("c4e5ca3e96b219539e3e61c3c4fbe5a9"); + set.add("c5e1448d1fb24ebcef161ee65f21a725"); + set.add("c60db0ccfc2a22a152f7470505eef8d3"); + set.add("c65e2561352e75a66b5738268b1d126a"); + set.add("c69c01ed9b781941561c3a9dcfacf7ca"); + set.add("c76bb0011d2519fc9e3af85de593e8a9"); + set.add("c7a946bb164a3f642e4c5f1b7af337f1"); + set.add("c833820441cbbf28a25d1ea7934ad6f8"); + set.add("c8762972b9325b7ec040c782aa9414d0"); + set.add("c8b1563c45f4fd4dc8ba5fafd5c566d2"); + set.add("c8f2a5a0533de5eae8d1d01da8fcfc1c"); + set.add("c94045226f625ab9a507552f64892fbe"); + set.add("ca365baface31f6167328e65a0aec03b"); + set.add("ca3dc74a6eb57042ea911afa05b1021b"); + set.add("ca5b57fca35c5bfa4281802b13381d0c"); + set.add("cab05efb1584bddbc5e4f064c1628a13"); + set.add("cad2db4a8a73a867a6cdacceec4044ac"); + set.add("cb6b65a06bbb9ba5536936188a08d836"); + set.add("cba49b7c8d1982635866a32956861df3"); + set.add("cbd94e882bdb84ec79ea2bebc1eb4aed"); + set.add("cc49ecf163d383488b44dbb17dd5b4d9"); + set.add("cc4bcc37de4d7bf87acea95ac914997e"); + set.add("cc9300ecd7f2799c750ca3efcde7ce20"); + set.add("ccccf43f691ed8bb94ac27d3eab98659"); + set.add("cd0d8952d3e5742f4bf62195e4b385ec"); + set.add("cd57964f0a86f3c9afbcca44733081d2"); + set.add("cd80d4f5366cd31ae31e377c636a773a"); + set.add("cd987a30667f7ff04a5852fd3f15fe3b"); + set.add("ce16d46a26c771b1adbff14cc7272bf2"); + set.add("ce43a9cf2d81a1e09237ed2279ca04be"); + set.add("ce7a500ffd8f5925bea7a48401fae95e"); + set.add("cf3894401e317e2545d0ae284802291f"); + set.add("cf779cafecefb6bae5137bb77582e7e2"); + set.add("cfdbc0be3829a6d55a5403ad92c77bcf"); + set.add("cff24b5ef480cd58324e927e4ba0ed37"); + set.add("d05a83622e817871306a3878a9d867e9"); + set.add("d060e6662cda17c2c523c74963d2d556"); + set.add("d0b058971d8a20e840de043c68c140b1"); + set.add("d0c122a8a62cb5728f1423816b26c49f"); + set.add("d141ed3ad5b33d6f92c976ad8d518b3b"); + set.add("d15c7bdb5fc7b9316d1cc60a85acdc64"); + set.add("d1c8ba5392f01f13dfef28e4ecd11cc2"); + set.add("d1e661c0bfe1c23ca4e91cfa0a40a9d3"); + set.add("d25a8aef0d42d7a31783e3da52dd4ee8"); + set.add("d26ef38d4ea39ba85171f444a8947443"); + set.add("d30b7d8663c7852f2be21d886b79a6eb"); + set.add("d315c862f290b99466e3d406d3104413"); + set.add("d4007ee1fd6ede4a148bdca7ab778dd3"); + set.add("d4805374b5f1ece75c0dd51d6283a0f6"); + set.add("d55b77d76f6ece8bbd33cb2afdbd040f"); + set.add("d570cb3cbfe88dfdf086bb1ab9ef76f8"); + set.add("d587ebc9902ba2620d52a610441470cc"); + set.add("d6196bfd55d758dd5a4899ce84cea85b"); + set.add("d6815ec0b3e12fea0f470c01c0da3888"); + set.add("d68971ffd271de2ddde6ac10c2817451"); + set.add("d68ff175d25d083eee4f31bf0701a0d8"); + set.add("d7fca16ff4e4ed99b72046f99533eef3"); + set.add("d815d7a8dd0446ba21ddbc37d3733f36"); + set.add("d8d40312d0751951b4c45f2100379950"); + set.add("d97e0d0fbea2a015d5e7ea229b99a6c3"); + set.add("d98c6a23fafafb0f9981f2123b1a1938"); + set.add("d99d6bfaede491ceae109a644cf54098"); + set.add("da0f523e815ce990a3a5f5b5574cec4a"); + set.add("da686b7f2a26a36051b273bbabdd7ecc"); + set.add("dadb54d4a8ba762a8d2a2fad5fcd7582"); + set.add("db03b5399e1a324660934ad81036d987"); + set.add("db29f48ac45b41ad389b1b4e8e30f553"); + set.add("db98a87e4be1e5b59f589335e1509996"); + set.add("db9be69a1dd929be69406f9e75533fd3"); + set.add("dba45dfda4d06aebf3adc194ca4ec35d"); + set.add("dc339714def04ec3e5e280faec8445e5"); + set.add("dc3fb757715c96f7c629b7712d87ca61"); + set.add("dc5dabf20b3fbee0d3a0b1d800c74d4f"); + set.add("dcc7b2c56f358641ea9a73c6a81479f5"); + set.add("dd922d98ecf5096482946d8671b647e3"); + set.add("dda8214d7b53392f8ed9fbe4178e90b9"); + set.add("ddd37af5af0a69bed06bc50cc0a6f4c2"); + set.add("de8d217fad9883d9bfdee5af01472971"); + set.add("def34fc0fd41527b300d3181a70cdecf"); + set.add("df00d9361332c48cba23bfcd41e799d4"); + set.add("df35772f10769bc28701c488d33e89b2"); + set.add("df769f9dc2477135b0c4320e7e7b4c2f"); + set.add("df95377a3f69b8fbe5dcdfa041491717"); + set.add("df98c3766238aa84f9a9dd92cd73fe72"); + set.add("dfd019d69302047a67434458d0fa8952"); + set.add("e037a9e26a8b319437ab7c962714dc56"); + set.add("e0d2f02d29a965fafd85a4ae6ad37246"); + set.add("e10e651cd85e41be3402f51885bbf107"); + set.add("e162d7b2e436ae6d369f4fbaf53af4b4"); + set.add("e176177a2b64669a6bcd1cf8beb35df2"); + set.add("e194252a63a3433b5a5278f68678b7dc"); + set.add("e19d16546c555d073454ea56ece1cbd6"); + set.add("e1cb375938189d4090b000ab41c77a06"); + set.add("e1ce2428389f0c2356881e626f161ccc"); + set.add("e27c22419443eb612af1620f4c8be007"); + set.add("e2a5adcdd7b01611736b6b74f8c506ee"); + set.add("e3084721ba7ae53996337e82c5f469ab"); + set.add("e35f4cccfe57bdd7338dadeba55609f1"); + set.add("e39a2ef2eaaaf7ba74623f14c917ee1d"); + set.add("e3e11cf57dc3f1c6ca59acb06370698f"); + set.add("e4557f7733332200116b753408cdb966"); + set.add("e48c96d6025b38addad2278f24c963ef"); + set.add("e48db7db130af48cccb2d830d3cbaa14"); + set.add("e4d169990e34bfeab0c6a780d6a49d58"); + set.add("e4ea1f6b01c9cdcf9e550792ed336384"); + set.add("e4ee8ada1fbbe886fb25a7f484609690"); + set.add("e54846d325334547923d8b64da70f529"); + set.add("e56ccf6eca77d62dde88c895abfc1c1a"); + set.add("e591790779db1c866b179d6f85b09dda"); + set.add("e5bec4799ceef43816054f92de9652b5"); + set.add("e5db5832d59e14d6999144fa8cd10e3f"); + set.add("e611fb9e857f9bee391056e1f971a0aa"); + set.add("e6321fdd099d70352883b45f6c2a20a9"); + set.add("e682fc42ee7ecdbf595116293cbe8a6b"); + set.add("e6ae48418d10883fe9657075b476274d"); + set.add("e6fa2a139e5c56f8f483aaeeee0b7fbb"); + set.add("e77d3c78240ec60f7f4dd67a2e71085a"); + set.add("e78ad15fb1fd450f9221147e458b1abd"); + set.add("e7a1dc89a6cab821776ea61fe6ba10f4"); + set.add("e7df074666f6caa44b798342bdab6230"); + set.add("e7f25163d78c2c658300cd0f9a8a3b04"); + set.add("e80f22347419025053de7da1f07912ec"); + set.add("e828123fa3cdf86dc0fe1b5c86d7c87d"); + set.add("e8764c00097a0a1254f43a16c98a1d7f"); + set.add("e89df60deddf270cbc2232bbe26420d1"); + set.add("e8d289f3c1aa961cf4ac8d164e286dde"); + set.add("e9051eac7829dc1a95987230fb21d2d9"); + set.add("e90846d2c3e16de5ed5dff4c21356edd"); + set.add("e954907cdfba1cf07f19f64af5cf45b1"); + set.add("e96e004e988b8e36b2ab9ed1b0f65649"); + set.add("e984d3924451d3498a3efccd845a77fe"); + set.add("e98b4097ddb057755e561c33a0f3428d"); + set.add("e9a2ba17cc4b93063d61813359fd6798"); + set.add("ea90b42f6ada6e0ac5d179af4129022d"); + set.add("eab4231af5b3ffab13f9a04b8aef0fad"); + set.add("eac17a7d30d24e60e6b2ce10059b37a0"); + set.add("eaf3af4d0b61df60ee8fe3a808de6ffd"); + set.add("eb3178d36a578fd8b430ea82429ee145"); + set.add("eb4fbf9835a3584e6015260f0dff2174"); + set.add("ec3a9437b1481db4d8cbc3b4fc0888a1"); + set.add("ec47d133c23dba3d015ae730a1b9659f"); + set.add("ec6d321581a133fee9766eedff4db9d6"); + set.add("eca16f6d986bd893af3c4a97b99df623"); + set.add("ecf09182c51e5110d636828364ba3ee6"); + set.add("ecfbf6f7017f0981ba6d30331c9dc870"); + set.add("ed1eaef061f0d30503a64f27d8ea2825"); + set.add("ed2e4441ad7dcbe0a5d3e62bc31aa9bc"); + set.add("ed6304572c0f1283fd06f9c09ef32b61"); + set.add("ed7d650fc0f5de8c9da107e53025f220"); + set.add("ef405c5b0de638c01cf57c379aaff45b"); + set.add("ef5ec03860cd32e910a3ddb2d8740300"); + set.add("efdc8c21ee843f98a0dc08ef694b6db7"); + set.add("f0111af67e822944e1dc1ab24e5b8458"); + set.add("f0e8026289bc1f9109b25f4599556aaf"); + set.add("f0ff5a7aa6f4e8785fa406636618c01d"); + set.add("f19a809facb46a7a094c17039126eb3e"); + set.add("f1c7524d454c25cdd837662a80709030"); + set.add("f202d26f911074235ac8e4a5c1ed4dad"); + set.add("f2250dd8736aa007a7e2530dca1c6081"); + set.add("f2e9d36561ed03eb23d38005919625d2"); + set.add("f303e8a2a96635d2f44c414229c349bb"); + set.add("f35b8eac398bae58ba622ef643f64aa2"); + set.add("f3a37dbd51e59af2801272fffe457d64"); + set.add("f3c4afc965427977685da607f8a6edca"); + set.add("f468c204661ab47379613a1d03f56571"); + set.add("f4ff8d1667c95377ac96e870170bfe64"); + set.add("f585dccae7fae67affbf500ecf9a3839"); + set.add("f59a4193ec432bd26f780a6b74b8be38"); + set.add("f5d44e9d1523c3e6834108d9c76b2da9"); + set.add("f69f58acf2b80f5dc29682c64de8da7f"); + set.add("f6adafaf98b92438e1ad916e51a65366"); + set.add("f6f175a7910c5039d0fa51393c920df8"); + set.add("f71a00225b1abf1dddfcace79d0345a2"); + set.add("f7446eb342242f011c118bb3439335a0"); + set.add("f76e6e86c9b0d84638c1c26c55b16cc4"); + set.add("f775463704e3d13817abd9674d62f468"); + set.add("f80df9b85c1219fd2030ada584fbfc35"); + set.add("f843fa1d0cd00122fcbcfd7caf1cb8ca"); + set.add("f88e05d8303a6e5dfbd60ceed3988d78"); + set.add("f92dbcd91aac65e0770f5fe947fc5a80"); + set.add("f9512c5cc198adeff74fed3d4b0f4509"); + set.add("f9e1ffe33f3676d0e39bc24e63cf3a99"); + set.add("fa492225fbf03ad58ee88b6d84914583"); + set.add("fa6279cc58de3fe6c617559684afec4f"); + set.add("fb2a4db1a1a68dae79653dd2e32ade50"); + set.add("fb2bc93f011d62ac03aed40d92b26ba2"); + set.add("fb9b6d2d2d5e3c219e0163092181e014"); + set.add("fbdc7fc274e5c71226372606beedb706"); + set.add("fbe25dc54e2761c2c5e9f1f3a98d7f0f"); + set.add("fbec5f910872b333be655c5143b1cb37"); + set.add("fc372707722b210b257ef9e2f731edc3"); + set.add("fcedc7e1d4fc17c7c4f2c6f6c7a820e0"); + set.add("fcff88f351f2535dcbab726dec9060ee"); + set.add("fd15d45e5f876ac3ff10cef478077e8b"); + set.add("fd21ff84af0fe74de102f1985d068dee"); + set.add("fd30f89057cd8ad151d612def38afb41"); + set.add("fdab6eed0ecadf924ae128f25e8b1a10"); + set.add("fdced723077daed39d0e4da044f75d64"); + set.add("fddbc361461ae318e147e420a805a428"); + set.add("fdee78ddeb6f567a636b9850f942256f"); + set.add("fe858217631f3aaf02d8aaf849c7b2c9"); + set.add("fec4bbfe3563397790d48ce6f5b48260"); + set.add("ff73d7804897f4e1624a3b5571e48fbb"); + set.add("ff78c3b27889d5365a03b3a3fd3a4c1e"); + set.add("ffac65c383eb053e54b267fe4dfd2141"); + + DIGESTS = Collections.unmodifiableSet(set); + } + + private PreferredMotorDigests() { + // Prevent instantiation + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/BodyComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/BodyComponentSaver.java new file mode 100644 index 00000000..1a65676b --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/BodyComponentSaver.java @@ -0,0 +1,15 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +public class BodyComponentSaver extends ExternalComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + // Body components have a natural length, store it now + elements.add(""+((net.sf.openrocket.rocketcomponent.BodyComponent)c).getLength()+""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java new file mode 100644 index 00000000..8b1ef6a5 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java @@ -0,0 +1,36 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class BodyTubeSaver extends SymmetricComponentSaver { + + private static final BodyTubeSaver instance = new BodyTubeSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube) c; + + if (tube.isOuterRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + tube.getOuterRadius() + ""); + + if (tube.isMotorMount()) { + elements.addAll(motorMountParams(tube)); + } + } + + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/BulkheadSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/BulkheadSaver.java new file mode 100644 index 00000000..9c0ef2d8 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/BulkheadSaver.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class BulkheadSaver extends RadiusRingComponentSaver { + + private static final BulkheadSaver instance = new BulkheadSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/CenteringRingSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/CenteringRingSaver.java new file mode 100644 index 00000000..bb428a3d --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/CenteringRingSaver.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class CenteringRingSaver extends RadiusRingComponentSaver { + + private static final CenteringRingSaver instance = new CenteringRingSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/ComponentAssemblySaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/ComponentAssemblySaver.java new file mode 100644 index 00000000..9e9d609d --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/ComponentAssemblySaver.java @@ -0,0 +1,7 @@ +package net.sf.openrocket.file.openrocket.savers; + +public class ComponentAssemblySaver extends RocketComponentSaver { + + // No-op + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/EllipticalFinSetSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/EllipticalFinSetSaver.java new file mode 100644 index 00000000..31b9dfb0 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/EllipticalFinSetSaver.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class EllipticalFinSetSaver extends FinSetSaver { + + private static final EllipticalFinSetSaver instance = new EllipticalFinSetSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c,list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + net.sf.openrocket.rocketcomponent.EllipticalFinSet fins = (net.sf.openrocket.rocketcomponent.EllipticalFinSet)c; + elements.add(""+fins.getLength()+""); + elements.add(""+fins.getHeight()+""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/EngineBlockSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/EngineBlockSaver.java new file mode 100644 index 00000000..921ddde5 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/EngineBlockSaver.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class EngineBlockSaver extends ThicknessRingComponentSaver { + + private static final EngineBlockSaver instance = new EngineBlockSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/ExternalComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/ExternalComponentSaver.java new file mode 100644 index 00000000..2f1b2a4e --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/ExternalComponentSaver.java @@ -0,0 +1,23 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.ExternalComponent; + + +public class ExternalComponentSaver extends RocketComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + ExternalComponent ext = (ExternalComponent)c; + + // Finish enum names are currently the same except for case + elements.add("" + ext.getFinish().name().toLowerCase() + ""); + + // Material + elements.add(materialParam(ext.getMaterial())); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java new file mode 100644 index 00000000..8756d89f --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.util.MathUtil; + +public class FinSetSaver extends ExternalComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + net.sf.openrocket.rocketcomponent.FinSet fins = (net.sf.openrocket.rocketcomponent.FinSet) c; + elements.add("" + fins.getFinCount() + ""); + elements.add("" + (fins.getBaseRotation() * 180.0 / Math.PI) + ""); + elements.add("" + fins.getThickness() + ""); + elements.add("" + fins.getCrossSection().name().toLowerCase() + + ""); + elements.add("" + (fins.getCantAngle() * 180.0 / Math.PI) + ""); + + // Save fin tabs only if they exist (compatibility with file version < 1.1) + if (!MathUtil.equals(fins.getTabHeight(),0) && + !MathUtil.equals(fins.getTabLength(), 0)) { + + elements.add("" + fins.getTabHeight() + ""); + elements.add("" + fins.getTabLength() + ""); + elements.add("" + + fins.getTabShift() + ""); + + } + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/FreeformFinSetSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/FreeformFinSetSaver.java new file mode 100644 index 00000000..c310e4d9 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/FreeformFinSetSaver.java @@ -0,0 +1,36 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.util.Coordinate; + + +public class FreeformFinSetSaver extends FinSetSaver { + + private static final FreeformFinSetSaver instance = new FreeformFinSetSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c,list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + FreeformFinSet fins = (FreeformFinSet)c; + elements.add(""); + for (Coordinate p: fins.getFinPoints()) { + elements.add(" "); + } + elements.add(""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/InnerTubeSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/InnerTubeSaver.java new file mode 100644 index 00000000..7eb0a538 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/InnerTubeSaver.java @@ -0,0 +1,43 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.InnerTube; + + +public class InnerTubeSaver extends ThicknessRingComponentSaver { + + private static final InnerTubeSaver instance = new InnerTubeSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + InnerTube tube = (InnerTube) c; + + elements.add("" + tube.getClusterConfiguration().getXMLName() + + ""); + elements.add("" + tube.getClusterScale() + ""); + elements.add("" + (tube.getClusterRotation() * 180.0 / Math.PI) + + ""); + + if (tube.isMotorMount()) { + elements.addAll(motorMountParams(tube)); + } + + + } + + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/InternalComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/InternalComponentSaver.java new file mode 100644 index 00000000..0a2dbe72 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/InternalComponentSaver.java @@ -0,0 +1,14 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +public class InternalComponentSaver extends RocketComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + // Nothing to save + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java new file mode 100644 index 00000000..1598bc41 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.file.openrocket.savers; + +import net.sf.openrocket.rocketcomponent.LaunchLug; + +import java.util.ArrayList; +import java.util.List; + + +public class LaunchLugSaver extends ExternalComponentSaver { + + private static final LaunchLugSaver instance = new LaunchLugSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + LaunchLug lug = (LaunchLug) c; + + elements.add("" + lug.getOuterRadius() + ""); + elements.add("" + lug.getLength() + ""); + elements.add("" + lug.getThickness() + ""); + elements.add("" + (lug.getRadialDirection()*180.0/Math.PI) + ""); + } + + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/MassComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/MassComponentSaver.java new file mode 100644 index 00000000..093303c2 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/MassComponentSaver.java @@ -0,0 +1,32 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.MassComponent; + + +public class MassComponentSaver extends MassObjectSaver { + + private static final MassComponentSaver instance = new MassComponentSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + MassComponent mass = (MassComponent) c; + + elements.add("" + mass.getMass() + ""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/MassObjectSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/MassObjectSaver.java new file mode 100644 index 00000000..298cb263 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/MassObjectSaver.java @@ -0,0 +1,23 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.MassObject; + + +public class MassObjectSaver extends InternalComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + MassObject mass = (MassObject) c; + + elements.add("" + mass.getLength() + ""); + elements.add("" + mass.getRadius() + ""); + elements.add("" + mass.getRadialPosition() + ""); + elements.add("" + (mass.getRadialDirection() * 180.0 / Math.PI) + + ""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java new file mode 100644 index 00000000..9733254f --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class NoseConeSaver extends TransitionSaver { + + private static final NoseConeSaver instance = new NoseConeSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c,list); + list.add(""); + + return list; + } + + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + // Transition handles nose cone saving as well + } +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/ParachuteSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/ParachuteSaver.java new file mode 100644 index 00000000..7d906e67 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/ParachuteSaver.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.Parachute; + + +public class ParachuteSaver extends RecoveryDeviceSaver { + + private static final ParachuteSaver instance = new ParachuteSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + Parachute para = (Parachute) c; + + elements.add("" + para.getDiameter() + ""); + elements.add("" + para.getLineCount() + ""); + elements.add("" + para.getLineLength() + ""); + elements.add(materialParam("linematerial", para.getLineMaterial())); + } + + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RadiusRingComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RadiusRingComponentSaver.java new file mode 100644 index 00000000..c1b90204 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RadiusRingComponentSaver.java @@ -0,0 +1,28 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.RadiusRingComponent; + + +public class RadiusRingComponentSaver extends RingComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + RadiusRingComponent comp = (RadiusRingComponent)c; + if (comp.isOuterRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + comp.getOuterRadius() + ""); + if (!(comp instanceof Bulkhead)) { + if (comp.isInnerRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + comp.getInnerRadius() + ""); + } + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RecoveryDeviceSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RecoveryDeviceSaver.java new file mode 100644 index 00000000..22dcaa9c --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RecoveryDeviceSaver.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.RecoveryDevice; + + +public class RecoveryDeviceSaver extends MassObjectSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + RecoveryDevice dev = (RecoveryDevice) c; + + if (dev.isCDAutomatic()) + elements.add("auto"); + else + elements.add("" + dev.getCD() + ""); + + elements.add("" + dev.getDeployEvent().name().toLowerCase() + ""); + elements.add("" + dev.getDeployAltitude() + ""); + elements.add("" + dev.getDeployDelay() + ""); + elements.add(materialParam(dev.getMaterial())); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RingComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RingComponentSaver.java new file mode 100644 index 00000000..c1999fba --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RingComponentSaver.java @@ -0,0 +1,22 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.RingComponent; + + +public class RingComponentSaver extends StructuralComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + RingComponent ring = (RingComponent) c; + + elements.add("" + ring.getLength() + ""); + elements.add("" + ring.getRadialPosition() + ""); + elements.add("" + (ring.getRadialDirection() * 180.0 / Math.PI) + + ""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java new file mode 100644 index 00000000..44e71e4b --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java @@ -0,0 +1,159 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.ComponentAssembly; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Color; +import net.sf.openrocket.util.LineStyle; + + +public class RocketComponentSaver { + + protected RocketComponentSaver() { + // Prevent instantiation from outside the package + } + + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + elements.add("" + RocketSaver.escapeXML(c.getName()) + ""); + + + // Save color and line style if significant + if (!(c instanceof Rocket || c instanceof ComponentAssembly)) { + Color color = c.getColor(); + if (color != null) { + elements.add(""); + } + + LineStyle style = c.getLineStyle(); + if (style != null) { + // Type names currently equivalent to the enum names except for case. + elements.add("" + style.name().toLowerCase() + ""); + } + } + + + // Save position unless "AFTER" + if (c.getRelativePosition() != RocketComponent.Position.AFTER) { + // The type names are currently equivalent to the enum names except for case. + String type = c.getRelativePosition().name().toLowerCase(); + elements.add("" + c.getPositionValue() + ""); + } + + + // Overrides + boolean overridden = false; + if (c.isMassOverridden()) { + elements.add("" + c.getOverrideMass() + ""); + overridden = true; + } + if (c.isCGOverridden()) { + elements.add("" + c.getOverrideCGX() + ""); + overridden = true; + } + if (overridden) { + elements.add("" + c.getOverrideSubcomponents() + + ""); + } + + + // Comment + if (c.getComment().length() > 0) { + elements.add("" + RocketSaver.escapeXML(c.getComment()) + ""); + } + + } + + + + + protected final String materialParam(Material mat) { + return materialParam("material", mat); + } + + + protected final String materialParam(String tag, Material mat) { + String str = "<" + tag; + + switch (mat.getType()) { + case LINE: + str += " type=\"line\""; + break; + case SURFACE: + str += " type=\"surface\""; + break; + case BULK: + str += " type=\"bulk\""; + break; + default: + throw new BugException("Unknown material type: " + mat.getType()); + } + + return str + " density=\"" + mat.getDensity() + "\">" + RocketSaver.escapeXML(mat.getName()) + ""; + } + + + protected final List motorMountParams(MotorMount mount) { + if (!mount.isMotorMount()) + return Collections.emptyList(); + + String[] motorConfigIDs = ((RocketComponent) mount).getRocket().getMotorConfigurationIDs(); + List elements = new ArrayList(); + + elements.add(""); + + for (String id : motorConfigIDs) { + Motor motor = mount.getMotor(id); + + // Nothing is stored if no motor loaded + if (motor == null) + continue; + + elements.add(" "); + if (motor.getMotorType() != Motor.Type.UNKNOWN) { + elements.add(" " + motor.getMotorType().name().toLowerCase() + ""); + } + if (motor instanceof ThrustCurveMotor) { + ThrustCurveMotor m = (ThrustCurveMotor) motor; + elements.add(" " + RocketSaver.escapeXML(m.getManufacturer().getSimpleName()) + + ""); + elements.add(" " + MotorDigest.digestMotor(m) + ""); + } + elements.add(" " + RocketSaver.escapeXML(motor.getDesignation()) + ""); + elements.add(" " + motor.getDiameter() + ""); + elements.add(" " + motor.getLength() + ""); + + // Motor delay + if (mount.getMotorDelay(id) == Motor.PLUGGED) { + elements.add(" none"); + } else { + elements.add(" " + mount.getMotorDelay(id) + ""); + } + + elements.add(" "); + } + + elements.add(" " + + mount.getIgnitionEvent().name().toLowerCase().replace("_", "") + + ""); + + elements.add(" " + mount.getIgnitionDelay() + ""); + elements.add(" " + mount.getMotorOverhang() + ""); + + elements.add(""); + + return elements; + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java new file mode 100644 index 00000000..e8b7a345 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java @@ -0,0 +1,73 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.ReferenceType; +import net.sf.openrocket.rocketcomponent.Rocket; + + +public class RocketSaver extends RocketComponentSaver { + + private static final RocketSaver instance = new RocketSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + Rocket rocket = (Rocket) c; + + if (rocket.getDesigner().length() > 0) { + elements.add("" + + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getDesigner()) + + ""); + } + if (rocket.getRevision().length() > 0) { + elements.add("" + + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getRevision()) + + ""); + } + + + // Motor configurations + String defId = rocket.getDefaultConfiguration().getMotorConfigurationID(); + for (String id : rocket.getMotorConfigurationIDs()) { + if (id == null) + continue; + + String str = ""; + } + elements.add(str); + } + + // Reference diameter + elements.add("" + rocket.getReferenceType().name().toLowerCase() + + ""); + if (rocket.getReferenceType() == ReferenceType.CUSTOM) { + elements.add("" + rocket.getCustomReferenceLength() + + ""); + } + + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/ShockCordSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/ShockCordSaver.java new file mode 100644 index 00000000..8b8ae01f --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/ShockCordSaver.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.ShockCord; + + +public class ShockCordSaver extends MassObjectSaver { + + private static final ShockCordSaver instance = new ShockCordSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + ShockCord mass = (ShockCord) c; + + elements.add("" + mass.getCordLength() + ""); + elements.add(materialParam(mass.getMaterial())); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/StageSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/StageSaver.java new file mode 100644 index 00000000..0fd0f6f3 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/StageSaver.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; + +public class StageSaver extends ComponentAssemblySaver { + + private static final StageSaver instance = new StageSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c,list); + list.add(""); + + return list; + } + + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/StreamerSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/StreamerSaver.java new file mode 100644 index 00000000..5b92852e --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/StreamerSaver.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.Streamer; + + +public class StreamerSaver extends RecoveryDeviceSaver { + + private static final StreamerSaver instance = new StreamerSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + Streamer st = (Streamer) c; + + elements.add("" + st.getStripLength() + ""); + elements.add("" + st.getStripWidth() + ""); + } + + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/StructuralComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/StructuralComponentSaver.java new file mode 100644 index 00000000..5cf5ce70 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/StructuralComponentSaver.java @@ -0,0 +1,18 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.StructuralComponent; + + +public class StructuralComponentSaver extends InternalComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + StructuralComponent comp = (StructuralComponent)c; + elements.add(materialParam(comp.getMaterial())); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/SymmetricComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/SymmetricComponentSaver.java new file mode 100644 index 00000000..bab5eccb --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/SymmetricComponentSaver.java @@ -0,0 +1,18 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +public class SymmetricComponentSaver extends BodyComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + net.sf.openrocket.rocketcomponent.SymmetricComponent comp = (net.sf.openrocket.rocketcomponent.SymmetricComponent)c; + if (comp.isFilled()) + elements.add("filled"); + else + elements.add(""+comp.getThickness()+""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/ThicknessRingComponentSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/ThicknessRingComponentSaver.java new file mode 100644 index 00000000..d5a3c1b3 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/ThicknessRingComponentSaver.java @@ -0,0 +1,22 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.List; + +import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; + + +public class ThicknessRingComponentSaver extends RingComponentSaver { + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + ThicknessRingComponent comp = (ThicknessRingComponent)c; + if (comp.isOuterRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + comp.getOuterRadius() + ""); + elements.add("" + comp.getThickness() + ""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java new file mode 100644 index 00000000..d7bb1ed7 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java @@ -0,0 +1,79 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Transition; + + +public class TransitionSaver extends SymmetricComponentSaver { + + private static final TransitionSaver instance = new TransitionSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + + + /* + * Note: This method must be capable of handling nose cones as well. + */ + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + net.sf.openrocket.rocketcomponent.Transition trans = (net.sf.openrocket.rocketcomponent.Transition) c; + boolean nosecone = (trans instanceof NoseCone); + + + Transition.Shape shape = trans.getType(); + elements.add("" + shape.name().toLowerCase() + ""); + if (shape.isClippable()) { + elements.add("" + trans.isClipped() + ""); + } + if (shape.usesParameter()) { + elements.add("" + trans.getShapeParameter() + ""); + } + + + if (!nosecone) { + if (trans.isForeRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + trans.getForeRadius() + ""); + } + + if (trans.isAftRadiusAutomatic()) + elements.add("auto"); + else + elements.add("" + trans.getAftRadius() + ""); + + + if (!nosecone) { + elements.add("" + trans.getForeShoulderRadius() + + ""); + elements.add("" + trans.getForeShoulderLength() + + ""); + elements.add("" + trans.getForeShoulderThickness() + + ""); + elements.add("" + trans.isForeShoulderCapped() + + ""); + } + + elements.add("" + trans.getAftShoulderRadius() + + ""); + elements.add("" + trans.getAftShoulderLength() + + ""); + elements.add("" + trans.getAftShoulderThickness() + + ""); + elements.add("" + trans.isAftShoulderCapped() + + ""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/TrapezoidFinSetSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/TrapezoidFinSetSaver.java new file mode 100644 index 00000000..21ac2aa8 --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/TrapezoidFinSetSaver.java @@ -0,0 +1,31 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class TrapezoidFinSetSaver extends FinSetSaver { + + private static final TrapezoidFinSetSaver instance = new TrapezoidFinSetSaver(); + + public static ArrayList getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + ArrayList list = new ArrayList(); + + list.add(""); + instance.addParams(c,list); + list.add(""); + + return list; + } + + @Override + protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List elements) { + super.addParams(c, elements); + + net.sf.openrocket.rocketcomponent.TrapezoidFinSet fins = (net.sf.openrocket.rocketcomponent.TrapezoidFinSet)c; + elements.add(""+fins.getRootChord()+""); + elements.add(""+fins.getTipChord()+""); + elements.add(""+fins.getSweep()+""); + elements.add(""+fins.getHeight()+""); + } + +} diff --git a/core/src/net/sf/openrocket/file/openrocket/savers/TubeCouplerSaver.java b/core/src/net/sf/openrocket/file/openrocket/savers/TubeCouplerSaver.java new file mode 100644 index 00000000..0e19fd8d --- /dev/null +++ b/core/src/net/sf/openrocket/file/openrocket/savers/TubeCouplerSaver.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.file.openrocket.savers; + +import java.util.ArrayList; +import java.util.List; + +public class TubeCouplerSaver extends ThicknessRingComponentSaver { + + private static final TubeCouplerSaver instance = new TubeCouplerSaver(); + + public static List getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { + List list = new ArrayList(); + + list.add(""); + instance.addParams(c, list); + list.add(""); + + return list; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/TipShapeCode.java b/core/src/net/sf/openrocket/file/rocksim/TipShapeCode.java new file mode 100644 index 00000000..976bdc9c --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/TipShapeCode.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.file.rocksim; + +import net.sf.openrocket.rocketcomponent.FinSet; + +/** + */ +public final class TipShapeCode { + + /** + * Convert a Rocksim tip shape to an OpenRocket CrossSection. + * + * @param tipShape the tip shape code from Rocksim + * + * @return a CrossSection instance + */ + public static FinSet.CrossSection convertTipShapeCode (int tipShape) { + switch (tipShape) { + case 0: + return FinSet.CrossSection.SQUARE; + case 1: + return FinSet.CrossSection.ROUNDED; + case 2: + return FinSet.CrossSection.AIRFOIL; + default: + return FinSet.CrossSection.SQUARE; + } + } + + public static int convertTipShapeCode (FinSet.CrossSection cs) { + if (FinSet.CrossSection.ROUNDED.equals(cs)) { + return 1; + } + if (FinSet.CrossSection.AIRFOIL.equals(cs)) { + return 2; + } + return 0; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/AbstractTransitionDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/AbstractTransitionDTO.java new file mode 100644 index 00000000..ce5db241 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/AbstractTransitionDTO.java @@ -0,0 +1,74 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.file.rocksim.importt.RocksimNoseConeCode; +import net.sf.openrocket.rocketcomponent.Transition; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + */ +@XmlAccessorType(XmlAccessType.FIELD) +public class AbstractTransitionDTO extends BasePartDTO { + @XmlElement(name = "ShapeCode") + private int shapeCode = 1; + @XmlElement(name = "ConstructionType") + private int constructionType = 1; + @XmlElement(name = "WallThickness") + private double wallThickness = 0d; + @XmlElement(name = "ShapeParameter") + private double shapeParameter = 0d; + + protected AbstractTransitionDTO() { + + } + + protected AbstractTransitionDTO(Transition nc) { + super(nc); + setConstructionType(nc.isFilled() ? 0 : 1); + setShapeCode(RocksimNoseConeCode.toCode(nc.getType())); + + if (Transition.Shape.POWER.equals(nc.getType()) || + Transition.Shape.HAACK.equals(nc.getType()) || + Transition.Shape.PARABOLIC.equals(nc.getType())) { + setShapeParameter(nc.getShapeParameter()); + } + + setWallThickness(nc.getThickness() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + + } + + public int getShapeCode() { + return shapeCode; + } + + public void setShapeCode(int theShapeCode) { + shapeCode = theShapeCode; + } + + public int getConstructionType() { + return constructionType; + } + + public void setConstructionType(int theConstructionType) { + constructionType = theConstructionType; + } + + public double getWallThickness() { + return wallThickness; + } + + public void setWallThickness(double theWallThickness) { + wallThickness = theWallThickness; + } + + public double getShapeParameter() { + return shapeParameter; + } + + public void setShapeParameter(double theShapeParameter) { + shapeParameter = theShapeParameter; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/BasePartDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/BasePartDTO.java new file mode 100644 index 00000000..48a4d910 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/BasePartDTO.java @@ -0,0 +1,238 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.BaseHandler; +import net.sf.openrocket.file.rocksim.importt.RocksimDensityType; +import net.sf.openrocket.file.rocksim.importt.RocksimFinishCode; +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.file.rocksim.importt.RocksimLocationMode; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.StructuralComponent; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement +@XmlAccessorType(XmlAccessType.FIELD) +public abstract class BasePartDTO { + + @XmlElement(name = "KnownMass") + private Double knownMass = 0d; + @XmlElement(name = "Density") + private double density = 0d; + @XmlElement(name = "Material") + private String material = ""; + @XmlElement(name = "Name") + private String name = ""; + @XmlElement(name = "KnownCG") + private Double knownCG = null; + @XmlElement(name = "UseKnownCG") + private int useKnownCG = 1; + @XmlElement(name = "Xb") + private double xb = 0; + @XmlElement(name = "CalcMass") + private double calcMass = 0d; + @XmlElement(name = "CalcCG") + private double calcCG = 0d; + @XmlElement(name = "DensityType") + private int densityType = 0; + @XmlElement(name = "RadialLoc") + private String radialLoc = "0."; + @XmlElement(name = "RadialAngle") + private double radialAngle = 0; + @XmlElement(name = "LocationMode") + private int locationMode = 0; + @XmlElement(name = "Len") + private double len = 0d; + @XmlElement(name = "FinishCode") + private int finishCode = 0; + + protected BasePartDTO() { + } + + protected BasePartDTO(RocketComponent ec) { + setCalcCG(ec.getCG().x * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setCalcMass(ec.getComponentMass() * RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + setKnownCG(ec.getOverrideCGX() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setKnownMass(ec.getOverrideMass() * RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + setLen(ec.getLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setUseKnownCG(ec.isCGOverridden() || ec.isMassOverridden() ? 1 : 0); + setName(ec.getName()); + + setXb(ec.getPositionValue() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + if (ec instanceof ExternalComponent) { + ExternalComponent comp = (ExternalComponent) ec; + setLocationMode(RocksimLocationMode.toCode(comp.getRelativePosition())); + + if (comp.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { + setXb(-1 * getXb()); + } + setDensity(comp.getMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY); + setDensityType(RocksimDensityType.toCode(comp.getMaterial().getType())); + String material = comp.getMaterial().getName(); + if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { + material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); + } + setMaterial(material); + + setFinishCode(RocksimFinishCode.toCode(comp.getFinish())); + } + else if (ec instanceof StructuralComponent) { + StructuralComponent comp = (StructuralComponent) ec; + + setLocationMode(RocksimLocationMode.toCode(comp.getRelativePosition())); + if (comp.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { + setXb(-1 * getXb()); + } + setDensity(comp.getMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY); + setDensityType(RocksimDensityType.toCode(comp.getMaterial().getType())); + String material = comp.getMaterial().getName(); + if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { + material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); + } + setMaterial(material); + } + else if (ec instanceof RecoveryDevice) { + RecoveryDevice comp = (RecoveryDevice) ec; + + setLocationMode(RocksimLocationMode.toCode(comp.getRelativePosition())); + if (comp.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { + setXb(-1 * getXb()); + } + setDensity(comp.getMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_SURFACE_DENSITY); + setDensityType(RocksimDensityType.toCode(comp.getMaterial().getType())); + String material = comp.getMaterial().getName(); + if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { + material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); + } + setMaterial(material); + + } + } + + public Double getKnownMass() { + return knownMass; + } + + public void setKnownMass(Double theKnownMass) { + knownMass = theKnownMass; + } + + public double getDensity() { + return density; + } + + public void setDensity(double theDensity) { + density = theDensity; + } + + public String getMaterial() { + return material; + } + + public void setMaterial(String theMaterial) { + material = theMaterial; + } + + public String getName() { + return name; + } + + public void setName(String theName) { + name = theName; + } + + public Double getKnownCG() { + return knownCG; + } + + public void setKnownCG(Double theKnownCG) { + knownCG = theKnownCG; + } + + public int getUseKnownCG() { + return useKnownCG; + } + + public void setUseKnownCG(int theUseKnownCG) { + useKnownCG = theUseKnownCG; + } + + public double getXb() { + return xb; + } + + public void setXb(double theXb) { + xb = theXb; + } + + public double getCalcMass() { + return calcMass; + } + + public void setCalcMass(double theCalcMass) { + calcMass = theCalcMass; + } + + public double getCalcCG() { + return calcCG; + } + + public void setCalcCG(double theCalcCG) { + calcCG = theCalcCG; + } + + public int getDensityType() { + return densityType; + } + + public void setDensityType(int theDensityType) { + densityType = theDensityType; + } + + public String getRadialLoc() { + return radialLoc; + } + + public void setRadialLoc(String theRadialLoc) { + radialLoc = theRadialLoc; + } + + public double getRadialAngle() { + return radialAngle; + } + + public void setRadialAngle(double theRadialAngle) { + radialAngle = theRadialAngle; + } + + public int getLocationMode() { + return locationMode; + } + + public void setLocationMode(int theLocationMode) { + locationMode = theLocationMode; + } + + public double getLen() { + return len; + } + + public void setLen(double theLen) { + len = theLen; + } + + public int getFinishCode() { + return finishCode; + } + + public void setFinishCode(int theFinishCode) { + finishCode = theFinishCode; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java new file mode 100644 index 00000000..f9325d85 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java @@ -0,0 +1,182 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TubeCoupler; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +/** + */ +@XmlRootElement(name = "BodyTube") +@XmlAccessorType(XmlAccessType.FIELD) +public class BodyTubeDTO extends BasePartDTO { + + @XmlElement(name = "OD") + private double od = 0d; + @XmlElement(name = "ID") + private double id = 0d; + @XmlElement(name = "IsMotorMount") + private int isMotorMount = 0; + @XmlElement(name = "MotorDia") + private double motorDia = 0d; + @XmlElement(name = "EngineOverhang") + private double engineOverhang = 0d; + @XmlElement(name = "IsInsideTube") + private int isInsideTube = 0; + @XmlElementWrapper(name = "AttachedParts") + @XmlElementRefs({ + @XmlElementRef(name = "BodyTube", type = BodyTubeDTO.class), + @XmlElementRef(name = "BodyTube", type = InnerBodyTubeDTO.class), + @XmlElementRef(name = "Ring", type = CenteringRingDTO.class), + @XmlElementRef(name = "LaunchLug", type = LaunchLugDTO.class), + @XmlElementRef(name = "FinSet", type = FinSetDTO.class), + @XmlElementRef(name = "CustomFinSet", type = CustomFinSetDTO.class), + @XmlElementRef(name = "Streamer", type = StreamerDTO.class), + @XmlElementRef(name = "Parachute", type = ParachuteDTO.class), + @XmlElementRef(name = "MassObject", type = MassObjectDTO.class)}) + List attachedParts = new ArrayList(); + + public BodyTubeDTO() { + } + + public BodyTubeDTO(InnerTube inner) { + super(inner); + } + + public BodyTubeDTO(BodyTube bt) { + super(bt); + + setEngineOverhang(bt.getMotorOverhang() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setId(bt.getInnerRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setOd(bt.getOuterRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setMotorDia((bt.getMotorMountDiameter() / 2) * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setMotorMount(bt.isMotorMount()); + + List children = bt.getChildren(); + for (int i = 0; i < children.size(); i++) { + RocketComponent rocketComponents = children.get(i); + if (rocketComponents instanceof InnerTube) { + attachedParts.add(new InnerBodyTubeDTO((InnerTube) rocketComponents)); + } else if (rocketComponents instanceof BodyTube) { + attachedParts.add(new BodyTubeDTO((BodyTube) rocketComponents)); + } else if (rocketComponents instanceof Transition) { + attachedParts.add(new TransitionDTO((Transition) rocketComponents)); + } else if (rocketComponents instanceof EngineBlock) { + attachedParts.add(new EngineBlockDTO((EngineBlock) rocketComponents)); + } else if (rocketComponents instanceof TubeCoupler) { + attachedParts.add(new TubeCouplerDTO((TubeCoupler) rocketComponents)); + } else if (rocketComponents instanceof CenteringRing) { + attachedParts.add(new CenteringRingDTO((CenteringRing) rocketComponents)); + } else if (rocketComponents instanceof Bulkhead) { + attachedParts.add(new BulkheadDTO((Bulkhead) rocketComponents)); + } else if (rocketComponents instanceof LaunchLug) { + attachedParts.add(new LaunchLugDTO((LaunchLug) rocketComponents)); + } else if (rocketComponents instanceof Streamer) { + attachedParts.add(new StreamerDTO((Streamer) rocketComponents)); + } else if (rocketComponents instanceof Parachute) { + attachedParts.add(new ParachuteDTO((Parachute) rocketComponents)); + } else if (rocketComponents instanceof MassObject) { + attachedParts.add(new MassObjectDTO((MassObject) rocketComponents)); + } else if (rocketComponents instanceof FreeformFinSet) { + attachedParts.add(new CustomFinSetDTO((FreeformFinSet) rocketComponents)); + } else if (rocketComponents instanceof FinSet) { + attachedParts.add(new FinSetDTO((FinSet) rocketComponents)); + } + + } + } + + public double getOd() { + return od; + } + + public void setOd(double theOd) { + od = theOd; + } + + public double getId() { + return id; + } + + public void setId(double theId) { + id = theId; + } + + public int getMotorMount() { + return isMotorMount; + } + + public void setMotorMount(boolean motorMount) { + if (motorMount) { + isMotorMount = 1; + } else { + isMotorMount = 0; + } + + } + + public void setMotorMount(int theMotorMount) { + isMotorMount = theMotorMount; + } + + public double getMotorDia() { + return motorDia; + } + + public void setMotorDia(double theMotorDia) { + motorDia = theMotorDia; + } + + public double getEngineOverhang() { + return engineOverhang; + } + + public void setEngineOverhang(double theEngineOverhang) { + engineOverhang = theEngineOverhang; + } + + public int getInsideTube() { + return isInsideTube; + } + + public void setInsideTube(boolean inside) { + if (inside) { + isInsideTube = 1; + } else { + isInsideTube = 0; + } + } + + public void setInsideTube(int theInsideTube) { + isInsideTube = theInsideTube; + } + + public List getAttachedParts() { + return attachedParts; + } + + public void addAttachedParts(BasePartDTO thePart) { + attachedParts.add(thePart); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/BulkheadDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/BulkheadDTO.java new file mode 100644 index 00000000..c5a82bfc --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/BulkheadDTO.java @@ -0,0 +1,18 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.rocketcomponent.Bulkhead; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "Ring") +@XmlAccessorType(XmlAccessType.FIELD) +public class BulkheadDTO extends CenteringRingDTO { + public BulkheadDTO(Bulkhead bh) { + super(bh); + setUsageCode(CenteringRingDTO.UsageCode.Bulkhead); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/CenteringRingDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/CenteringRingDTO.java new file mode 100644 index 00000000..e2e8a7c8 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/CenteringRingDTO.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.RadiusRingComponent; +import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlTransient; + +/** + */ +@XmlRootElement(name = "Ring") +@XmlAccessorType(XmlAccessType.FIELD) +public class CenteringRingDTO extends BasePartDTO { + + @XmlTransient + protected enum UsageCode { + //UsageCode + CenteringRing(0), + Bulkhead(1), + EngineBlock(2), + Sleeve(3), + TubeCoupler(4); + + int ordinal; + + UsageCode(int x) { + ordinal = x; + } + } + + @XmlElement(name = "OD") + private double od = 0d; + @XmlElement(name = "ID") + private double id = 0d; + @XmlElement(name = "UsageCode") + private int usageCode = UsageCode.CenteringRing.ordinal; + @XmlElement(name = "AutoSize") + private int autoSize = 0; + + public CenteringRingDTO() { + + } + public CenteringRingDTO(RadiusRingComponent cr) { + super(cr); + setId(cr.getInnerRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setOd(cr.getOuterRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + } + + public CenteringRingDTO(ThicknessRingComponent trc) { + super(trc); + setId(trc.getInnerRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setOd(trc.getOuterRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + } + public double getOd() { + return od; + } + + public void setOd(double theOd) { + od = theOd; + } + + public double getId() { + return id; + } + + public void setId(double theId) { + id = theId; + } + + public int getUsageCode() { + return usageCode; + } + + public void setUsageCode(int theUsageCode) { + usageCode = theUsageCode; + } + + public void setUsageCode(UsageCode theUsageCode) { + usageCode = theUsageCode.ordinal; + } + + public int getAutoSize() { + return autoSize; + } + + public void setAutoSize(int theAutoSize) { + autoSize = theAutoSize; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/CustomFinSetDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/CustomFinSetDTO.java new file mode 100644 index 00000000..99112c9d --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/CustomFinSetDTO.java @@ -0,0 +1,48 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.util.Coordinate; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "CustomFinSet") +@XmlAccessorType(XmlAccessType.FIELD) +public class CustomFinSetDTO extends FinSetDTO { + + @XmlElement(name = "PointList") + private String pointList = ""; + + public CustomFinSetDTO() { + } + + public CustomFinSetDTO(FreeformFinSet ec) { + super(ec); + setPointList(convertFreeFormPoints(ec.getFinPoints())); + } + + + private String convertFreeFormPoints(Coordinate[] points) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < points.length; i++) { + Coordinate point = points[i]; + sb.append(point.x * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH).append(",") + .append(point.y * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH).append("|"); + } + return sb.toString(); + } + + public String getPointList() { + return pointList; + } + + public void setPointList(String thePointList) { + pointList = thePointList; + } +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/export/EngineBlockDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/EngineBlockDTO.java new file mode 100644 index 00000000..bcb6d575 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/EngineBlockDTO.java @@ -0,0 +1,19 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.rocketcomponent.EngineBlock; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "Ring") +@XmlAccessorType(XmlAccessType.FIELD) +public class EngineBlockDTO extends CenteringRingDTO{ + + public EngineBlockDTO(EngineBlock eb) { + super(eb); + setUsageCode(UsageCode.EngineBlock); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java new file mode 100644 index 00000000..b97571c1 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java @@ -0,0 +1,191 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.TipShapeCode; +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "FinSet") +@XmlAccessorType(XmlAccessType.FIELD) +public class FinSetDTO extends BasePartDTO { + + @XmlElement(name = "FinCount") + private int finCount = 0; + @XmlElement(name = "RootChord") + private double rootChord = 0d; + @XmlElement(name = "TipChord") + private double tipChord = 0d; + @XmlElement(name = "SemiSpan") + private double semiSpan = 0d; + @XmlElement(name = "SweepDistance") + private double sweepDistance = 0d; + @XmlElement(name = "Thickness") + private double thickness = 0d; + @XmlElement(name = "ShapeCode") + private int shapeCode = 0; + @XmlElement(name = "TipShapeCode") + private int tipShapeCode = 0; + @XmlElement(name = "TabLength") + private double tabLength = 0d; + @XmlElement(name = "TabDepth") + private double tabDepth = 0d; + @XmlElement(name = "TabOffset") + private double tabOffset = 0d; + @XmlElement(name = "SweepMode") + private int sweepMode = 1; + @XmlElement(name = "CantAngle") + private double cantAngle = 0d; + + public FinSetDTO() { + } + + public FinSetDTO(FinSet ec) { + super(ec); + + setCantAngle(ec.getCantAngle()); + setFinCount(ec.getFinCount()); + setRootChord(ec.getLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setTabDepth(ec.getTabHeight() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setTabLength(ec.getTabLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setTabOffset(ec.getTabShift() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setThickness(ec.getThickness() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + + if (ec.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { + setXb(getXb() + getLen()); + } + + setRadialAngle(ec.getBaseRotation()); + setTipShapeCode(TipShapeCode.convertTipShapeCode(ec.getCrossSection())); + if (ec instanceof TrapezoidFinSet) { + TrapezoidFinSet tfs = (TrapezoidFinSet) ec; + setShapeCode(0); + setSemiSpan(tfs.getHeight() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setTipChord(tfs.getTipChord() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setSweepDistance(tfs.getSweep() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setLen(tfs.getLength()); + } + else if (ec instanceof EllipticalFinSet) { + EllipticalFinSet efs = (EllipticalFinSet) ec; + setShapeCode(1); + setSemiSpan(efs.getHeight() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setLen(efs.getLength() *RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + else if (ec instanceof FreeformFinSet) { + setShapeCode(2); + } + } + + public int getFinCount() { + return finCount; + } + + public void setFinCount(int theFinCount) { + finCount = theFinCount; + } + + public double getRootChord() { + return rootChord; + } + + public void setRootChord(double theRootChord) { + rootChord = theRootChord; + } + + public double getTipChord() { + return tipChord; + } + + public void setTipChord(double theTipChord) { + tipChord = theTipChord; + } + + public double getSemiSpan() { + return semiSpan; + } + + public void setSemiSpan(double theSemiSpan) { + semiSpan = theSemiSpan; + } + + public double getSweepDistance() { + return sweepDistance; + } + + public void setSweepDistance(double theSweepDistance) { + sweepDistance = theSweepDistance; + } + + public double getThickness() { + return thickness; + } + + public void setThickness(double theThickness) { + thickness = theThickness; + } + + public int getShapeCode() { + return shapeCode; + } + + public void setShapeCode(int theShapeCode) { + shapeCode = theShapeCode; + } + + public int getTipShapeCode() { + return tipShapeCode; + } + + public void setTipShapeCode(int theTipShapeCode) { + tipShapeCode = theTipShapeCode; + } + + public double getTabLength() { + return tabLength; + } + + public void setTabLength(double theTabLength) { + tabLength = theTabLength; + } + + public double getTabDepth() { + return tabDepth; + } + + public void setTabDepth(double theTabDepth) { + tabDepth = theTabDepth; + } + + public double getTabOffset() { + return tabOffset; + } + + public void setTabOffset(double theTabOffset) { + tabOffset = theTabOffset; + } + + public int getSweepMode() { + return sweepMode; + } + + public void setSweepMode(int theSweepMode) { + sweepMode = theSweepMode; + } + + public double getCantAngle() { + return cantAngle; + } + + public void setCantAngle(double theCantAngle) { + cantAngle = theCantAngle; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java new file mode 100644 index 00000000..c898abb2 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java @@ -0,0 +1,66 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TubeCoupler; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.List; + +/** + */ +@XmlRootElement(name = "BodyTube") +@XmlAccessorType(XmlAccessType.FIELD) +public class InnerBodyTubeDTO extends BodyTubeDTO { + + public InnerBodyTubeDTO() { + super.setInsideTube(true); + } + + public InnerBodyTubeDTO(InnerTube bt) { + super(bt); + setEngineOverhang(bt.getMotorOverhang() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setId(bt.getInnerRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setOd(bt.getOuterRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setMotorDia((bt.getMotorMountDiameter() / 2) * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setMotorMount(bt.isMotorMount()); + + List children = bt.getChildren(); + for (int i = 0; i < children.size(); i++) { + RocketComponent rocketComponents = children.get(i); + if (rocketComponents instanceof InnerTube) { + attachedParts.add(new InnerBodyTubeDTO((InnerTube) rocketComponents)); + } else if (rocketComponents instanceof BodyTube) { + attachedParts.add(new BodyTubeDTO((BodyTube) rocketComponents)); + } else if (rocketComponents instanceof Transition) { + attachedParts.add(new TransitionDTO((Transition) rocketComponents)); + } else if (rocketComponents instanceof EngineBlock) { + attachedParts.add(new EngineBlockDTO((EngineBlock) rocketComponents)); + } else if (rocketComponents instanceof TubeCoupler) { + attachedParts.add(new TubeCouplerDTO((TubeCoupler) rocketComponents)); + } else if (rocketComponents instanceof CenteringRing) { + attachedParts.add(new CenteringRingDTO((CenteringRing) rocketComponents)); + } else if (rocketComponents instanceof Bulkhead) { + attachedParts.add(new BulkheadDTO((Bulkhead) rocketComponents)); + } else if (rocketComponents instanceof Streamer) { + attachedParts.add(new StreamerDTO((Streamer) rocketComponents)); + } else if (rocketComponents instanceof Parachute) { + attachedParts.add(new ParachuteDTO((Parachute) rocketComponents)); + } else if (rocketComponents instanceof MassObject) { + attachedParts.add(new MassObjectDTO((MassObject) rocketComponents)); + } + } + setInsideTube(true); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/LaunchLugDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/LaunchLugDTO.java new file mode 100644 index 00000000..f2932c83 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/LaunchLugDTO.java @@ -0,0 +1,47 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.LaunchLug; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "LaunchLug") +@XmlAccessorType(XmlAccessType.FIELD) +public class LaunchLugDTO extends BasePartDTO { + + @XmlElement(name = "OD") + private double od = 0d; + @XmlElement(name = "ID") + private double id = 0d; + + public LaunchLugDTO() { + } + + public LaunchLugDTO(LaunchLug ec) { + super(ec); + setId(ec.getInnerRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setOd(ec.getOuterRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setRadialAngle(ec.getRadialDirection()); + } + + public double getOd() { + return od; + } + + public void setOd(double theOd) { + od = theOd; + } + + public double getId() { + return id; + } + + public void setId(double theId) { + id = theId; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/MassObjectDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/MassObjectDTO.java new file mode 100644 index 00000000..ea48844d --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/MassObjectDTO.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.rocketcomponent.MassObject; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "MassObject") +@XmlAccessorType(XmlAccessType.FIELD) +public class MassObjectDTO extends BasePartDTO{ + + @XmlElement(name = "TypeCode") + private int typeCode = 0; + + public MassObjectDTO() { + } + + public MassObjectDTO(MassObject ec) { + super(ec); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/NoseConeDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/NoseConeDTO.java new file mode 100644 index 00000000..4a154db9 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/NoseConeDTO.java @@ -0,0 +1,114 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TubeCoupler; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import javax.xml.bind.annotation.XmlElementWrapper; +import javax.xml.bind.annotation.XmlRootElement; +import java.util.ArrayList; +import java.util.List; + +/** + */ +@XmlRootElement(name = "NoseCone") +@XmlAccessorType(XmlAccessType.FIELD) +public class NoseConeDTO extends AbstractTransitionDTO { + + + @XmlElement(name = "BaseDia") + private double baseDia = 0d; + @XmlElement(name = "ShoulderLen") + private double shoulderLen = 0d; + @XmlElement(name = "ShoulderOD") + private double shoulderOD = 0d; + + @XmlElementWrapper(name = "AttachedParts") + @XmlElementRefs({ + @XmlElementRef(name = "BodyTube", type = BodyTubeDTO.class), + @XmlElementRef(name = "BodyTube", type = InnerBodyTubeDTO.class), + @XmlElementRef(name = "FinSet", type = FinSetDTO.class), + @XmlElementRef(name = "CustomFinSet", type = CustomFinSetDTO.class), + @XmlElementRef(name = "Ring", type = CenteringRingDTO.class), + @XmlElementRef(name = "Parachute", type = ParachuteDTO.class), + @XmlElementRef(name = "MassObject", type = MassObjectDTO.class)}) + List attachedParts = new ArrayList(); + + public NoseConeDTO() { + } + + public NoseConeDTO(NoseCone nc) { + super(nc); + setBaseDia(nc.getAftRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setShoulderLen(nc.getAftShoulderLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setShoulderOD(nc.getAftShoulderRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + + List children = nc.getChildren(); + for (int i = 0; i < children.size(); i++) { + RocketComponent rocketComponents = children.get(i); + if (rocketComponents instanceof InnerTube) { + attachedParts.add(new InnerBodyTubeDTO((InnerTube) rocketComponents)); + } else if (rocketComponents instanceof BodyTube) { + attachedParts.add(new BodyTubeDTO((BodyTube) rocketComponents)); + } else if (rocketComponents instanceof Transition) { + attachedParts.add(new TransitionDTO((Transition) rocketComponents)); + } else if (rocketComponents instanceof EngineBlock) { + attachedParts.add(new EngineBlockDTO((EngineBlock) rocketComponents)); + } else if (rocketComponents instanceof TubeCoupler) { + attachedParts.add(new TubeCouplerDTO((TubeCoupler) rocketComponents)); + } else if (rocketComponents instanceof CenteringRing) { + attachedParts.add(new CenteringRingDTO((CenteringRing) rocketComponents)); + } else if (rocketComponents instanceof Bulkhead) { + attachedParts.add(new BulkheadDTO((Bulkhead) rocketComponents)); + } else if (rocketComponents instanceof Parachute) { + attachedParts.add(new ParachuteDTO((Parachute) rocketComponents)); + } else if (rocketComponents instanceof MassObject) { + attachedParts.add(new MassObjectDTO((MassObject) rocketComponents)); + } else if (rocketComponents instanceof FreeformFinSet) { + attachedParts.add(new CustomFinSetDTO((FreeformFinSet) rocketComponents)); + } else if (rocketComponents instanceof FinSet) { + attachedParts.add(new FinSetDTO((FinSet) rocketComponents)); + } + } + } + + public double getBaseDia() { + return baseDia; + } + + public void setBaseDia(double theBaseDia) { + baseDia = theBaseDia; + } + + public double getShoulderLen() { + return shoulderLen; + } + + public void setShoulderLen(double theShoulderLen) { + shoulderLen = theShoulderLen; + } + + public double getShoulderOD() { + return shoulderOD; + } + + public void setShoulderOD(double theShoulderOD) { + shoulderOD = theShoulderOD; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/ParachuteDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/ParachuteDTO.java new file mode 100644 index 00000000..eb612227 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/ParachuteDTO.java @@ -0,0 +1,129 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.BaseHandler; +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.Parachute; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "Parachute") +@XmlAccessorType(XmlAccessType.FIELD) +public class ParachuteDTO extends BasePartDTO { + + @XmlElement(name = "Dia") + private double dia = 0d; + @XmlElement(name = "SpillHoleDia") + private double spillHoleDia = 0d; + @XmlElement(name = "ShroudLineCount") + private int ShroudLineCount = 0; + @XmlElement(name = "Thickness") + private double thickness = 0d; + @XmlElement(name = "ShroudLineLen") + private double shroudLineLen = 0d; + @XmlElement(name = "ChuteCount") + private int chuteCount = 1; + @XmlElement(name = "ShroudLineMassPerMM") + private double shroudLineMassPerMM = 0d; + @XmlElement(name = "ShroudLineMaterial") + private String shroudLineMaterial = ""; + @XmlElement(name = "DragCoefficient") + private double dragCoefficient = 0.75d; + + public ParachuteDTO() { + } + + public ParachuteDTO(Parachute ec) { + super(ec); + + setChuteCount(1); + setDia(ec.getDiameter() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setDragCoefficient(ec.getCD()); + setShroudLineCount(ec.getLineCount()); + setShroudLineLen(ec.getLineLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + + String material = ec.getLineMaterial().getName(); + setShroudLineMassPerMM(ec.getLineMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LINE_DENSITY); + + if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { + material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); + } + setShroudLineMaterial(material); + } + + public double getDia() { + return dia; + } + + public void setDia(double theDia) { + dia = theDia; + } + + public double getSpillHoleDia() { + return spillHoleDia; + } + + public void setSpillHoleDia(double theSpillHoleDia) { + spillHoleDia = theSpillHoleDia; + } + + public int getShroudLineCount() { + return ShroudLineCount; + } + + public void setShroudLineCount(int theShroudLineCount) { + ShroudLineCount = theShroudLineCount; + } + + public double getThickness() { + return thickness; + } + + public void setThickness(double theThickness) { + thickness = theThickness; + } + + public double getShroudLineLen() { + return shroudLineLen; + } + + public void setShroudLineLen(double theShroudLineLen) { + shroudLineLen = theShroudLineLen; + } + + public int getChuteCount() { + return chuteCount; + } + + public void setChuteCount(int theChuteCount) { + chuteCount = theChuteCount; + } + + public double getShroudLineMassPerMM() { + return shroudLineMassPerMM; + } + + public void setShroudLineMassPerMM(double theShroudLineMassPerMM) { + shroudLineMassPerMM = theShroudLineMassPerMM; + } + + public String getShroudLineMaterial() { + return shroudLineMaterial; + } + + public void setShroudLineMaterial(String theShroudLineMaterial) { + shroudLineMaterial = theShroudLineMaterial; + } + + public double getDragCoefficient() { + return dragCoefficient; + } + + public void setDragCoefficient(double theDragCoefficient) { + dragCoefficient = theDragCoefficient; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/RocketDesignDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/RocketDesignDTO.java new file mode 100644 index 00000000..9abc4464 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/RocketDesignDTO.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.file.rocksim.export; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + */ +@XmlAccessorType(XmlAccessType.FIELD) +public class RocketDesignDTO { + + @XmlElement(name = "Name") + private String name; + @XmlElement(name = "StageCount") + private int stageCount = 1; + @XmlElement(name = "ViewType") + private int viewType = 0; + @XmlElement(name = "ViewStageCount") + private int viewStageCount = 3; + @XmlElement(name = "ViewTypeEdit") + private int viewTypeEdit = 0; + @XmlElement(name = "ViewStageCountEdit") + private int viewStageCountEdit = 3; + @XmlElement(name = "ZoomFactor") + private double zoomFactor = 0d; + @XmlElement (name = "ZoomFactorEdit") + private double zoomFactorEdit = 0d; + @XmlElement(name = "ScrollPosX") + private int scrollPosX = 0; + @XmlElement(name = "ScrollPosY") + private int scrollPosY = 0; + @XmlElement(name = "ScrollPosXEdit") + private int scrollPosXEdit = 0; + @XmlElement(name = "ScrollPosYEdit") + private int scrollPosYEdit = 0; + @XmlElement(name = "ThreeDFlags") + private int threeDFlags = 0; + @XmlElement(name = "ThreeDFlagsEdit") + private int threeDFlagsEdit = 0; + + @XmlElement(name = "CPCalcFlags") + private String cpCalcFlags = "1"; + @XmlElement(name = "UseKnownMass") + private String useKnownMass = "0"; + @XmlElement(name = "Stage3Parts") + private StageDTO stage3 = new StageDTO(); + @XmlElement(name = "Stage2Parts", required = true, nillable = false) + private StageDTO stage2 = new StageDTO(); + @XmlElement(name = "Stage1Parts", required = false, nillable = false) + private StageDTO stage1 = new StageDTO(); + + public RocketDesignDTO() { + } + + public String getName() { + return name; + } + + public void setName(String theName) { + name = theName; + } + + public int getStageCount() { + return stageCount; + } + + public void setStageCount(int theStageCount) { + stageCount = theStageCount; + } + + public StageDTO getStage3() { + return stage3; + } + + public void setStage3(StageDTO theStage3) { + stage3 = theStage3; + } + + public StageDTO getStage2() { + return stage2; + } + + public void setStage2(StageDTO theStage2) { + stage2 = theStage2; + } + + public StageDTO getStage1() { + return stage1; + } + + public void setStage1(StageDTO theStage1) { + stage1 = theStage1; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/RocksimDesignDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/RocksimDesignDTO.java new file mode 100644 index 00000000..56129250 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/RocksimDesignDTO.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.file.rocksim.export; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; + +/** + */ +@XmlAccessorType(XmlAccessType.FIELD) +public class RocksimDesignDTO { + + @XmlElement(name = "RocketDesign") + private RocketDesignDTO design; + + public RocksimDesignDTO() { + } + + public RocketDesignDTO getDesign() { + return design; + } + + public void setDesign(RocketDesignDTO theDesign) { + design = theDesign; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/RocksimDocumentDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/RocksimDocumentDTO.java new file mode 100644 index 00000000..681f4086 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/RocksimDocumentDTO.java @@ -0,0 +1,32 @@ +package net.sf.openrocket.file.rocksim.export; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement(name = "RockSimDocument") +@XmlAccessorType(XmlAccessType.FIELD) +public class RocksimDocumentDTO { + + @XmlElement(name = "FileVersion") + private final String version = "4"; + + @XmlElement(name = "DesignInformation") + private RocksimDesignDTO design; + + public RocksimDocumentDTO() { + } + + public RocksimDesignDTO getDesign() { + return design; + } + + public void setDesign(RocksimDesignDTO theDesign) { + this.design = theDesign; + } + + public String getVersion() { + return version; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java b/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java new file mode 100644 index 00000000..39500625 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java @@ -0,0 +1,122 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.StorageOptions; +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.startup.Application; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.util.List; + +/** + */ +public class RocksimSaver extends RocketSaver { + + private static final LogHelper log = Application.getLogger(); + + public String marshalToRocksim(OpenRocketDocument doc) { + + try { + JAXBContext binder = JAXBContext.newInstance(RocksimDocumentDTO.class); + Marshaller marshaller = binder.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + StringWriter sw = new StringWriter(); + + marshaller.marshal(toRocksimDocumentDTO(doc), sw); + return sw.toString(); + } catch (Exception e) { + e.printStackTrace(); + } + + return null; + } + + @Override + public void save(OutputStream dest, OpenRocketDocument doc, StorageOptions options) throws IOException { + log.info("Saving .rkt file"); + + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(dest, "UTF-8")); + writer.write(marshalToRocksim(doc)); + writer.flush(); + writer.close(); + } + + @Override + public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) { + return marshalToRocksim(doc).length(); + } + + private RocksimDocumentDTO toRocksimDocumentDTO(OpenRocketDocument doc) { + RocksimDocumentDTO rsd = new RocksimDocumentDTO(); + + rsd.setDesign(toRocksimDesignDTO(doc.getRocket())); + + return rsd; + } + + private RocksimDesignDTO toRocksimDesignDTO(Rocket rocket) { + RocksimDesignDTO result = new RocksimDesignDTO(); + result.setDesign(toRocketDesignDTO(rocket)); + return result; + } + + private RocketDesignDTO toRocketDesignDTO(Rocket rocket) { + RocketDesignDTO result = new RocketDesignDTO(); + result.setName(rocket.getName()); + int stageCount = rocket.getStageCount(); + result.setStageCount(stageCount); + if (stageCount > 0) { + result.setStage3(toStageDTO(rocket.getChild(0).getStage())); + } + if (stageCount > 1) { + result.setStage2(toStageDTO(rocket.getChild(1).getStage())); + } + if (stageCount > 2) { + result.setStage1(toStageDTO(rocket.getChild(2).getStage())); + } + return result; + } + + private StageDTO toStageDTO(Stage stage) { + StageDTO result = new StageDTO(); + + List children = stage.getChildren(); + for (int i = 0; i < children.size(); i++) { + RocketComponent rocketComponents = children.get(i); + if (rocketComponents instanceof NoseCone) { + result.addExternalPart(toNoseConeDTO((NoseCone) rocketComponents)); + } else if (rocketComponents instanceof BodyTube) { + result.addExternalPart(toBodyTubeDTO((BodyTube) rocketComponents)); + } else if (rocketComponents instanceof Transition) { + result.addExternalPart(toTransitionDTO((Transition) rocketComponents)); + } + } + return result; + } + + private NoseConeDTO toNoseConeDTO(NoseCone nc) { + return new NoseConeDTO(nc); + } + + private BodyTubeDTO toBodyTubeDTO(BodyTube bt) { + return new BodyTubeDTO(bt); + } + + private TransitionDTO toTransitionDTO(Transition tran) { + return new TransitionDTO(tran); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java new file mode 100644 index 00000000..112be2c6 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/StageDTO.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.util.ArrayList; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElementRef; +import javax.xml.bind.annotation.XmlElementRefs; +import java.util.List; + +/** + */ +@XmlAccessorType(XmlAccessType.FIELD) +public class StageDTO { + + @XmlElementRefs({ + @XmlElementRef(name = "BodyTube", type = BodyTubeDTO.class), + @XmlElementRef(name = "NoseCone", type = NoseConeDTO.class), + @XmlElementRef(name = "Transition", type = TransitionDTO.class) + }) + private List externalPart = new ArrayList(); + + public StageDTO() { + } + + public List getExternalPart() { + return externalPart; + } + + public void addExternalPart(BasePartDTO theExternalPartDTO) { + externalPart.add(theExternalPartDTO); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/StreamerDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/StreamerDTO.java new file mode 100644 index 00000000..cb687666 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/StreamerDTO.java @@ -0,0 +1,46 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.Streamer; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "Streamer") +@XmlAccessorType(XmlAccessType.FIELD) +public class StreamerDTO extends BasePartDTO { + + @XmlElement(name = "Width") + private double width = 0d; + @XmlElement(name = "DragCoefficient") + private double dragCoefficient = 0.75d; + + public StreamerDTO() { + } + + public StreamerDTO(Streamer ec) { + super(ec); + setWidth(ec.getStripWidth() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setDragCoefficient(ec.getCD()); + } + + public double getWidth() { + return width; + } + + public void setWidth(double theWidth) { + width = theWidth; + } + + public double getDragCoefficient() { + return dragCoefficient; + } + + public void setDragCoefficient(double theDragCoefficient) { + dragCoefficient = theDragCoefficient; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/TransitionDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/TransitionDTO.java new file mode 100644 index 00000000..5d668303 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/TransitionDTO.java @@ -0,0 +1,92 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.file.rocksim.importt.RocksimHandler; +import net.sf.openrocket.rocketcomponent.Transition; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "Transition") +@XmlAccessorType(XmlAccessType.FIELD) +public class TransitionDTO extends AbstractTransitionDTO { + + + @XmlElement(name = "FrontShoulderLen") + private double frontShoulderLen = 0d; + @XmlElement(name = "RearShoulderLen") + private double rearShoulderLen = 0d; + @XmlElement(name = "FrontShoulderDia") + private double frontShoulderDia = 0d; + @XmlElement(name = "RearShoulderDia") + private double rearShoulderDia = 0d; + @XmlElement(name = "FrontDia") + private double frontDia = 0d; + @XmlElement(name = "RearDia") + private double rearDia = 0d; + + public TransitionDTO() { + } + + public TransitionDTO(Transition tran) { + super(tran); + setFrontDia(tran.getForeRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setRearDia(tran.getAftRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setFrontShoulderDia(tran.getForeShoulderRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setFrontShoulderLen(tran.getForeShoulderLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + setRearShoulderDia(tran.getAftShoulderRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + setRearShoulderLen(tran.getAftShoulderLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + + + } + public double getFrontShoulderLen() { + return frontShoulderLen; + } + + public void setFrontShoulderLen(double theFrontShoulderLen) { + frontShoulderLen = theFrontShoulderLen; + } + + public double getRearShoulderLen() { + return rearShoulderLen; + } + + public void setRearShoulderLen(double theRearShoulderLen) { + rearShoulderLen = theRearShoulderLen; + } + + public double getFrontShoulderDia() { + return frontShoulderDia; + } + + public void setFrontShoulderDia(double theFrontShoulderDia) { + frontShoulderDia = theFrontShoulderDia; + } + + public double getRearShoulderDia() { + return rearShoulderDia; + } + + public void setRearShoulderDia(double theRearShoulderDia) { + rearShoulderDia = theRearShoulderDia; + } + + public double getFrontDia() { + return frontDia; + } + + public void setFrontDia(double theFrontDia) { + frontDia = theFrontDia; + } + + public double getRearDia() { + return rearDia; + } + + public void setRearDia(double theRearDia) { + rearDia = theRearDia; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/export/TubeCouplerDTO.java b/core/src/net/sf/openrocket/file/rocksim/export/TubeCouplerDTO.java new file mode 100644 index 00000000..52d995e3 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/export/TubeCouplerDTO.java @@ -0,0 +1,19 @@ +package net.sf.openrocket.file.rocksim.export; + +import net.sf.openrocket.rocketcomponent.TubeCoupler; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +/** + */ +@XmlRootElement(name = "Ring") +@XmlAccessorType(XmlAccessType.FIELD) +public class TubeCouplerDTO extends CenteringRingDTO { + + public TubeCouplerDTO(TubeCoupler tc) { + super(tc); + setUsageCode(UsageCode.TubeCoupler); + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/AttachedPartsHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/AttachedPartsHandler.java new file mode 100644 index 00000000..4543c519 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/AttachedPartsHandler.java @@ -0,0 +1,74 @@ +/* + * AttachedPartsHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.rocketcomponent.RocketComponent; + +import java.util.HashMap; + +/** + * A SAX handler for the Rocksim AttachedParts XML type. + */ +class AttachedPartsHandler extends ElementHandler { + /** The parent component. */ + private final RocketComponent component; + + /** + * Constructor. + * + * @param c the parent + * + * @throws IllegalArgumentException thrown if c is null + */ + public AttachedPartsHandler(RocketComponent c) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of any attached part may not be null."); + } + component = c; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + if ("FinSet".equals(element)) { + return new FinSetHandler(component); + } + if ("CustomFinSet".equals(element)) { + return new FinSetHandler(component); + } + if ("LaunchLug".equals(element)) { + return new LaunchLugHandler(component, warnings); + } + if ("Parachute".equals(element)) { + return new ParachuteHandler(component, warnings); + } + if ("Streamer".equals(element)) { + return new StreamerHandler(component, warnings); + } + if ("MassObject".equals(element)) { + return new MassObjectHandler(component, warnings); + } + if ("Ring".equals(element)) { + return new RingHandler(component, warnings); + } + if ("BodyTube".equals(element)) { + return new InnerBodyTubeHandler(component, warnings); + } + if ("Transition".equals(element)) { + return new TransitionHandler(component, warnings); + } + if ("TubeFinSet".equals(element)) { + warnings.add("Tube fins are not currently supported. Ignoring."); + } + if ("RingTail".equals(element)) { + warnings.add("Ring tails are not currently supported. Ignoring."); + } + if ("ExternalPod".equals(element)) { + warnings.add("Pods are not currently supported. Ignoring."); + } + return null; + } +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/BaseHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/BaseHandler.java new file mode 100644 index 00000000..59fb05a3 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/BaseHandler.java @@ -0,0 +1,260 @@ +/* + * BaseHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; + +/** + * An abstract base class that handles common parsing. All Rocksim component handlers are subclassed from here. + * + * @param the specific RocketComponent subtype for which the concrete handler can create + */ +public abstract class BaseHandler extends ElementHandler { + + /** + * Prepend rocksim materials. + */ + public static final String ROCKSIM_MATERIAL_PREFIX = "RS: "; + /** + * The overridden mass. + */ + private Double mass = 0d; + /** + * The overridden Cg. + */ + private Double cg = 0d; + /** + * The density of the material in the component. + */ + private Double density = 0d; + /** + * The internal Rocksim density type. + */ + private RocksimDensityType densityType = RocksimDensityType.ROCKSIM_BULK; + + /** + * The material name. + */ + private String materialName = ""; + + /** + * The SAX method called when the closing element tag is reached. + * + * @param element the element name. + * @param attributes attributes of the element. + * @param content the textual content of the element. + * @param warnings the warning set to store warnings in. + * @throws SAXException + */ + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + final C component = getComponent(); + try { + if ("Name".equals(element)) { + component.setName(content); + } + if ("KnownMass".equals(element)) { + mass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + } + if ("Density".equals(element)) { + density = Math.max(0d, Double.parseDouble(content) ); + } + if ("KnownCG".equals(element)) { + cg = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("UseKnownCG".equals(element)) { //Rocksim sets UseKnownCG to true to control the override of both cg and mass + boolean override = "1".equals(content); + setOverride(component, override, mass, cg); + } + if ("DensityType".equals(element)) { + densityType = RocksimDensityType.fromCode(Integer.parseInt(content)); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void endHandler(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + /* Because of the order of XML elements in Rocksim, not all information is known at the time it really needs + to be acted upon. So we keep temporary instance variables to be used here at the end of the parsing. + */ + density = computeDensity(densityType, density); + RocketComponent component = getComponent(); + updateComponentMaterial(component, materialName, getMaterialType(), density); + } + + /** + * Compute the density. Rocksim does strange things with densities. For some streamer material it's in cubic, + * rather than square, units. In those cases it needs to be converted to an appropriate SURFACE material density. + * Some G10 fiberglass materials are in cubic units, other G10 fiberglass is in square units. And due to a + * Rocksim bug, some densities are 0 when they clearly should not be. + * + * This may be overridden for specific component density computations. + * + * @param type the rocksim density + * @param rawDensity the density as specified in the Rocksim design file + * @return a value in OpenRocket SURFACE density units + */ + protected double computeDensity(RocksimDensityType type, double rawDensity) { + return rawDensity / type.asOpenRocket(); + } + + /** + * If the Rocksim component does not override the mass, then create a Material based upon the density defined + * for that component. This *should* result in a consistent representation of Cg between Rocksim and OpenRocket. + * + * @param component the component + * @param type the type of the material + * @param density the density in g/cm^3 + * @param definedMaterial the material that is currently defined on the component; used only to get the name + * as it appears in Rocksim + */ + public static void updateComponentMaterial(RocketComponent component, String definedMaterial, Material.Type type, + double density) { + if (definedMaterial != null) { + Material custom = createCustomMaterial(type, definedMaterial, density); + setMaterial(component, custom); + } + } + + /** + * Override the mass and Cg of the component. + * + * @param component the component + * @param override true if any override should happen + * @param mass the override mass + * @param cg the override cg + */ + public static void setOverride(RocketComponent component, boolean override, double mass, double cg) { + if (override) { + component.setCGOverridden(override); + component.setMassOverridden(override); + component.setOverrideSubcomponents(false); //Rocksim does not support this type of override + component.setOverrideMass(mass); + component.setOverrideCGX(cg); + } + } + + /** + * Get the component this handler is working upon. + * + * @return a component + */ + protected abstract C getComponent(); + + /** + * Get the required type of material for this component. + * + * @return the required material type + */ + protected abstract Material.Type getMaterialType(); + + /** + * Some CG positions in Rocksim do not correspond to the CG position reference in OpenRocket. + * + * @param theCG the CG value to really use when overriding CG on the OpenRocket component + */ + protected void setCG(double theCG) { + cg = theCG; + } + + /** + * Set the material name as specified in the Rocksim design file. + * + * @param content the material name + */ + protected void setMaterialName(String content) { + materialName = content; + } + + /** + * Add child to parent only if the child is compatible. Otherwise add to warning set. + * + * @param parent the parent component + * @param child the child component + * @param warnings the warning set + * + * @return true if the child is compatible with parent + */ + protected static boolean isCompatible(RocketComponent parent, Class child, WarningSet warnings) { + if (!parent.isCompatible(child)) { + warnings.add(child.getName() + " can not be attached to " + + parent.getComponentName() + ", ignoring component."); + return false; + } + else { + return true; + } + } + + /** + * Create a custom material based on the density. The name of the material is prepended with 'RS: ' to + * indicate it came from a RockSim material. + * + * @param type the type of the material + * @param name the name of the component + * @param density the density + * + * @return a Material instance + */ + public static Material createCustomMaterial(Material.Type type, String name, double density) { + return Material.newMaterial(type, ROCKSIM_MATERIAL_PREFIX + name, density, true); + } + + /** + * Set the material onto an instance of RocketComponent. This is done because only some subtypes of RocketComponent + * have the setMaterial method. Unfortunately the supertype cannot be used. + * + * @param component the component who's material is to be set + * @param material the material to be set on the component (defined by getComponent()) + */ + private static void setMaterial(RocketComponent component, Material material) { + try { + final Method method = getMethod(component, "setMaterial", new Class[]{Material.class}); + if (method != null) { + method.invoke(component, material); + } + } + catch (IllegalAccessException ignored) { + } + catch (InvocationTargetException ignored) { + } + } + + /** + * Find a method by name and argument list. + * + * @param component the component who's material is to be seta + * @param name the method name + * @param args the class types of the parameters + * + * @return the Method instance, or null + */ + private static Method getMethod(RocketComponent component, String name, Class[] args) { + Method method = null; + try { + method = component.getClass().getMethod(name, args); + } + catch (NoSuchMethodException ignored) { + } + return method; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/BodyTubeHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/BodyTubeHandler.java new file mode 100644 index 00000000..f2acb4cb --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/BodyTubeHandler.java @@ -0,0 +1,103 @@ +/* + * BodyTubeHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A SAX handler for Rocksim Body Tubes. + */ +class BodyTubeHandler extends BaseHandler { + /** + * The OpenRocket BodyTube. + */ + private final BodyTube bodyTube; + + /** + * Constructor. + * + * @param c parent component + * @param warnings the warning set + * @throws IllegalArgumentException thrown if c is null + */ + public BodyTubeHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of a body tube may not be null."); + } + bodyTube = new BodyTube(); + if (isCompatible(c, BodyTube.class, warnings)) { + c.addChild(bodyTube); + } + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + if ("AttachedParts".equals(element)) { + return new AttachedPartsHandler(bodyTube); + } + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("OD".equals(element)) { + bodyTube.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + } + if ("ID".equals(element)) { + final double r = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS; + bodyTube.setInnerRadius(r); + } + if ("Len".equals(element)) { + bodyTube.setLength(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("FinishCode".equals(element)) { + bodyTube.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); + } + if ("IsMotorMount".equals(element)) { + bodyTube.setMotorMount("1".equals(content)); + } + if ("EngineOverhang".equals(element)) { + bodyTube.setMotorOverhang(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * Get the component this handler is working upon. + * + * @return a component + */ + @Override + public BodyTube getComponent() { + return bodyTube; + } + + /** + * Get the required type of material for this component. + * + * @return BULK + */ + public Material.Type getMaterialType() { + return Material.Type.BULK; + } +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java new file mode 100644 index 00000000..1ddb5677 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java @@ -0,0 +1,393 @@ +/* + * FinSetHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.IllegalFinPointException; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.util.Coordinate; +import org.xml.sax.SAXException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +/** + * A SAX handler for Rocksim fin sets. Because the type of fin may not be known first (in Rocksim file format, the fin + * shape type is in the middle of the XML structure), and because we're using SAX not DOM, all of the fin + * characteristics are kept here until the closing FinSet tag. At that point, asOpenRocket method is called + * to construct the corresponding OpenRocket FinSet. + */ +class FinSetHandler extends ElementHandler { + /** + * The parent component. + */ + private final RocketComponent component; + + /** + * The name of the fin. + */ + private String name; + /** + * The Rocksim fin shape code. + */ + private int shapeCode; + /** + * The location of the fin on its parent. + */ + private double location = 0.0d; + /** + * The OpenRocket Position which gives the absolute/relative positioning for location. + */ + private RocketComponent.Position position; + /** + * The number of fins in this fin set. + */ + private int finCount; + /** + * The length of the root chord. + */ + private double rootChord = 0.0d; + /** + * The length of the tip chord. + */ + private double tipChord = 0.0d; + /** + * The length of the mid-chord (aka height). + */ + private double midChordLen = 0.0d; + /** + * The distance of the leading edge from root to top. + */ + private double sweepDistance = 0.0d; + /** + * The angle the fins have been rotated from the y-axis, if looking down the tube, in radians. + */ + private double radialAngle = 0.0d; + /** + * The thickness of the fins. + */ + private double thickness; + /** + * The finish of the fins. + */ + private ExternalComponent.Finish finish; + /** + * The shape of the tip. + */ + private int tipShapeCode; + /** + * The length of the TTW tab. + */ + private double tabLength = 0.0d; + /** + * The depth of the TTW tab. + */ + private double tabDepth = 0.0d; + /** + * The offset of the tab, from the front of the fin. + */ + private double taboffset = 0.0d; + /** + * The elliptical semi-span (height). + */ + private double semiSpan; + /** + * The list of custom points. + */ + private String pointList; + /** + * Override the Cg and mass. + */ + private boolean override = false; + /** + * The overridden mass. + */ + private Double mass = 0d; + /** + * The overridden Cg. + */ + private Double cg = 0d; + /** + * The density of the material in the component. + */ + private Double density = 0d; + /** + * The material name. + */ + private String materialName = ""; + /** + * The Rocksim calculated mass. + */ + private Double calcMass = 0d; + /** + * The Rocksim calculated cg. + */ + private Double calcCg = 0d; + + + /** + * Constructor. + * + * @param c the parent + * + * @throws IllegalArgumentException thrown if c is null + */ + public FinSetHandler (RocketComponent c) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of a fin set may not be null."); + } + component = c; + } + + @Override + public ElementHandler openElement (String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement (String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + try { + if ("Name".equals(element)) { + name = content; + } + if ("Material".equals(element)) { + materialName = content; + } + if ("FinishCode".equals(element)) { + finish = RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket(); + } + if ("Xb".equals(element)) { + location = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("LocationMode".equals(element)) { + position = RocksimLocationMode.fromCode(Integer.parseInt(content)).asOpenRocket(); + } + if ("FinCount".equals(element)) { + finCount = Integer.parseInt(content); + } + if ("RootChord".equals(element)) { + rootChord = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("TipChord".equals(element)) { + tipChord = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("SemiSpan".equals(element)) { + semiSpan = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("MidChordLen".equals(element)) { + midChordLen = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("SweepDistance".equals(element)) { + sweepDistance = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("Thickness".equals(element)) { + thickness = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("TipShapeCode".equals(element)) { + tipShapeCode = Integer.parseInt(content); + } + if ("TabLength".equals(element)) { + tabLength = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("TabDepth".equals(element)) { + tabDepth = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("TabOffset".equals(element)) { + taboffset = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("RadialAngle".equals(element)) { + radialAngle = Double.parseDouble(content); + } + if ("ShapeCode".equals(element)) { + shapeCode = Integer.parseInt(content); + } + if ("PointList".equals(element)) { + pointList = content; + } + if ("KnownMass".equals(element)) { + mass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + } + if ("Density".equals(element)) { + density = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY); + } + if ("KnownCG".equals(element)) { + cg = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + } + if ("UseKnownCG".equals(element)) { + override = "1".equals(content); + } + if ("CalcMass".equals(element)) { + calcMass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; + } + if ("CalcCg".equals(element)) { + calcCg = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + @Override + public void endHandler (String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + //Create the fin set and correct for overrides and actual material densities + final FinSet finSet = asOpenRocket(warnings); + if (component.isCompatible(finSet)) { + BaseHandler.setOverride(finSet, override, mass, cg); + if (!override && finSet.getCrossSection().equals(FinSet.CrossSection.AIRFOIL)) { + //Override mass anyway. This is done only for AIRFOIL because Rocksim does not compute different + //mass/cg for different cross sections, but OpenRocket does. This can lead to drastic differences + //in mass. To counteract that, the cross section value is retained but the mass/cg is overridden + //with the calculated values from Rocksim. This will best approximate the Rocksim design in OpenRocket. + BaseHandler.setOverride(finSet, true, calcMass, calcCg); + } + BaseHandler.updateComponentMaterial(finSet, materialName, Material.Type.BULK, density); + component.addChild(finSet); + } + else { + warnings.add(finSet.getComponentName() + " can not be attached to " + + component.getComponentName() + ", ignoring component."); + } + } + + + /** + * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's FinSet. + * + * @param warnings the warning set to convey incompatibilities to the user + * + * @return a FinSet instance + */ + public FinSet asOpenRocket (WarningSet warnings) { + FinSet result; + + if (shapeCode == 0) { + //Trapezoidal + result = new TrapezoidFinSet(); + ((TrapezoidFinSet) result).setFinShape(rootChord, tipChord, sweepDistance, semiSpan, thickness); + } + else if (shapeCode == 1) { + //Elliptical + result = new EllipticalFinSet(); + ((EllipticalFinSet) result).setHeight(semiSpan); + ((EllipticalFinSet) result).setLength(rootChord); + } + else if (shapeCode == 2) { + + result = new FreeformFinSet(); + try { + ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings)); + } + catch (IllegalFinPointException e) { + warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring."); + } + } + else { + return null; + } + result.setThickness(thickness); + result.setName(name); + result.setFinCount(finCount); + result.setFinish(finish); + //All TTW tabs in Rocksim are relative to the front of the fin. + result.setTabRelativePosition(FinSet.TabRelativePosition.FRONT); + result.setTabHeight(tabDepth); + result.setTabLength(tabLength); + result.setTabShift(taboffset); + result.setBaseRotation(radialAngle); + result.setCrossSection(convertTipShapeCode(tipShapeCode)); + result.setRelativePosition(position); + PositionDependentHandler.setLocation(result, position, location); + return result; + + } + + /** + * Convert a Rocksim string that represents fin plan points into an array of OpenRocket coordinates. + * + * @param pointList a comma and pipe delimited string of X,Y coordinates from Rocksim. This is of the format: + *

x0,y0|x1,y1|x2,y2|... 
+ * @param warnings the warning set to convey incompatibilities to the user + * + * @return an array of OpenRocket Coordinates + */ + private Coordinate[] toCoordinates (String pointList, WarningSet warnings) { + List result = new ArrayList(); + if (pointList != null && !pointList.isEmpty()) { + String[] points = pointList.split("\\Q|\\E"); + for (String point : points) { + String[] aPoint = point.split(","); + try { + if (aPoint.length > 1) { + Coordinate c = new Coordinate( + Double.parseDouble(aPoint[0]) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH, + Double.parseDouble(aPoint[1]) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + result.add(c); + } + else { + warnings.add("Invalid fin point pair."); + } + } + catch (NumberFormatException nfe) { + warnings.add("Fin point not in numeric format."); + } + } + if (!result.isEmpty()) { + //OpenRocket requires fin plan points be ordered from leading root chord to trailing root chord in the + //Coordinate array. + Coordinate last = result.get(result.size() - 1); + if (last.x == 0 && last.y == 0) { + Collections.reverse(result); + } + } + } + final Coordinate[] coords = new Coordinate[result.size()]; + return result.toArray(coords); + } + + + /** + * Convert a Rocksim tip shape to an OpenRocket CrossSection. + * + * @param tipShape the tip shape code from Rocksim + * + * @return a CrossSection instance + */ + public static FinSet.CrossSection convertTipShapeCode (int tipShape) { + switch (tipShape) { + case 0: + return FinSet.CrossSection.SQUARE; + case 1: + return FinSet.CrossSection.ROUNDED; + case 2: + return FinSet.CrossSection.AIRFOIL; + default: + return FinSet.CrossSection.SQUARE; + } + } + + public static int convertTipShapeCode (FinSet.CrossSection cs) { + if (FinSet.CrossSection.ROUNDED.equals(cs)) { + return 1; + } + if (FinSet.CrossSection.AIRFOIL.equals(cs)) { + return 2; + } + return 0; + } + +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/InnerBodyTubeHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/InnerBodyTubeHandler.java new file mode 100644 index 00000000..edcf83f9 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/InnerBodyTubeHandler.java @@ -0,0 +1,113 @@ +/* + * InnerBodyTubeHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A SAX handler for Rocksim inside tubes. + */ +class InnerBodyTubeHandler extends PositionDependentHandler { + + /** + * The OpenRocket InnerTube instance. + */ + private final InnerTube bodyTube; + + /** + * Constructor. + * + * @param c the parent component + * @param warnings the warning set + * @throws IllegalArgumentException thrown if c is null + */ + public InnerBodyTubeHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of an inner tube may not be null."); + } + bodyTube = new InnerTube(); + if (isCompatible(c, InnerTube.class, warnings)) { + c.addChild(bodyTube); + } + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + if ("AttachedParts".equals(element)) { + return new AttachedPartsHandler(bodyTube); + } + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("OD".equals(element)) { + bodyTube.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + } + if ("ID".equals(element)) { + final double r = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS; + bodyTube.setInnerRadius(r); + } + if ("Len".equals(element)) { + bodyTube.setLength(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("IsMotorMount".equals(element)) { + bodyTube.setMotorMount("1".equals(content)); + } + if ("EngineOverhang".equals(element)) { + bodyTube.setMotorOverhang(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * Get the InnerTube component this handler is working upon. + * + * @return an InnerTube component + */ + @Override + public InnerTube getComponent() { + return bodyTube; + } + + /** + * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not + * public in all components. + * + * @param position the OpenRocket position + */ + @Override + public void setRelativePosition(RocketComponent.Position position) { + bodyTube.setRelativePosition(position); + } + + /** + * Get the required type of material for this component. + * + * @return BULK + */ + @Override + public Material.Type getMaterialType() { + return Material.Type.BULK; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/LaunchLugHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/LaunchLugHandler.java new file mode 100644 index 00000000..1a9d51aa --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/LaunchLugHandler.java @@ -0,0 +1,110 @@ +/* + * LaunchLugHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * The SAX handler for Rocksim Launch Lugs. + */ +class LaunchLugHandler extends PositionDependentHandler { + + /** + * The OpenRocket LaunchLug instance. + */ + private final LaunchLug lug; + + /** + * Constructor. + * + * @param c the parent + * @param warnings the warning set + * + * @throws IllegalArgumentException thrown if c is null + */ + public LaunchLugHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of a launch lug may not be null."); + } + lug = new LaunchLug(); + if (isCompatible(c, LaunchLug.class, warnings)) { + c.addChild(lug); + } + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("OD".equals(element)) { + lug.setOuterRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("ID".equals(element)) { + lug.setInnerRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("Len".equals(element)) { + lug.setLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + if ("RadialAngle".equals(element)) { + lug.setRadialDirection(Double.parseDouble(content)); + } + if ("FinishCode".equals(element)) { + lug.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * Get the LaunchLug component this handler is working upon. + * + * @return a LaunchLug component + */ + @Override + public LaunchLug getComponent() { + return lug; + } + + /** + * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not + * public in all components. + * + * @param position the OpenRocket position + */ + @Override + public void setRelativePosition(RocketComponent.Position position) { + lug.setRelativePosition(position); + } + + /** + * Get the required type of material for this component. + * + * @return BULK + */ + @Override + public Material.Type getMaterialType() { + return Material.Type.BULK; + } +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/MassObjectHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/MassObjectHandler.java new file mode 100644 index 00000000..53a33d8b --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/MassObjectHandler.java @@ -0,0 +1,113 @@ +/* + * MassObjectHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A SAX handler for Rocksim's MassObject XML type. + */ +class MassObjectHandler extends PositionDependentHandler { + + /** + * The Rocksim Mass length fudge factor. Rocksim completely exaggerates the length of a mass object to the point + * that it looks ridiculous in OpenRocket. This fudge factor is here merely to get the typical mass object to + * render in the OpenRocket UI with it's bounds mostly inside it's parent. The odd thing about it is that + * Rocksim does not expose the length of a mass object in the UI and actually treats mass objects as point objects - + * not 3 or even 2 dimensional. + */ + public static final int MASS_LEN_FUDGE_FACTOR = 100; + + /** + * The OpenRocket MassComponent - counterpart to the RS MassObject. + */ + private final MassComponent mass; + + /** + * Constructor. + *l + * @param c the parent component + * @param warnings the warning set + * + * @throws IllegalArgumentException thrown if c is null + */ + public MassObjectHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of a mass component may not be null."); + } + mass = new MassComponent(); + if (isCompatible(c, MassComponent.class, warnings)) { + c.addChild(mass); + } + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + try { + if ("Len".equals(element)) { + mass.setLength(Double.parseDouble(content) / (RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH * MASS_LEN_FUDGE_FACTOR)); + } + if ("KnownMass".equals(element)) { + mass.setComponentMass(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + } + if ("KnownCG".equals(element)) { + //Setting the CG of the Mass Object to 0 is important because of the different ways that Rocksim and + //OpenRocket treat mass objects. Rocksim treats them as points (even though the data file contains a + //length) and because Rocksim sets the CG of the mass object to really be relative to the front of + //the parent. But that value is already assumed in the position and position value for the component. + //Thus it needs to be set to 0 to say that the mass object's CG is at the point of the mass object. + super.setCG(0); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * Get the component this handler is working upon. + * + * @return a component + */ + @Override + public MassComponent getComponent() { + return mass; + } + + /** + * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not + * public in all components. + * + * @param position the OpenRocket position + */ + public void setRelativePosition(RocketComponent.Position position) { + mass.setRelativePosition(position); + } + + /** + * Get the required type of material for this component. Does not apply to MassComponents. + * + * @return BULK + */ + @Override + public Material.Type getMaterialType() { + return Material.Type.BULK; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/NoseConeHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/NoseConeHandler.java new file mode 100644 index 00000000..7021331d --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/NoseConeHandler.java @@ -0,0 +1,151 @@ +/* + * NoseConeHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * The SAX nose cone handler for Rocksim NoseCones. + */ +class NoseConeHandler extends BaseHandler { + + /** + * The OpenRocket NoseCone. + */ + private final NoseCone noseCone = new NoseCone(); + + /** + * The wall thickness. Used for hollow nose cones. + */ + private double thickness = 0d; + + /** + * Constructor. + * + * @param c the parent component to the nosecone + * @param warnings the warning set + * + * @throws IllegalArgumentException thrown if c is null + */ + public NoseConeHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent component of a nose cone may not be null."); + } + if (isCompatible(c, NoseCone.class, warnings)) { + c.addChild(noseCone); + noseCone.setAftRadiusAutomatic(false); + } + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + //Nose cones in Rocksim may have attached parts - namely Mass Objects - as children. + if ("AttachedParts".equals(element)) { + return new AttachedPartsHandler(noseCone); + } + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("ShapeCode".equals(element)) { + noseCone.setType(RocksimNoseConeCode.fromCode(Integer.parseInt(content)).asOpenRocket()); + } + if ("Len".equals(element)) { + noseCone.setLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("BaseDia".equals(element)) { + noseCone.setAftRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("WallThickness".equals(element)) { + thickness = Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("ShoulderOD".equals(element)) { + noseCone.setAftShoulderRadius(Math.max(0, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("ShoulderLen".equals(element)) { + noseCone.setAftShoulderLength(Math.max(0, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("ShapeParameter".equals(element)) { + //The Rocksim ShapeParameter only applies to certain shapes, although it is included + //in the design file for all nose cones. Applying it when it should not be causes oddities so + //a check is made for the allowable shapes. + if (Transition.Shape.POWER.equals(noseCone.getType()) || + Transition.Shape.HAACK.equals(noseCone.getType()) || + Transition.Shape.PARABOLIC.equals(noseCone.getType())) { + noseCone.setShapeParameter(Double.parseDouble(content)); + } + } + if ("ConstructionType".equals(element)) { + int typeCode = Integer.parseInt(content); + if (typeCode == 0) { + //SOLID + noseCone.setFilled(true); + } + else if (typeCode == 1) { + //HOLLOW + noseCone.setFilled(false); + } + } + if ("FinishCode".equals(element)) { + noseCone.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + @Override + public void endHandler(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.endHandler(element, attributes, content, warnings); + + if (noseCone.isFilled()) { + noseCone.setAftShoulderThickness(noseCone.getAftShoulderRadius()); + } + else { + noseCone.setThickness(thickness); + noseCone.setAftShoulderThickness(thickness); + } + } + + /** + * Get the nose cone component this handler is working upon. + * + * @return a nose cone component + */ + @Override + public NoseCone getComponent() { + return noseCone; + } + + /** + * Get the required type of material for this component. + * + * @return BULK + */ + public Material.Type getMaterialType() { + return Material.Type.BULK; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/ParachuteHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/ParachuteHandler.java new file mode 100644 index 00000000..3ff7555e --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/ParachuteHandler.java @@ -0,0 +1,120 @@ +/* + * ParachuteHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A SAX handler for Rocksim's Parachute XML type. + */ +class ParachuteHandler extends RecoveryDeviceHandler { + /** + * The OpenRocket Parachute instance + */ + private final Parachute chute; + /** + * The shroud line density. + */ + private double shroudLineDensity = 0.0d; + + /** + * Constructor. + * + * @param c the parent component + * @param warnings the warning set + * + * @throws IllegalArgumentException thrown if c is null + */ + public ParachuteHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent of a parachute may not be null."); + } + chute = new Parachute(); + if (isCompatible(c, Parachute.class, warnings)) { + c.addChild(chute); + } + } + + /** + * {@inheritDoc} + */ + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + /** + * {@inheritDoc} + */ + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + try { + if ("Dia".equals(element)) { + chute.setDiameter(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + /* Rocksim doesn't have a packed parachute radius, so we approximate it. */ + double packed; + RocketComponent parent = chute.getParent(); + if (parent instanceof BodyTube) { + packed = ((BodyTube) parent).getOuterRadius() * 0.9; + } + else if (parent instanceof InnerTube) { + packed = ((InnerTube) parent).getInnerRadius() * 0.9; + } + else { + packed = chute.getDiameter() * 0.025; + } + chute.setRadius(packed); + } + if ("ShroudLineCount".equals(element)) { + chute.setLineCount(Math.max(0, Integer.parseInt(content))); + } + if ("ShroudLineLen".equals(element)) { + chute.setLineLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("SpillHoleDia".equals(element)) { + //Not supported in OpenRocket + double spillHoleRadius = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS; + warnings.add("Parachute spill holes are not supported. Ignoring."); + } + if ("ShroudLineMassPerMM".equals(element)) { + shroudLineDensity = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LINE_DENSITY; + } + if ("ShroudLineMaterial".equals(element)) { + chute.setLineMaterial(createCustomMaterial(Material.Type.LINE, content, shroudLineDensity)); + } + if ("DragCoefficient".equals(element)) { + chute.setCD(Double.parseDouble(content)); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * Get the component this handler is working upon. + * + * @return a component + */ + public Parachute getComponent() { + return chute; + } + +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/PositionDependentHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/PositionDependentHandler.java new file mode 100644 index 00000000..6639b050 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/PositionDependentHandler.java @@ -0,0 +1,85 @@ +/* + * PositionDependentHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * An abstract base class that handles position dependencies for all lower level components that + * are position aware. + * + * @param the specific position dependent RocketComponent subtype for which the concrete handler can create + */ +public abstract class PositionDependentHandler extends BaseHandler { + + /** Temporary position value. */ + private Double positionValue = 0d; + + /** Temporary position. */ + private RocketComponent.Position position = RocketComponent.Position.TOP; + + /** + * {@inheritDoc} + */ + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + if ("Xb".equals(element)) { + positionValue = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("LocationMode".equals(element)) { + position = RocksimLocationMode.fromCode(Integer.parseInt( + content)).asOpenRocket(); + } + } + + /** + * This method sets the position information onto the component. Rocksim splits the location/position + * information into two disparate data elements. Both pieces of data are necessary to map into OpenRocket's + * position model. + * + * @param element the element name + * @param attributes the attributes + * @param content the content of the element + * @param warnings the warning set to store warnings in. + * @throws org.xml.sax.SAXException not thrown + */ + @Override + public void endHandler(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + super.endHandler(element, attributes, content, warnings); + setRelativePosition(position); + setLocation(getComponent(), position, positionValue); + } + + /** + * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not + * public in all components. + * + * @param position the OpenRocket position + */ + protected abstract void setRelativePosition(RocketComponent.Position position); + + /** + * Set the position of a component. + * + * @param component the component + * @param position the relative position + * @param location the actual position value + */ + public static void setLocation(RocketComponent component, RocketComponent.Position position, double location) { + if (position.equals(RocketComponent.Position.BOTTOM)) { + component.setPositionValue(-1d * location); + } + else { + component.setPositionValue(location); + } + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RecoveryDeviceHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/RecoveryDeviceHandler.java new file mode 100644 index 00000000..72e356db --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RecoveryDeviceHandler.java @@ -0,0 +1,114 @@ +/* + * RecoveryDeviceHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A handler specific to streamers and parachutes. This is done because Rocksim allows any type of material to be + * used as a recovery device, which causes oddities with respect to densities. Density computation is overridden + * here to try to correctly compute a material's density in OpenRocket units. + * + * @param either a Streamer or Parachute + */ +public abstract class RecoveryDeviceHandler extends PositionDependentHandler { + + /** + * The thickness. Not used by every component, and some component handlers may parse it for their own purposes. + */ + private double thickness = 0d; + /** + * The Rocksim calculated mass. Used only when not overridden and when Rocksim says density == 0 (Rocksim bug). + */ + private Double calcMass = 0d; + + /** + * {@inheritDoc} + */ + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("Thickness".equals(element)) { + thickness = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("CalcMass".equals(element)) { + calcMass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + + /** + * Compute the density. Rocksim does strange things with densities. For some streamer material it's in cubic, + * rather than square, units. In those cases it needs to be converted to an appropriate SURFACE material density. + * + * @param type the rocksim density + * @param rawDensity the density as specified in the Rocksim design file + * @return a value in OpenRocket SURFACE density units + */ + protected double computeDensity(RocksimDensityType type, double rawDensity) { + + double result; + + if (rawDensity > 0d) { + //ROCKSIM_SURFACE is a square area density; compute normally + //ROCKSIM_LINE is a single length dimension (kg/m) but Rocksim ignores thickness for this type and treats + //it like a SURFACE. + if (RocksimDensityType.ROCKSIM_SURFACE.equals(type) || RocksimDensityType.ROCKSIM_LINE.equals(type)) { + result = rawDensity / RocksimDensityType.ROCKSIM_SURFACE.asOpenRocket(); + } + //ROCKSIM_BULK is a cubic area density; multiple by thickness to make per square area; the result, when + //multiplied by the area will then equal Rocksim's computed mass. + else { + result = (rawDensity / type.asOpenRocket()) * thickness; + } + } + else { + result = calcMass / getComponent().getArea(); + //A Rocksim bug on streamers/parachutes results in a 0 density at times. When that is detected, try + //to compute an approximate density from Rocksim's computed mass. + if (RocksimDensityType.ROCKSIM_BULK.equals(type)) { + //ROCKSIM_BULK is a cubic area density; multiple by thickness to make per square area + result *= thickness; + } + } + return result; + } + + /** + * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not + * public in all components. + * + * @param position the OpenRocket position + */ + @Override + public void setRelativePosition(RocketComponent.Position position) { + getComponent().setRelativePosition(position); + } + + /** + * Get the required type of material for this component. This is the OpenRocket type, which does NOT always + * correspond to Rocksim. Some streamer material is defined as BULK in the Rocksim file. In those cases + * it is adjusted in this handler. + * + * @return SURFACE + */ + @Override + public Material.Type getMaterialType() { + return Material.Type.SURFACE; + } + +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RingHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/RingHandler.java new file mode 100644 index 00000000..20f6aac0 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RingHandler.java @@ -0,0 +1,232 @@ +/* + * RingHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.RingComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TubeCoupler; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A SAX handler for centering rings, tube couplers, and bulkheads. + */ +class RingHandler extends PositionDependentHandler { + + /** + * The OpenRocket Ring. + */ + private final CenteringRing ring = new CenteringRing(); + + /** + * The parent component. + */ + private final RocketComponent parent; + + /** + * The parsed Rocksim UsageCode. + */ + private int usageCode = 0; + + /** + * Constructor. + * + * @param theParent the parent component + * @param warnings the warning set + * @throws IllegalArgumentException thrown if c is null + */ + public RingHandler(RocketComponent theParent, WarningSet warnings) throws IllegalArgumentException { + if (theParent == null) { + throw new IllegalArgumentException("The parent of a ring may not be null."); + } + parent = theParent; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("OD".equals(element)) { + ring.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + } + if ("ID".equals(element)) { + ring.setInnerRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); + } + if ("Len".equals(element)) { + ring.setLength(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + if ("UsageCode".equals(element)) { + usageCode = Integer.parseInt(content); + } + } catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * Get the ring component this handler is working upon. + * + * @return a component + */ + @Override + public CenteringRing getComponent() { + return ring; + } + + /** + * This method adds the CenteringRing as a child of the parent rocket component. + * + * @param warnings the warning set + */ + public void asCenteringRing(WarningSet warnings) { + + if (isCompatible(parent, CenteringRing.class, warnings)) { + parent.addChild(ring); + } + } + + /** + * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's Bulkhead. + *

+ * Side Effect Warning: This method adds the resulting Bulkhead as a child of the parent rocket component! + * + * @param warnings the warning set + */ + public void asBulkhead(WarningSet warnings) { + + Bulkhead result = new Bulkhead(); + + copyValues(result); + + if (isCompatible(parent, Bulkhead.class, warnings)) { + parent.addChild(result); + } + } + + /** + * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's TubeCoupler. + *

+ * Side Effect Warning: This method adds the resulting TubeCoupler as a child of the parent rocket component! + * + * @param warnings the warning set + */ + public void asTubeCoupler(WarningSet warnings) { + + TubeCoupler result = new TubeCoupler(); + + copyValues(result); + + if (isCompatible(parent, TubeCoupler.class, warnings)) { + parent.addChild(result); + } + } + + /** + * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's Engine Block. + *

+ * Side Effect Warning: This method adds the resulting EngineBlock as a child of the parent rocket component! + * + * @param warnings the warning set + */ + public void asEngineBlock(WarningSet warnings) { + + EngineBlock result = new EngineBlock(); + + copyValues(result); + + if (isCompatible(parent, EngineBlock.class, warnings)) { + parent.addChild(result); + } + } + + /** + * Copy values from the base ring to the specific component. + * + * @param result the target to which ring values will be copied + */ + private void copyValues(RingComponent result) { + result.setOuterRadius(ring.getOuterRadius()); + result.setInnerRadius(ring.getInnerRadius()); + result.setLength(ring.getLength()); + result.setName(ring.getName()); + setOverride(result, ring.isOverrideSubcomponentsEnabled(), ring.getOverrideMass(), ring.getOverrideCGX()); + result.setRelativePosition(ring.getRelativePosition()); + result.setPositionValue(ring.getPositionValue()); + result.setMaterial(ring.getMaterial()); + } + + /** + * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not + * public in all components. + * + * @param position the OpenRocket position + */ + @Override + public void setRelativePosition(RocketComponent.Position position) { + ring.setRelativePosition(position); + } + + @Override + public void endHandler(String element, HashMap attributes, String content, WarningSet warnings) throws SAXException { + super.endHandler(element, attributes, content, warnings); + + // The XML element in Rocksim design file is used for many types of components, unfortunately. + // Additional subelements are used to indicate the type of the rocket component. When parsing using SAX + // this poses a problem because we can't "look ahead" to see what type is being represented at the start + // of parsing - something that would be nice to do so that we can instantiate the correct OR component + // at the start, then just call setters for the appropriate data. + + // To overcome that, a CenteringRing is instantiated at the start of parsing, it's mutators are called, + // and then at the end (this method) converts the CenteringRing to a more appropriate type. CenteringRing + // is generic enough to support the representation of all similar types without loss of data. + + //UsageCode + // 0 == Centering Ring + // 1 == Bulkhead + // 2 == Engine Block + // 3 == Sleeve + // 4 == Tube Coupler + + if (usageCode == 1) { + //Bulkhead + asBulkhead(warnings); + } else if (usageCode == 2) { + asEngineBlock(warnings); + } else if (usageCode == 4) { + //TubeCoupler + asTubeCoupler(warnings); + } else { + //Default + asCenteringRing(warnings); + } + } + + /** + * Get the required type of material for this component. + * + * @return BULK + */ + @Override + public Material.Type getMaterialType() { + return Material.Type.BULK; + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RocksimDensityType.java b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimDensityType.java new file mode 100644 index 00000000..9e70fa86 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimDensityType.java @@ -0,0 +1,78 @@ +/* + * RocksimDensityType.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.material.Material; + +/** + * Models the nose cone shape of a rocket. Maps from Rocksim's notion to OpenRocket's. + */ +public enum RocksimDensityType { + ROCKSIM_BULK (0, RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY), + ROCKSIM_SURFACE(1, RocksimHandler.ROCKSIM_TO_OPENROCKET_SURFACE_DENSITY), + ROCKSIM_LINE (2, RocksimHandler.ROCKSIM_TO_OPENROCKET_LINE_DENSITY); + + /** The Rocksim enumeration value. Sent in XML. */ + private final int ordinal; + + /** The corresponding OpenRocket shape. */ + private final double conversion; + + /** + * Constructor. + * + * @param idx the Rocksim shape code + * @param theConversion the numerical conversion ratio to OpenRocket + */ + private RocksimDensityType(int idx, double theConversion) { + ordinal = idx; + conversion = theConversion; + } + + /** + * Get the OpenRocket shape that corresponds to the Rocksim value. + * + * @return a conversion + */ + public double asOpenRocket() { + return conversion; + } + + /** + * Lookup an instance of this enum based upon the Rocksim code. + * + * @param rocksimDensityType the Rocksim code (from XML) + * @return an instance of this enum + */ + public static RocksimDensityType fromCode(int rocksimDensityType) { + RocksimDensityType[] values = values(); + for (RocksimDensityType value : values) { + if (value.ordinal == rocksimDensityType) { + return value; + } + } + return ROCKSIM_BULK; //Default + } + + /** + * Get the ordinal code. + * + * @param type the OR type + * + * @return the Rocksim XML value + */ + public static int toCode(Material.Type type) { + if (type.equals(Material.Type.BULK)) { + return ROCKSIM_BULK.ordinal; + } + if (type.equals(Material.Type.LINE)) { + return ROCKSIM_LINE.ordinal; + } + if (type.equals(Material.Type.SURFACE)) { + return ROCKSIM_SURFACE.ordinal; + } + return ROCKSIM_BULK.ordinal; + } +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RocksimFinishCode.java b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimFinishCode.java new file mode 100644 index 00000000..658db56a --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimFinishCode.java @@ -0,0 +1,81 @@ +/* + * RocksimFinishCode.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.rocketcomponent.ExternalComponent; + +/** + * Models the finish of a component. + */ +public enum RocksimFinishCode { + POLISHED(0, ExternalComponent.Finish.POLISHED), + GLOSS(1, ExternalComponent.Finish.SMOOTH), + MATT(2, ExternalComponent.Finish.NORMAL), + UNFINISHED(3, ExternalComponent.Finish.UNFINISHED); + + /** The Rocksim code (from XML). */ + private final int ordinal; + + /** The corresponding OpenRocket finish. */ + private final ExternalComponent.Finish finish; + + /** + * Constructor. + * + * @param idx the Rocksim enum value + * @param theFinish the OpenRocket finish + */ + private RocksimFinishCode(int idx, ExternalComponent.Finish theFinish) { + ordinal = idx; + finish = theFinish; + } + + /** + * Get the OpenRocket finish. + * + * @return a Finish instance + */ + public ExternalComponent.Finish asOpenRocket() { + return finish; + } + + /** + * Lookup an instance of this enum from a Rocksim value. + * + * @param rocksimFinishCode the Rocksim value + * + * @return an instance of this enum; Defaults to MATT + */ + public static RocksimFinishCode fromCode(int rocksimFinishCode) { + RocksimFinishCode[] values = values(); + for (RocksimFinishCode value : values) { + if (value.ordinal == rocksimFinishCode) { + return value; + } + } + return MATT; //Default + } + + /** + * Get the ordinal code. + * + * @param type the OR type + * + * @return the Rocksim XML value + */ + public static int toCode(ExternalComponent.Finish type) { + if (type.equals(ExternalComponent.Finish.UNFINISHED)) { + return UNFINISHED.ordinal; + } + if (type.equals(ExternalComponent.Finish.POLISHED)) { + return POLISHED.ordinal; + } + if (type.equals(ExternalComponent.Finish.SMOOTH)) { + return GLOSS.ordinal; + } + return MATT.ordinal; + } + +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RocksimHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimHandler.java new file mode 100644 index 00000000..7d2d527a --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimHandler.java @@ -0,0 +1,373 @@ +/* + * RocksimHandler.java + * + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Stage; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * This class is a Sax element handler for Rocksim version 9 design files. It parses the Rocksim file (typically + * a .rkt extension) and creates corresponding OpenRocket components. This is a best effort approach and may not + * be an exact replica. + *

+ * Limitations: Rocksim flight simulations are not imported; tube fins are not supported; Rocksim 'pods' are not supported. + */ +public class RocksimHandler extends ElementHandler { + + /** + * Length conversion. Rocksim is in millimeters, OpenRocket in meters. + */ + public static final int ROCKSIM_TO_OPENROCKET_LENGTH = 1000; + + /** + * Mass conversion. Rocksim is in grams, OpenRocket in kilograms. + */ + public static final int ROCKSIM_TO_OPENROCKET_MASS = 1000; + + /** + * Bulk Density conversion. Rocksim is in kilograms/cubic meter, OpenRocket in kilograms/cubic meter. + */ + public static final int ROCKSIM_TO_OPENROCKET_BULK_DENSITY = 1; + + /** + * Surface Density conversion. Rocksim is in grams/sq centimeter, OpenRocket in kilograms/sq meter. 1000/(100*100) = 1/10 + */ + public static final double ROCKSIM_TO_OPENROCKET_SURFACE_DENSITY = 1/10d; + + /** + * Line Density conversion. Rocksim is in kilograms/meter, OpenRocket in kilograms/meter. + */ + public static final int ROCKSIM_TO_OPENROCKET_LINE_DENSITY = 1; + + /** + * Radius conversion. Rocksim is always in diameters, OpenRocket mostly in radius. + */ + public static final int ROCKSIM_TO_OPENROCKET_RADIUS = 2 * ROCKSIM_TO_OPENROCKET_LENGTH; + + /** + * The main content handler. + */ + private RocksimContentHandler handler = null; + + /** + * Return the OpenRocketDocument read from the file, or null if a document + * has not been read yet. + * + * @return the document read, or null. + */ + public OpenRocketDocument getDocument() { + return handler.getDocument(); + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + + // Check for unknown elements + if (!element.equals("RockSimDocument")) { + warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); + return null; + } + + // Check for first call + if (handler != null) { + warnings.add(Warning.fromString("Multiple document elements found, ignoring later " + + "ones.")); + return null; + } + + handler = new RocksimContentHandler(); + return handler; + } + +} + +/** + * Handles the content of the tag. + */ +class RocksimContentHandler extends ElementHandler { + /** + * The OpenRocketDocument that is the container for the rocket. + */ + private final OpenRocketDocument doc; + + /** + * The top-level component, from which all child components are added. + */ + private final Rocket rocket; + + /** + * The rocksim file version. + */ + private String version; + + /** + * Constructor. + */ + public RocksimContentHandler() { + this.rocket = new Rocket(); + this.doc = new OpenRocketDocument(rocket); + } + + /** + * Get the OpenRocket document that has been created from parsing the Rocksim design file. + * + * @return the instantiated OpenRocketDocument + */ + public OpenRocketDocument getDocument() { + return doc; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + if ("DesignInformation".equals(element)) { + //The next sub-element is "RocketDesign", which is really the only thing that matters. Rather than + //create another handler just for that element, handle it here. + return this; + } + if ("FileVersion".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("RocketDesign".equals(element)) { + return new RocketDesignHandler(rocket); + } + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + /** + * SAX handler for Rocksim file version number. The value is not used currently, but could be used in the future + * for backward/forward compatibility reasons (different lower level handlers could be called via a strategy pattern). + */ + if ("FileVersion".equals(element)) { + version = content; + } + } + + /** + * Answer the file version. + * + * @return the version of the Rocksim design file + */ + public String getVersion() { + return version; + } +} + + +/** + * A SAX handler for the high level Rocksim design. This structure includes sub-structures for each of the stages. + * Correct functioning of this handler is predicated on the stage count element appearing before the actual stage parts + * structures. If that invariant is not true, then behavior will be unpredictable. + */ +class RocketDesignHandler extends ElementHandler { + /** + * The parent component. + */ + private final RocketComponent component; + /** + * The parsed stage count. Defaults to 1. + */ + private int stageCount = 1; + /** + * The overridden stage 1 mass. + */ + private double stage1Mass = 0d; + /** + * The overridden stage 2 mass. + */ + private double stage2Mass = 0d; + /** + * The overridden stage 3 mass. + */ + private double stage3Mass = 0d; + /** + * The overridden stage 1 Cg. + */ + private double stage1CG = 0d; + /** + * The overridden stage 2 Cg. + */ + private double stage2CG = 0d; + /** + * The overridden stage 3 Cg. + */ + private double stage3CG = 0d; + + /** + * Constructor. + * + * @param c the parent component + */ + public RocketDesignHandler(RocketComponent c) { + component = c; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + /** + * In Rocksim stages are from the top down, so a single stage rocket is actually stage '3'. A 2-stage + * rocket defines stage '2' as the initial booster with stage '3' sitting atop it. And so on. + */ + if ("Stage3Parts".equals(element)) { + final Stage stage = new Stage(); + if (stage3Mass > 0.0d) { + stage.setMassOverridden(true); + stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override + stage.setOverrideMass(stage3Mass); + } + if (stage3CG > 0.0d) { + stage.setCGOverridden(true); + stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override + stage.setOverrideCGX(stage3CG); + } + component.addChild(stage); + return new StageHandler(stage); + } + if ("Stage2Parts".equals(element)) { + if (stageCount >= 2) { + final Stage stage = new Stage(); + if (stage2Mass > 0.0d) { + stage.setMassOverridden(true); + stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override + stage.setOverrideMass(stage2Mass); + } + if (stage2CG > 0.0d) { + stage.setCGOverridden(true); + stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override + stage.setOverrideCGX(stage2CG); + } + component.addChild(stage); + return new StageHandler(stage); + } + } + if ("Stage1Parts".equals(element)) { + if (stageCount == 3) { + final Stage stage = new Stage(); + if (stage1Mass > 0.0d) { + stage.setMassOverridden(true); + stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override + stage.setOverrideMass(stage1Mass); + } + if (stage1CG > 0.0d) { + stage.setCGOverridden(true); + stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override + stage.setOverrideCGX(stage1CG); + } + component.addChild(stage); + return new StageHandler(stage); + } + } + if ("Name".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("StageCount".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("Stage3Mass".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("Stage2Mass".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("Stage1Mass".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("Stage3CG".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("Stage2CGAlone".equals(element)) { + return PlainTextHandler.INSTANCE; + } + if ("Stage1CGAlone".equals(element)) { + return PlainTextHandler.INSTANCE; + } + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + try { + if ("Name".equals(element)) { + component.setName(content); + } + if ("StageCount".equals(element)) { + stageCount = Integer.parseInt(content); + } + if ("Stage3Mass".equals(element)) { + stage3Mass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; + } + if ("Stage2Mass".equals(element)) { + stage2Mass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; + } + if ("Stage1Mass".equals(element)) { + stage1Mass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; + } + if ("Stage3CG".equals(element)) { + stage3CG = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("Stage2CGAlone".equals(element)) { + stage2CG = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + if ("Stage1CGAlone".equals(element)) { + stage1CG = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + +} + +/** + * A SAX handler for a Rocksim stage. + */ +class StageHandler extends ElementHandler { + /** + * The parent OpenRocket component. + */ + private final RocketComponent component; + + /** + * Constructor. + * + * @param c the parent component + * @throws IllegalArgumentException thrown if c is null + */ + public StageHandler(RocketComponent c) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The stage component may not be null."); + } + component = c; + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + if ("NoseCone".equals(element)) { + return new NoseConeHandler(component, warnings); + } + if ("BodyTube".equals(element)) { + return new BodyTubeHandler(component, warnings); + } + if ("Transition".equals(element)) { + return new TransitionHandler(component, warnings); + } + return null; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RocksimLoader.java b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimLoader.java new file mode 100644 index 00000000..a58a8553 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimLoader.java @@ -0,0 +1,57 @@ +/* + * RocksimLoader.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.RocketLoadException; +import net.sf.openrocket.file.RocketLoader; +import net.sf.openrocket.file.simplesax.SimpleSAX; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This class is the main entry point for Rocksim design file imported to OpenRocket. Currently only Rocksim v9 + * file formats are supported, although it is possible that v8 formats will work for most components. + * + * In the cases of v9 components that exist in Rocksim but have no corollary in OpenRocket a message is added to + * a warning set and presented to the user. In effect, this loading is a 'best-effort' mapping and is not meant to + * be an exact representation of any possible Rocksim design in an OpenRocket format. + * + * Rocksim simulations are not imported. + * + * Wish List: + * Material interface (or at least make them abstract in RocketComponent) + * setMaterial + * getMaterial + */ +public class RocksimLoader extends RocketLoader { + /** + * This method is called by the default implementations of {@link #load(java.io.File)} + * and {@link #load(java.io.InputStream)} to load the rocket. + * + * @throws net.sf.openrocket.file.RocketLoadException + * if an error occurs during loading. + */ + @Override + protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException { + + InputSource xmlSource = new InputSource(source); + + RocksimHandler handler = new RocksimHandler(); + + try { + SimpleSAX.readXML(xmlSource, handler, warnings); + } catch (SAXException e) { + throw new RocketLoadException("Malformed XML in input.", e); + } + + final OpenRocketDocument document = handler.getDocument(); + document.setFile(null); + document.clearUndo(); + return document; + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RocksimLocationMode.java b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimLocationMode.java new file mode 100644 index 00000000..02daeb83 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimLocationMode.java @@ -0,0 +1,71 @@ +/* + * RocksimLocationMode.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.rocketcomponent.RocketComponent; + +/** + * Models the relative position of parts on a rocket. Maps from Rocksim's notion to OpenRocket's. + */ +public enum RocksimLocationMode { + FRONT_OF_OWNING_PART (0, RocketComponent.Position.TOP), + FROM_TIP_OF_NOSE (1, RocketComponent.Position.ABSOLUTE), + BACK_OF_OWNING_PART (2, RocketComponent.Position.BOTTOM); + + /** The value Rocksim uses internally (and in the XML file). */ + private final int ordinal; + + /** The OpenRocket position equivalent. */ + private final RocketComponent.Position position; + + /** + * Constructor. + * + * @param idx the rocksim enum value + * @param theOpenRocketPosition the corresponding OpenRocket position + */ + RocksimLocationMode(int idx, RocketComponent.Position theOpenRocketPosition) { + ordinal = idx; + position = theOpenRocketPosition; + } + + /** + * Get the OpenRocket position. + * + * @return the position instance + */ + public RocketComponent.Position asOpenRocket() { + return position; + } + + /** + * Lookup an instance of this class from a rocksim enum value. + * + * @param rocksimCode the rocksim enum value + * + * @return an instance of this enum + */ + public static RocksimLocationMode fromCode(int rocksimCode) { + RocksimLocationMode[] values = values(); + for (RocksimLocationMode value : values) { + if (value.ordinal == rocksimCode) { + return value; + } + } + return FRONT_OF_OWNING_PART; + } + + public static int toCode(RocketComponent.Position position) { + if (RocketComponent.Position.TOP.equals(position)) { + return 0; + } + if (RocketComponent.Position.ABSOLUTE.equals(position)) { + return 1; + } + if (RocketComponent.Position.BOTTOM.equals(position)) { + return 2; + } + return 0; + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/RocksimNoseConeCode.java b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimNoseConeCode.java new file mode 100644 index 00000000..04540020 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/RocksimNoseConeCode.java @@ -0,0 +1,81 @@ +/* + * RocksimNoseConeCode.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.rocketcomponent.Transition; + +/** + * Models the nose cone shape of a rocket. Maps from Rocksim's notion to OpenRocket's. + */ +public enum RocksimNoseConeCode { + CONICAL (0, Transition.Shape.CONICAL), + OGIVE (1, Transition.Shape.OGIVE), + PARABOLIC (2, Transition.Shape.ELLIPSOID), //Rocksim' PARABOLIC most closely resembles an ELLIPSOID in OpenRocket + ELLIPTICAL (3, Transition.Shape.ELLIPSOID), + POWER_SERIES (4, Transition.Shape.POWER), + PARABOLIC_SERIES(5, Transition.Shape.PARABOLIC), + HAACK (6, Transition.Shape.HAACK); + + /** The Rocksim enumeration value. Sent in XML. */ + private final int ordinal; + + /** The corresponding OpenRocket shape. */ + private final Transition.Shape shape; + + /** + * Constructor. + * + * @param idx the Rocksim shape code + * @param aShape the corresponding OpenRocket shape + */ + private RocksimNoseConeCode(int idx, Transition.Shape aShape) { + ordinal = idx; + shape = aShape; + } + + /** + * Get the OpenRocket shape that corresponds to the Rocksim shape. + * + * @return a shape + */ + public Transition.Shape asOpenRocket() { + return shape; + } + + /** + * Lookup an instance of this enum based upon the Rocksim code. + * + * @param rocksimShapeCode the Rocksim code (from XML) + * @return an instance of this enum + */ + public static RocksimNoseConeCode fromCode(int rocksimShapeCode) { + RocksimNoseConeCode[] values = values(); + for (RocksimNoseConeCode value : values) { + if (value.ordinal == rocksimShapeCode) { + return value; + } + } + return PARABOLIC; //Default + } + + /** + * Lookup an ordinal value for the Rocksim code. + * + * @param type the OR Shape + * + * @return the Rocksim code + */ + public static int toCode(Transition.Shape type) { + RocksimNoseConeCode[] values = values(); + for (RocksimNoseConeCode value : values) { + if (value.shape.equals(type)) { + if (value.ordinal == 2) { + return 3; + } + return value.ordinal; + } + } + return ELLIPTICAL.ordinal; //Default + } +} diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/StreamerHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/StreamerHandler.java new file mode 100644 index 00000000..3002a783 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/StreamerHandler.java @@ -0,0 +1,87 @@ +/* + * StreamerHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Streamer; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * A SAX handler for Streamer components. + */ +class StreamerHandler extends RecoveryDeviceHandler { + + /** + * The OpenRocket Streamer. + */ + private final Streamer streamer; + + /** + * Constructor. + * + * @param c the parent component + * @param warnings the warning set + * + * @throws IllegalArgumentException thrown if c is null + */ + public StreamerHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent of a streamer may not be null."); + } + streamer = new Streamer(); + if (isCompatible(c, Streamer.class, warnings)) { + c.addChild(streamer); + } + } + + /** + * {@inheritDoc} + */ + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + /** + * {@inheritDoc} + */ + @Override + public void closeElement(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("Width".equals(element)) { + streamer.setStripWidth(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("Len".equals(element)) { + streamer.setStripLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("DragCoefficient".equals(element)) { + streamer.setCD(Double.parseDouble(content)); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Streamer getComponent() { + return streamer; + } + +} + diff --git a/core/src/net/sf/openrocket/file/rocksim/importt/TransitionHandler.java b/core/src/net/sf/openrocket/file/rocksim/importt/TransitionHandler.java new file mode 100644 index 00000000..b589b677 --- /dev/null +++ b/core/src/net/sf/openrocket/file/rocksim/importt/TransitionHandler.java @@ -0,0 +1,154 @@ +/* + * TransitionHandler.java + */ +package net.sf.openrocket.file.rocksim.importt; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.file.simplesax.ElementHandler; +import net.sf.openrocket.file.simplesax.PlainTextHandler; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import org.xml.sax.SAXException; + +import java.util.HashMap; + +/** + * The SAX handler for Transition components. + */ +class TransitionHandler extends BaseHandler { + /** + * The OpenRocket Transition. + */ + private final Transition transition = new Transition(); + + /** + * The wall thickness. Used for hollow nose cones. + */ + private double thickness = 0d; + + /** + * Constructor. + * + * @param c the parent component + * @param warnings the warning set + * @throws IllegalArgumentException thrown if c is null + */ + public TransitionHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { + if (c == null) { + throw new IllegalArgumentException("The parent of a transition may not be null."); + } + if (isCompatible(c, Transition.class, warnings)) { + c.addChild(transition); + } + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, WarningSet warnings) { + return PlainTextHandler.INSTANCE; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + super.closeElement(element, attributes, content, warnings); + + try { + if ("ShapeCode".equals(element)) { + transition.setType(RocksimNoseConeCode.fromCode(Integer.parseInt(content)).asOpenRocket()); + } + if ("Len".equals(element)) { + transition.setLength(Math.max(0, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("FrontDia".equals(element)) { + transition.setForeRadius(Math.max(0, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("RearDia".equals(element)) { + transition.setAftRadius(Math.max(0, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("WallThickness".equals(element)) { + thickness = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); + } + if ("FrontShoulderDia".equals(element)) { + transition.setForeShoulderRadius(Math.max(0d, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("RearShoulderDia".equals(element)) { + transition.setAftShoulderRadius(Math.max(0d, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); + } + if ("FrontShoulderLen".equals(element)) { + transition.setForeShoulderLength(Math.max(0d, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("RearShoulderLen".equals(element)) { + transition.setAftShoulderLength(Math.max(0d, Double.parseDouble( + content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); + } + if ("ShapeParameter".equals(element)) { + if (Transition.Shape.POWER.equals(transition.getType()) || + Transition.Shape.HAACK.equals(transition.getType()) || + Transition.Shape.PARABOLIC.equals(transition.getType())) { + transition.setShapeParameter(Double.parseDouble(content)); + } + } + if ("ConstructionType".equals(element)) { + int typeCode = Integer.parseInt(content); + if (typeCode == 0) { + //SOLID + transition.setFilled(true); + } + else if (typeCode == 1) { + //HOLLOW + transition.setFilled(false); + } + } + if ("FinishCode".equals(element)) { + transition.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); + } + if ("Material".equals(element)) { + setMaterialName(content); + } + } + catch (NumberFormatException nfe) { + warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); + } + } + + @Override + public void endHandler(String element, HashMap attributes, String content, WarningSet warnings) + throws SAXException { + super.endHandler(element, attributes, content, warnings); + + if (transition.isFilled()) { + transition.setAftShoulderThickness(transition.getAftShoulderRadius()); + transition.setForeShoulderThickness(transition.getForeShoulderRadius()); + } + else { + transition.setThickness(thickness); + transition.setAftShoulderThickness(thickness); + transition.setForeShoulderThickness(thickness); + } + } + + + @Override + public Transition getComponent() { + return transition; + } + + /** + * Get the required type of material for this component. + * + * @return BULK + */ + public Material.Type getMaterialType() { + return Material.Type.BULK; + } + + +} + diff --git a/core/src/net/sf/openrocket/file/simplesax/DelegatorHandler.java b/core/src/net/sf/openrocket/file/simplesax/DelegatorHandler.java new file mode 100644 index 00000000..3ec6bdc1 --- /dev/null +++ b/core/src/net/sf/openrocket/file/simplesax/DelegatorHandler.java @@ -0,0 +1,121 @@ +package net.sf.openrocket.file.simplesax; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; + +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * The actual SAX handler class. Contains the necessary methods for parsing the SAX source. + * Delegates the actual content parsing to {@link ElementHandler} objects. + */ +class DelegatorHandler extends DefaultHandler { + private final WarningSet warnings; + + private final Deque handlerStack = new ArrayDeque(); + private final Deque elementData = new ArrayDeque(); + private final Deque> elementAttributes = new ArrayDeque>(); + + + // Ignore all elements as long as ignore > 0 + private int ignore = 0; + + + public DelegatorHandler(ElementHandler initialHandler, WarningSet warnings) { + this.warnings = warnings; + handlerStack.add(initialHandler); + elementData.add(new StringBuilder()); // Just in case + } + + + ///////// SAX handlers + + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) throws SAXException { + + // Check for ignore + if (ignore > 0) { + ignore++; + return; + } + + // Check for unknown namespace + if (!uri.equals("")) { + warnings.add(Warning.fromString("Unknown namespace element '" + uri + + "' encountered, ignoring.")); + ignore++; + return; + } + + // Add layer to data stacks + elementData.push(new StringBuilder()); + elementAttributes.push(copyAttributes(attributes)); + + // Call the handler + ElementHandler h = handlerStack.peek(); + h = h.openElement(localName, elementAttributes.peek(), warnings); + if (h != null) { + handlerStack.push(h); + } else { + // Start ignoring elements + ignore++; + } + } + + + /** + * Stores encountered characters in the elementData stack. + */ + @Override + public void characters(char[] chars, int start, int length) throws SAXException { + // Check for ignore + if (ignore > 0) + return; + + StringBuilder sb = elementData.peek(); + sb.append(chars, start, length); + } + + + /** + * Removes the last layer from the stack. + */ + @Override + public void endElement(String uri, String localName, String name) throws SAXException { + + // Check for ignore + if (ignore > 0) { + ignore--; + return; + } + + // Remove data from stack + String data = elementData.pop().toString(); // throws on error + HashMap attr = elementAttributes.pop(); + + // Remove last handler and call the next one + ElementHandler h; + + h = handlerStack.pop(); + h.endHandler(localName, attr, data, warnings); + + h = handlerStack.peek(); + h.closeElement(localName, attr, data, warnings); + } + + + private static HashMap copyAttributes(Attributes atts) { + HashMap ret = new HashMap(); + for (int i = 0; i < atts.getLength(); i++) { + ret.put(atts.getLocalName(i), atts.getValue(i)); + } + return ret; + } +} diff --git a/core/src/net/sf/openrocket/file/simplesax/ElementHandler.java b/core/src/net/sf/openrocket/file/simplesax/ElementHandler.java new file mode 100644 index 00000000..ae678f80 --- /dev/null +++ b/core/src/net/sf/openrocket/file/simplesax/ElementHandler.java @@ -0,0 +1,94 @@ +package net.sf.openrocket.file.simplesax; + +import java.util.HashMap; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; + +import org.xml.sax.SAXException; + + +/** + * A "simple XML" element handler. An object of this class handles a single element of + * an XML file. If the input file is: + * + * + * message + * + * + * and the initial handler is initHandler, then the following methods will be called: + * + * 1. initHandler.{@link #openElement(String, HashMap, WarningSet)} is called for + * the opening element , which returns fooHandler + * 2. fooHandler.{@link #openElement(String, HashMap, WarningSet)} is called for + * the opening element , which returns barHandler + * 3. barHandler.{@link #endHandler(String, HashMap, String, WarningSet)} is called for + * the closing element + * 4. fooHandler.{@link #closeElement(String, HashMap, String, WarningSet)} is called for + * the closing element + * 5. fooHandler.{@link #endHandler(String, HashMap, String, WarningSet)} is called for + * the closing element + * 6. initHandler.{@link #closeElement(String, HashMap, String, WarningSet)} is called for + * the closing element + * + * Note that {@link #endHandler(String, HashMap, String, WarningSet)} is not called for + * the initial handler. + * + * @author Sampo Niskanen + */ +public abstract class ElementHandler { + + /** + * Called when an opening element is encountered. Returns the handler that will handle + * the elements within that element, or null if the element and all of + * its contents is to be ignored. + *

+ * Note that this method may also return this, in which case this + * handler will also handle the subelement. + * + * @param element the element name. + * @param attributes attributes of the element. + * @param warnings the warning set to store warnings in. + * @return the handler that handles elements encountered within this element, + * or null if the element is to be ignored. + */ + public abstract ElementHandler openElement(String element, + HashMap attributes, WarningSet warnings) throws SAXException; + + /** + * Called when an element is closed. The default implementation checks whether there is + * any non-space text within the element and if there exists any attributes, and adds + * a warning of both. This can be used at the and of the method to check for + * spurious data. + * + * @param element the element name. + * @param attributes attributes of the element. + * @param content the textual content of the element. + * @param warnings the warning set to store warnings in. + */ + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + + if (!content.trim().equals("")) { + warnings.add(Warning.fromString("Unknown text in element '" + element + + "', ignoring.")); + } + if (!attributes.isEmpty()) { + warnings.add(Warning.fromString("Unknown attributes in element '" + element + + "', ignoring.")); + } + } + + + /** + * Called when the element block that this handler is handling ends. + * The default implementation is a no-op. + * + * @param warnings the warning set to store warnings in. + */ + public void endHandler(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + // No-op + } + +} diff --git a/core/src/net/sf/openrocket/file/simplesax/NullElementHandler.java b/core/src/net/sf/openrocket/file/simplesax/NullElementHandler.java new file mode 100644 index 00000000..e8dd2df8 --- /dev/null +++ b/core/src/net/sf/openrocket/file/simplesax/NullElementHandler.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.file.simplesax; + +import java.util.HashMap; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; + +import org.xml.sax.SAXException; + +/** + * A singleton element handler that does not accept any content in the element + * except whitespace text. All subelements are ignored and a warning is produced + * of them. It ignores any attributes. + *

+ * This class can be used for elements that have no content but contain attributes. + * + * @author Sampo Niskanen + */ +public class NullElementHandler extends ElementHandler { + public static final NullElementHandler INSTANCE = new NullElementHandler(); + + private static final HashMap EMPTY_MAP = new HashMap(); + + private NullElementHandler() { + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) throws SAXException { + super.closeElement(element, EMPTY_MAP, content, warnings); + } + +} diff --git a/core/src/net/sf/openrocket/file/simplesax/PlainTextHandler.java b/core/src/net/sf/openrocket/file/simplesax/PlainTextHandler.java new file mode 100644 index 00000000..03be721a --- /dev/null +++ b/core/src/net/sf/openrocket/file/simplesax/PlainTextHandler.java @@ -0,0 +1,31 @@ +package net.sf.openrocket.file.simplesax; + +import java.util.HashMap; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; + +/** + * An element handler that does not allow any sub-elements. If any are encountered + * a warning is generated and they are ignored. + */ +public class PlainTextHandler extends ElementHandler { + public static final PlainTextHandler INSTANCE = new PlainTextHandler(); + + private PlainTextHandler() { + } + + @Override + public ElementHandler openElement(String element, HashMap attributes, + WarningSet warnings) { + warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); + return null; + } + + @Override + public void closeElement(String element, HashMap attributes, + String content, WarningSet warnings) { + // Warning from openElement is sufficient. + } +} + diff --git a/core/src/net/sf/openrocket/file/simplesax/SimpleSAX.java b/core/src/net/sf/openrocket/file/simplesax/SimpleSAX.java new file mode 100644 index 00000000..025c8886 --- /dev/null +++ b/core/src/net/sf/openrocket/file/simplesax/SimpleSAX.java @@ -0,0 +1,47 @@ +package net.sf.openrocket.file.simplesax; + +import java.io.IOException; + +import net.sf.openrocket.aerodynamics.WarningSet; + +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.XMLReaderFactory; + + +/** + * A "simple SAX" XML reader. This system imposes the limit that an XML element may + * contain either textual (non-whitespace) content OR additional elements, but not + * both. This holds true for both the OpenRocket and RockSim design formats and the + * RockSim engine definition format. + *

+ * The actual handling is performed by subclasses of {@link ElementHandler}. The + * initial handler is provided to the {@link #readXML(InputSource, ElementHandler, WarningSet)} + * method. + * + * @author Sampo Niskanen + */ +public class SimpleSAX { + + /** + * Read a simple XML file. + * + * @param source the SAX input source. + * @param initialHandler the initial content handler. + * @param warnings a warning set to store warning (cannot be null). + * @throws IOException if an I/O exception occurs while reading. + * @throws SAXException if e.g. malformed XML is encountered. + */ + public static void readXML(InputSource source, ElementHandler initialHandler, + WarningSet warnings) throws IOException, SAXException { + + DelegatorHandler xmlhandler = new DelegatorHandler(initialHandler, warnings); + + XMLReader reader = XMLReaderFactory.createXMLReader(); + reader.setContentHandler(xmlhandler); + reader.setErrorHandler(xmlhandler); + reader.parse(source); + } + +} diff --git a/core/src/net/sf/openrocket/gui/Resettable.java b/core/src/net/sf/openrocket/gui/Resettable.java new file mode 100644 index 00000000..1f30d2b7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/Resettable.java @@ -0,0 +1,15 @@ +package net.sf.openrocket.gui; + +/** + * An interface for GUI elements with a resettable model. The resetModel() method in + * this interface resets the model to some default model, releasing the old model + * listening connections. + * + * Some components that don't have a settable model simply release the current model. + * These components cannot therefore be reused after calling resetModel(). + * + * @author Sampo Niskanen + */ +public interface Resettable { + public void resetModel(); +} diff --git a/core/src/net/sf/openrocket/gui/SpinnerEditor.java b/core/src/net/sf/openrocket/gui/SpinnerEditor.java new file mode 100644 index 00000000..843eed6a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/SpinnerEditor.java @@ -0,0 +1,21 @@ +package net.sf.openrocket.gui; + +import javax.swing.JSpinner; + +/** + * Editable editor for a JSpinner. Simply uses JSpinner.DefaultEditor, which has been made + * editable. Why the f*** isn't this possible in the normal API? + * + * @author Sampo Niskanen + */ + +public class SpinnerEditor extends JSpinner.NumberEditor { +//public class SpinnerEditor extends JSpinner.DefaultEditor { + + public SpinnerEditor(JSpinner spinner) { + //super(spinner); + super(spinner,"0.0##"); + //getTextField().setEditable(true); + } + +} diff --git a/core/src/net/sf/openrocket/gui/StorageOptionChooser.java b/core/src/net/sf/openrocket/gui/StorageOptionChooser.java new file mode 100644 index 00000000..49b21974 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/StorageOptionChooser.java @@ -0,0 +1,287 @@ +package net.sf.openrocket.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.ButtonGroup; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JSpinner; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.document.StorageOptions; +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.file.openrocket.OpenRocketSaver; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.startup.Application; + +public class StorageOptionChooser extends JPanel { + + public static final double DEFAULT_SAVE_TIME_SKIP = 0.20; + + private final OpenRocketDocument document; + + private JRadioButton allButton; + private JRadioButton someButton; + private JRadioButton noneButton; + + private JSpinner timeSpinner; + + private JCheckBox compressButton; + + private JLabel estimateLabel; + + + private boolean artificialEvent = false; + private static final Translator trans = Application.getTranslator(); + + public StorageOptionChooser(OpenRocketDocument doc, StorageOptions opts) { + super(new MigLayout()); + + this.document = doc; + + + ChangeListener changeUpdater = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + updateEstimate(); + } + }; + ActionListener actionUpdater = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateEstimate(); + } + }; + + + ButtonGroup buttonGroup = new ButtonGroup(); + String tip; + + //// Simulated data to store: + this.add(new JLabel(trans.get("StorageOptChooser.lbl.Simdatatostore")), "spanx, wrap unrel"); + + //// All simulated data + allButton = new JRadioButton(trans.get("StorageOptChooser.rdbut.Allsimdata")); + //// Store all simulated data.
+ //// This can result in very large files! + allButton.setToolTipText(trans.get("StorageOptChooser.lbl.longA1") + + trans.get("StorageOptChooser.lbl.longA2")); + buttonGroup.add(allButton); + allButton.addActionListener(actionUpdater); + this.add(allButton, "spanx, wrap rel"); + + //// Every + someButton = new JRadioButton(trans.get("StorageOptChooser.rdbut.Every")); + //// Store plottable values approximately this far apart.
" + //// Larger values result in smaller files. + tip = trans.get("StorageOptChooser.lbl.longB1") + + trans.get("StorageOptChooser.lbl.longB2"); + someButton.setToolTipText(tip); + buttonGroup.add(someButton); + someButton.addActionListener(actionUpdater); + this.add(someButton, ""); + + timeSpinner = new JSpinner(new SpinnerNumberModel(0.0, 0.0, 5.0, 0.1)); + timeSpinner.setToolTipText(tip); + timeSpinner.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (artificialEvent) + return; + someButton.setSelected(true); + } + }); + this.add(timeSpinner, "wmin 55lp"); + timeSpinner.addChangeListener(changeUpdater); + + //// seconds + JLabel label = new JLabel(trans.get("StorageOptChooser.lbl.seconds")); + label.setToolTipText(tip); + this.add(label, "wrap rel"); + + //// Only primary figures + noneButton = new JRadioButton(trans.get("StorageOptChooser.rdbut.Onlyprimfig")); + //// Store only the values shown in the summary table.
+ //// This results in the smallest files. + noneButton.setToolTipText(trans.get("StorageOptChooser.lbl.longC1") + + trans.get("StorageOptChooser.lbl.longC2")); + buttonGroup.add(noneButton); + noneButton.addActionListener(actionUpdater); + this.add(noneButton, "spanx, wrap 20lp"); + + + //// Compress file + compressButton = new JCheckBox(trans.get("StorageOptChooser.checkbox.Compfile")); + //// Using compression reduces the file size significantly. + compressButton.setToolTipText(trans.get("StorageOptChooser.lbl.UsingComp")); + compressButton.addActionListener(actionUpdater); + this.add(compressButton, "spanx, wrap para"); + + + // Estimate is updated in loadOptions(opts) + estimateLabel = new JLabel(""); + //// An estimate on how large the resulting file would + //// be with the present options. + estimateLabel.setToolTipText(trans.get("StorageOptChooser.lbl.longD1")); + this.add(estimateLabel, "spanx"); + + + this.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createEmptyBorder(0, 10, 0, 0), + //// Save options + BorderFactory.createTitledBorder(trans.get("StorageOptChooser.ttip.Saveopt")))); + + loadOptions(opts); + } + + + public void loadOptions(StorageOptions opts) { + double t; + + // Data storage radio button + t = opts.getSimulationTimeSkip(); + if (t == StorageOptions.SIMULATION_DATA_ALL) { + allButton.setSelected(true); + t = DEFAULT_SAVE_TIME_SKIP; + } else if (t == StorageOptions.SIMULATION_DATA_NONE) { + noneButton.setSelected(true); + t = DEFAULT_SAVE_TIME_SKIP; + } else { + someButton.setSelected(true); + } + + // Time skip spinner + artificialEvent = true; + timeSpinner.setValue(t); + artificialEvent = false; + + // Compression checkbox + compressButton.setSelected(opts.isCompressionEnabled()); + + updateEstimate(); + } + + + public void storeOptions(StorageOptions opts) { + double t; + + if (allButton.isSelected()) { + t = StorageOptions.SIMULATION_DATA_ALL; + } else if (noneButton.isSelected()) { + t = StorageOptions.SIMULATION_DATA_NONE; + } else { + t = (Double)timeSpinner.getValue(); + } + + opts.setSimulationTimeSkip(t); + + opts.setCompressionEnabled(compressButton.isSelected()); + + opts.setExplicitlySet(true); + } + + + + // TODO: MEDIUM: The estimation method always uses OpenRocketSaver! + private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver(); + + private void updateEstimate() { + StorageOptions opts = new StorageOptions(); + + storeOptions(opts); + long size = ROCKET_SAVER.estimateFileSize(document, opts); + size = Math.max((size+512)/1024, 1); + + String formatted; + + if (size >= 10000) { + formatted = (size/1000) + " MB"; + } else if (size >= 1000){ + formatted = (size/1000) + "." + ((size/100)%10) + " MB"; + } else if (size >= 100) { + formatted = ((size/10)*10) + " kB"; + } else { + formatted = size + " kB"; + } + + //// Estimated file size: + estimateLabel.setText(trans.get("StorageOptChooser.lbl.Estfilesize") + " " + formatted); + } + + + + /** + * Asks the user the storage options using a modal dialog window if the document + * contains simulated data and the user has not explicitly set how to store the data. + * + * @param document the document to check. + * @param parent the parent frame for the dialog. + * @return true to continue, false if the user cancelled. + */ + public static boolean verifyStorageOptions(OpenRocketDocument document, JFrame parent) { + StorageOptions options = document.getDefaultStorageOptions(); + + if (options.isExplicitlySet()) { + // User has explicitly set the values, save as is + return true; + } + + + boolean hasData = false; + + simulationLoop: + for (Simulation s: document.getSimulations()) { + if (s.getStatus() == Simulation.Status.NOT_SIMULATED || + s.getStatus() == Simulation.Status.EXTERNAL) + continue; + + FlightData data = s.getSimulatedData(); + if (data == null) + continue; + + for (int i=0; i < data.getBranchCount(); i++) { + FlightDataBranch branch = data.getBranch(i); + if (branch == null) + continue; + if (branch.getLength() > 0) { + hasData = true; + break simulationLoop; + } + } + } + + + if (!hasData) { + // No data to store, do not ask only about compression + return true; + } + + + StorageOptionChooser chooser = new StorageOptionChooser(document, options); + + //// Save options + if (JOptionPane.showConfirmDialog(parent, chooser, trans.get("StorageOptChooser.lbl.Saveopt"), + JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) != + JOptionPane.OK_OPTION) { + // User cancelled + return false; + } + + chooser.storeOptions(options); + return true; + } + +} diff --git a/core/src/net/sf/openrocket/gui/TextFieldListener.java b/core/src/net/sf/openrocket/gui/TextFieldListener.java new file mode 100644 index 00000000..d0773004 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/TextFieldListener.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.gui; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; + +import javax.swing.JTextField; + +public abstract class TextFieldListener implements ActionListener, FocusListener { + private JTextField field; + + public void listenTo(JTextField newField) { + if (field != null) { + field.removeActionListener(this); + field.removeFocusListener(this); + } + field = newField; + if (field != null) { + field.addActionListener(this); + field.addFocusListener(this); + } + } + + public abstract void setText(String text); + + public void actionPerformed(ActionEvent e) { + setText(field.getText()); + } + public void focusGained(FocusEvent e) { } + public void focusLost(FocusEvent e) { + setText(field.getText()); + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/adaptors/BooleanModel.java b/core/src/net/sf/openrocket/gui/adaptors/BooleanModel.java new file mode 100644 index 00000000..a2f95ed3 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/BooleanModel.java @@ -0,0 +1,331 @@ +package net.sf.openrocket.gui.adaptors; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; + +import javax.swing.AbstractAction; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Invalidatable; +import net.sf.openrocket.util.Invalidator; +import net.sf.openrocket.util.MemoryManagement; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.StateChangeListener; + + +/** + * A class that adapts an isXXX/setXXX boolean variable. It functions as an Action suitable + * for usage in JCheckBox or JToggleButton. You can create a suitable button with + * + * check = new JCheckBox(new BooleanModel(component,"Value")) + * check.setText("Label"); + * + * This will produce a button that uses isValue() and setValue(boolean) of the corresponding + * component. + *

+ * Additionally a number of component enabled states may be controlled by this class using + * the method {@link #addEnableComponent(Component, boolean)}. + * + * @author Sampo Niskanen + */ +public class BooleanModel extends AbstractAction implements StateChangeListener, Invalidatable { + private static final LogHelper log = Application.getLogger(); + + private final ChangeSource source; + private final String valueName; + + /* Only used when referencing a ChangeSource! */ + private final Method getMethod; + private final Method setMethod; + private final Method getEnabled; + + /* Only used with internal boolean value! */ + private boolean value; + + + private final List components = new ArrayList(); + private final List componentEnableState = new ArrayList(); + + private String toString = null; + + private int firing = 0; + + private boolean oldValue; + private boolean oldEnabled; + + private Invalidator invalidator = new Invalidator(this); + + + /** + * Construct a BooleanModel that holds the boolean value within itself. + * + * @param initialValue the initial value of the boolean + */ + public BooleanModel(boolean initialValue) { + this.valueName = null; + this.source = null; + this.getMethod = null; + this.setMethod = null; + this.getEnabled = null; + + this.value = initialValue; + + oldValue = getValue(); + oldEnabled = getIsEnabled(); + + this.setEnabled(oldEnabled); + this.putValue(SELECTED_KEY, oldValue); + + } + + /** + * Construct a BooleanModel that references the boolean from a ChangeSource method. + * + * @param source the boolean source. + * @param valueName the name of the getter/setter method (without the get/is/set prefix) + */ + public BooleanModel(ChangeSource source, String valueName) { + this.source = source; + this.valueName = valueName; + + Method getter = null, setter = null; + + + // Try get/is and set + try { + getter = source.getClass().getMethod("is" + valueName); + } catch (NoSuchMethodException ignore) { + } + if (getter == null) { + try { + getter = source.getClass().getMethod("get" + valueName); + } catch (NoSuchMethodException ignore) { + } + } + try { + setter = source.getClass().getMethod("set" + valueName, boolean.class); + } catch (NoSuchMethodException ignore) { + } + + if (getter == null || setter == null) { + throw new IllegalArgumentException("get/is methods for boolean '" + valueName + + "' not present in class " + source.getClass().getCanonicalName()); + } + + getMethod = getter; + setMethod = setter; + + Method e = null; + try { + e = source.getClass().getMethod("is" + valueName + "Enabled"); + } catch (NoSuchMethodException ignore) { + } + getEnabled = e; + + oldValue = getValue(); + oldEnabled = getIsEnabled(); + + this.setEnabled(oldEnabled); + this.putValue(SELECTED_KEY, oldValue); + + source.addChangeListener(this); + } + + public boolean getValue() { + + if (getMethod != null) { + + try { + return (Boolean) getMethod.invoke(source); + } catch (IllegalAccessException e) { + throw new BugException("getMethod execution error for source " + source, e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + + } else { + + // Use internal value + return value; + + } + } + + public void setValue(boolean b) { + checkState(true); + log.debug("Setting value of " + this + " to " + b); + + if (setMethod != null) { + try { + setMethod.invoke(source, new Object[] { b }); + } catch (IllegalAccessException e) { + throw new BugException("setMethod execution error for source " + source, e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } else { + // Manually fire state change - normally the ChangeSource fires it + value = b; + stateChanged(null); + } + } + + + /** + * Add a component the enabled status of which will be controlled by the value + * of this boolean. The component will be enabled exactly when + * the state of this model is equal to that of enableState. + * + * @param component the component to control. + * @param enableState the state in which the component should be enabled. + */ + public void addEnableComponent(Component component, boolean enableState) { + checkState(true); + components.add(component); + componentEnableState.add(enableState); + updateEnableStatus(); + } + + /** + * Add a component which will be enabled when this boolean is true. + * This is equivalent to booleanModel.addEnableComponent(component, true). + * + * @param component the component to control. + * @see #addEnableComponent(Component, boolean) + */ + public void addEnableComponent(Component component) { + checkState(true); + addEnableComponent(component, true); + } + + private void updateEnableStatus() { + boolean state = getValue(); + + for (int i = 0; i < components.size(); i++) { + Component c = components.get(i); + boolean b = componentEnableState.get(i); + c.setEnabled(state == b); + } + } + + + + private boolean getIsEnabled() { + if (getEnabled == null) + return true; + try { + return (Boolean) getEnabled.invoke(source); + } catch (IllegalAccessException e) { + throw new BugException("getEnabled execution error for source " + source, e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + @Override + public void stateChanged(EventObject event) { + checkState(true); + + if (firing > 0) { + log.debug("Ignoring stateChanged of " + this + ", currently firing events"); + return; + } + + boolean v = getValue(); + boolean e = getIsEnabled(); + if (oldValue != v) { + log.debug("Value of " + this + " has changed to " + v + " oldValue=" + oldValue); + oldValue = v; + firing++; + this.putValue(SELECTED_KEY, getValue()); + // this.firePropertyChange(SELECTED_KEY, !v, v); + updateEnableStatus(); + firing--; + } + if (oldEnabled != e) { + log.debug("Enabled status of " + this + " has changed to " + e + " oldEnabled=" + oldEnabled); + oldEnabled = e; + setEnabled(e); + } + } + + + @Override + public void actionPerformed(ActionEvent e) { + if (firing > 0) { + log.debug("Ignoring actionPerformed of " + this + ", currently firing events"); + return; + } + + boolean v = (Boolean) this.getValue(SELECTED_KEY); + log.user("Value of " + this + " changed to " + v + " oldValue=" + oldValue); + if (v != oldValue) { + firing++; + setValue(v); + oldValue = getValue(); + // Update all states + this.putValue(SELECTED_KEY, oldValue); + this.setEnabled(getIsEnabled()); + updateEnableStatus(); + firing--; + } + } + + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + checkState(true); + super.addPropertyChangeListener(listener); + } + + + /** + * Invalidates this model by removing all listeners and removing this from + * listening to the source. After invalidation no listeners can be added to this + * model and the value cannot be set. + */ + @Override + public void invalidate() { + invalidator.invalidate(); + + PropertyChangeListener[] listeners = this.getPropertyChangeListeners(); + if (listeners.length > 0) { + log.warn("Invalidating " + this + " while still having listeners " + listeners); + for (PropertyChangeListener l : listeners) { + this.removePropertyChangeListener(l); + } + } + if (source != null) { + source.removeChangeListener(this); + } + MemoryManagement.collectable(this); + } + + + private void checkState(boolean error) { + invalidator.check(error); + } + + + + @Override + public String toString() { + if (toString == null) { + if (source != null) { + toString = "BooleanModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; + } else { + toString = "BooleanModel[internal value]"; + } + } + return toString; + } +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/Column.java b/core/src/net/sf/openrocket/gui/adaptors/Column.java new file mode 100644 index 00000000..87997c48 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/Column.java @@ -0,0 +1,67 @@ +package net.sf.openrocket.gui.adaptors; + +import javax.swing.table.TableColumnModel; + +public abstract class Column { + private final String name; + + /** + * Create a new column with specified name. Additionally, the {@link #getValueAt(int)} + * method must be implemented. + * + * @param name the caption of the column. + */ + public Column(String name) { + this.name = name; + } + + /** + * Return the caption of the column. + */ + @Override + public String toString() { + return name; + } + + /** + * Return the default width of the column. This is used by the method + * {@link #ColumnTableModel.setColumnWidth(TableColumnModel)}. The default width is + * 100, the method may be overridden to return other values relative to this value. + * + * @return the relative width of the column (default 100). + */ + public int getDefaultWidth() { + return 100; + } + + + /** + * Returns the exact width of this column. If the return value is positive, + * both the minimum and maximum widths of this column are set to this value + * + * @return the absolute exact width of the column (default 0). + */ + public int getExactWidth() { + return 0; + } + + + /** + * Return the column type class. This is necessary for example for numerical + * sorting of Value objects, showing booleans as checkboxes etc. + * + * @return the object class of this column, by default Object.class. + */ + public Class getColumnClass() { + return Object.class; + } + + /** + * Return the value in this column at the specified row. + * + * @param row the row of the data. + * @return the value at the specified position. + */ + public abstract Object getValueAt(int row); + +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java b/core/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java new file mode 100644 index 00000000..e50a8ea5 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.gui.adaptors; + +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import net.sf.openrocket.startup.Application; + +public abstract class ColumnTableModel extends AbstractTableModel { + private final Column[] columns; + + public ColumnTableModel(Column... columns) { + this.columns = columns; + } + + public void setColumnWidths(TableColumnModel model) { + for (int i = 0; i < columns.length; i++) { + if (columns[i].getExactWidth() > 0) { + TableColumn col = model.getColumn(i); + int w = columns[i].getExactWidth(); + col.setResizable(false); + col.setMinWidth(w); + col.setMaxWidth(w); + col.setPreferredWidth(w); + } else { + model.getColumn(i).setPreferredWidth(columns[i].getDefaultWidth()); + } + } + } + + @Override + public int getColumnCount() { + return columns.length; + } + + @Override + public String getColumnName(int col) { + return columns[col].toString(); + } + + @Override + public Class getColumnClass(int col) { + return columns[col].getColumnClass(); + } + + @Override + public Object getValueAt(int row, int col) { + if ((row < 0) || (row >= getRowCount()) || + (col < 0) || (col >= columns.length)) { + Application.getExceptionHandler().handleErrorCondition("Error: Requested illegal column/row, col=" + col + " row=" + row); + return null; + } + return columns[col].getValueAt(row); + } + +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java b/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java new file mode 100644 index 00000000..820a54a6 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/DoubleModel.java @@ -0,0 +1,941 @@ +package net.sf.openrocket.gui.adaptors; + +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.EventListener; +import java.util.EventObject; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.BoundedRangeModel; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Invalidatable; +import net.sf.openrocket.util.Invalidator; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.MemoryManagement; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.StateChangeListener; + + +/** + * A model connector that can read and modify any value of any ChangeSource that + * has the appropriate get/set methods defined. + * + * The variable is defined in the constructor by providing the variable name as a string + * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a + * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2. + * + * Sub-models suitable for JSpinners and other components are available from the appropriate + * methods. + * + * @author Sampo Niskanen + */ + +public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable { + private static final LogHelper log = Application.getLogger(); + + + public static final DoubleModel ZERO = new DoubleModel(0); + + //////////// JSpinner Model //////////// + + /** + * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel + * to be compatible with the NumberEditor, but only has the necessary methods defined. + */ + private class ValueSpinnerModel extends SpinnerNumberModel implements Invalidatable { + + @Override + public Object getValue() { + return currentUnit.toUnit(DoubleModel.this.getValue()); + } + + @Override + public void setValue(Object value) { + if (firing > 0) { + // Ignore, if called when model is sending events + log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() + + " value=" + value + ", currently firing events"); + return; + } + Number num = (Number) value; + double newValue = num.doubleValue(); + double converted = currentUnit.fromUnit(newValue); + + log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue + + " converted=" + converted); + DoubleModel.this.setValue(converted); + + } + + @Override + public Object getNextValue() { + double d = currentUnit.toUnit(DoubleModel.this.getValue()); + double max = currentUnit.toUnit(maxValue); + if (MathUtil.equals(d, max)) + return null; + d = currentUnit.getNextValue(d); + if (d > max) + d = max; + return d; + } + + @Override + public Object getPreviousValue() { + double d = currentUnit.toUnit(DoubleModel.this.getValue()); + double min = currentUnit.toUnit(minValue); + if (MathUtil.equals(d, min)) + return null; + d = currentUnit.getPreviousValue(d); + if (d < min) + d = min; + return d; + } + + + @Override + public Comparable getMinimum() { + return currentUnit.toUnit(minValue); + } + + @Override + public Comparable getMaximum() { + return currentUnit.toUnit(maxValue); + } + + + @Override + public void addChangeListener(ChangeListener l) { + DoubleModel.this.addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l) { + DoubleModel.this.removeChangeListener(l); + } + + @Override + public void invalidate() { + DoubleModel.this.invalidate(); + } + } + + /** + * Returns a new SpinnerModel with the same base as the DoubleModel. + * The values given to the JSpinner are in the currently selected units. + * + * @return A compatibility layer for a SpinnerModel. + */ + public SpinnerModel getSpinnerModel() { + return new ValueSpinnerModel(); + } + + + + + + //////////// JSlider model //////////// + + private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable { + private static final int MAX = 1000; + + /* + * Use linear scale value = linear1 * x + linear0 when x < linearPosition + * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise + */ + + // Linear in range x <= linearPosition + private final double linearPosition; + + // May be changing DoubleModels when using linear model + private final DoubleModel min, mid, max; + + // Linear multiplier and constant + //private final double linear1; + //private final double linear0; + + // Non-linear multiplier, exponent and constant + private final double quad2, quad1, quad0; + + + + public ValueSliderModel(DoubleModel min, DoubleModel max) { + linearPosition = 1.0; + + this.min = min; + this.mid = max; // Never use exponential scale + this.max = max; + + min.addChangeListener(this); + max.addChangeListener(this); + + quad2 = quad1 = quad0 = 0; // Not used + } + + + + /** + * Generate a linear model from min to max. + */ + public ValueSliderModel(double min, double max) { + linearPosition = 1.0; + + this.min = new DoubleModel(min); + this.mid = new DoubleModel(max); // Never use exponential scale + this.max = new DoubleModel(max); + + quad2 = quad1 = quad0 = 0; // Not used + } + + public ValueSliderModel(double min, double mid, double max) { + this(min, 0.5, mid, max); + } + + /* + * v(x) = mul * x^exp + add + * + * v(pos) = mul * pos^exp + add = mid + * v(1) = mul + add = max + * v'(pos) = mul*exp * pos^(exp-1) = linearMul + */ + public ValueSliderModel(double min, double pos, double mid, double max) { + this.min = new DoubleModel(min); + this.mid = new DoubleModel(mid); + this.max = new DoubleModel(max); + + + linearPosition = pos; + //linear0 = min; + //linear1 = (mid-min)/pos; + + if (!(min < mid && mid <= max && 0 < pos && pos < 1)) { + throw new IllegalArgumentException("Bad arguments for ValueSliderModel " + + "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos); + } + + /* + * quad2..0 are calculated such that + * f(pos) = mid - continuity + * f(1) = max - end point + * f'(pos) = linear1 - continuity of derivative + */ + + double delta = (mid - min) / pos; + quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1); + quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1); + quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1); + + } + + private double pow2(double x) { + return x * x; + } + + @Override + public int getValue() { + double value = DoubleModel.this.getValue(); + if (value <= min.getValue()) + return 0; + if (value >= max.getValue()) + return MAX; + + double x; + if (value <= mid.getValue()) { + // Use linear scale + //linear0 = min; + //linear1 = (mid-min)/pos; + + x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue()); + } else { + // Use quadratic scale + // Further solution of the quadratic equation + // a*x^2 + b*x + c-value == 0 + x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2); + } + return (int) (x * MAX); + } + + + @Override + public void setValue(int newValue) { + if (firing > 0) { + // Ignore loops + log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() + + " value=" + newValue + ", currently firing events"); + return; + } + + double x = (double) newValue / MAX; + double scaledValue; + + if (x <= linearPosition) { + // Use linear scale + //linear0 = min; + //linear1 = (mid-min)/pos; + + scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue(); + } else { + // Use quadratic scale + scaledValue = quad2 * x * x + quad1 * x + quad0; + } + + double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue))); + log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue + + " scaledValue=" + scaledValue + " converted=" + converted); + DoubleModel.this.setValue(converted); + } + + + // Static get-methods + private boolean isAdjusting; + + @Override + public int getExtent() { + return 0; + } + + @Override + public int getMaximum() { + return MAX; + } + + @Override + public int getMinimum() { + return 0; + } + + @Override + public boolean getValueIsAdjusting() { + return isAdjusting; + } + + // Ignore set-values + @Override + public void setExtent(int newExtent) { + } + + @Override + public void setMaximum(int newMaximum) { + } + + @Override + public void setMinimum(int newMinimum) { + } + + @Override + public void setValueIsAdjusting(boolean b) { + isAdjusting = b; + } + + @Override + public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) { + setValueIsAdjusting(adjusting); + setValue(value); + } + + // Pass change listeners to the underlying model + @Override + public void addChangeListener(ChangeListener l) { + DoubleModel.this.addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l) { + DoubleModel.this.removeChangeListener(l); + } + + @Override + public void invalidate() { + DoubleModel.this.invalidate(); + } + + @Override + public void stateChanged(EventObject e) { + // Min or max range has changed. + // Fire if not already firing + if (firing == 0) + fireStateChanged(); + } + } + + + public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) { + return new ValueSliderModel(min, max); + } + + public BoundedRangeModel getSliderModel(double min, double max) { + return new ValueSliderModel(min, max); + } + + public BoundedRangeModel getSliderModel(double min, double mid, double max) { + return new ValueSliderModel(min, mid, max); + } + + public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) { + return new ValueSliderModel(min, pos, mid, max); + } + + + + + + //////////// Action model //////////// + + private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable { + private boolean oldValue = false; + + public AutomaticActionModel() { + oldValue = isAutomatic(); + addChangeListener(this); + } + + + @Override + public boolean isEnabled() { + return isAutomaticAvailable(); + } + + @Override + public Object getValue(String key) { + if (key.equals(Action.SELECTED_KEY)) { + oldValue = isAutomatic(); + return oldValue; + } + return super.getValue(key); + } + + @Override + public void putValue(String key, Object value) { + if (firing > 0) { + log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() + + " key=" + key + " value=" + value + ", currently firing events"); + return; + } + if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) { + log.user("ActionModel putValue called for " + DoubleModel.this.toString() + + " key=" + key + " value=" + value); + oldValue = (Boolean) value; + setAutomatic((Boolean) value); + } else { + log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() + + " key=" + key + " value=" + value); + super.putValue(key, value); + } + } + + // Implement a wrapper to the ChangeListeners + ArrayList propertyChangeListeners = + new ArrayList(); + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + propertyChangeListeners.add(listener); + DoubleModel.this.addChangeListener(this); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + propertyChangeListeners.remove(listener); + if (propertyChangeListeners.isEmpty()) + DoubleModel.this.removeChangeListener(this); + } + + // If the value has changed, generate an event to the listeners + @Override + public void stateChanged(EventObject e) { + boolean newValue = isAutomatic(); + if (oldValue == newValue) + return; + PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY, + oldValue, newValue); + oldValue = newValue; + Object[] l = propertyChangeListeners.toArray(); + for (int i = 0; i < l.length; i++) { + ((PropertyChangeListener) l[i]).propertyChange(event); + } + } + + @Override + public void actionPerformed(ActionEvent e) { + // Setting performed in putValue + } + + @Override + public void invalidate() { + DoubleModel.this.invalidate(); + } + } + + /** + * Returns a new Action corresponding to the changes of the automatic setting + * property of the value model. This may be used directly with e.g. check buttons. + * + * @return A compatibility layer for an Action. + */ + public Action getAutomaticAction() { + return new AutomaticActionModel(); + } + + + + + + //////////// Main model ///////////// + + /* + * The main model handles all values in SI units, i.e. no conversion is made within the model. + */ + + private final ChangeSource source; + private final String valueName; + private final double multiplier; + + private final Method getMethod; + private final Method setMethod; + + private final Method getAutoMethod; + private final Method setAutoMethod; + + private final ArrayList listeners = new ArrayList(); + + private final UnitGroup units; + private Unit currentUnit; + + private final double minValue; + private final double maxValue; + + private String toString = null; + + + private int firing = 0; // >0 when model itself is sending events + + + // Used to differentiate changes in valueName and other changes in the component: + private double lastValue = 0; + private boolean lastAutomatic = false; + + private Invalidator invalidator = new Invalidator(this); + + + /** + * Generate a DoubleModel that contains an internal double value. + * + * @param value the initial value. + */ + public DoubleModel(double value) { + this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + /** + * Generate a DoubleModel that contains an internal double value. + * + * @param value the initial value. + * @param unit the unit for the value. + */ + public DoubleModel(double value, UnitGroup unit) { + this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + /** + * Generate a DoubleModel that contains an internal double value. + * + * @param value the initial value. + * @param unit the unit for the value. + * @param min minimum value. + */ + public DoubleModel(double value, UnitGroup unit, double min) { + this(value, unit, min, Double.POSITIVE_INFINITY); + } + + /** + * Generate a DoubleModel that contains an internal double value. + * + * @param value the initial value. + * @param unit the unit for the value. + * @param min minimum value. + * @param max maximum value. + */ + public DoubleModel(double value, UnitGroup unit, double min, double max) { + this.lastValue = value; + this.minValue = min; + this.maxValue = max; + + source = null; + valueName = "Constant value"; + multiplier = 1; + + getMethod = setMethod = null; + getAutoMethod = setAutoMethod = null; + units = unit; + currentUnit = units.getDefaultUnit(); + } + + + /** + * Generates a new DoubleModel that changes the values of the specified component. + * The double value is read and written using the methods "get"/"set" + valueName. + * + * @param source Component whose parameter to use. + * @param valueName Name of methods used to get/set the parameter. + * @param multiplier Value shown by the model is the value from component.getXXX * multiplier + * @param min Minimum value allowed (in SI units) + * @param max Maximum value allowed (in SI units) + */ + public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit, + double min, double max) { + this.source = source; + this.valueName = valueName; + this.multiplier = multiplier; + + this.units = unit; + currentUnit = units.getDefaultUnit(); + + this.minValue = min; + this.maxValue = max; + + try { + getMethod = source.getClass().getMethod("get" + valueName); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("get method for value '" + valueName + + "' not present in class " + source.getClass().getCanonicalName()); + } + + Method s = null; + try { + s = source.getClass().getMethod("set" + valueName, double.class); + } catch (NoSuchMethodException e1) { + } // Ignore + setMethod = s; + + // Automatic selection methods + + Method set = null, get = null; + + try { + get = source.getClass().getMethod("is" + valueName + "Automatic"); + set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class); + } catch (NoSuchMethodException e) { + } // ignore + + if (set != null && get != null) { + getAutoMethod = get; + setAutoMethod = set; + } else { + getAutoMethod = null; + setAutoMethod = null; + } + + } + + public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit, + double min) { + this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY); + } + + public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) { + this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, + double min, double max) { + this(source, valueName, 1.0, unit, min, max); + } + + public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) { + this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY); + } + + public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) { + this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + public DoubleModel(ChangeSource source, String valueName) { + this(source, valueName, 1.0, UnitGroup.UNITS_NONE, + Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); + } + + public DoubleModel(ChangeSource source, String valueName, double min) { + this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY); + } + + public DoubleModel(ChangeSource source, String valueName, double min, double max) { + this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max); + } + + + + /** + * Returns the value of the variable (in SI units). + */ + public double getValue() { + if (getMethod == null) // Constant value + return lastValue; + + try { + return (Double) getMethod.invoke(source) * multiplier; + } catch (IllegalArgumentException e) { + throw new BugException("Unable to invoke getMethod of " + this, e); + } catch (IllegalAccessException e) { + throw new BugException("Unable to invoke getMethod of " + this, e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + /** + * Sets the value of the variable. + * @param v New value for parameter in SI units. + */ + public void setValue(double v) { + checkState(true); + + log.debug("Setting value " + v + " for " + this); + if (setMethod == null) { + if (getMethod != null) { + throw new BugException("setMethod not available for variable '" + valueName + + "' in class " + source.getClass().getCanonicalName()); + } + lastValue = v; + fireStateChanged(); + return; + } + + try { + setMethod.invoke(source, v / multiplier); + } catch (IllegalArgumentException e) { + throw new BugException("Unable to invoke setMethod of " + this, e); + } catch (IllegalAccessException e) { + throw new BugException("Unable to invoke setMethod of " + this, e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + + /** + * Returns whether setting the value automatically is available. + */ + public boolean isAutomaticAvailable() { + return (getAutoMethod != null) && (setAutoMethod != null); + } + + /** + * Returns whether the value is currently being set automatically. + * Returns false if automatic setting is not available at all. + */ + public boolean isAutomatic() { + if (getAutoMethod == null) + return false; + + try { + return (Boolean) getAutoMethod.invoke(source); + } catch (IllegalArgumentException e) { + throw new BugException("Method call failed", e); + } catch (IllegalAccessException e) { + throw new BugException("Method call failed", e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + /** + * Sets whether the value should be set automatically. Simply fires a + * state change event if automatic setting is not available. + */ + public void setAutomatic(boolean auto) { + checkState(true); + + if (setAutoMethod == null) { + log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available"); + fireStateChanged(); // in case something is out-of-sync + return; + } + + log.debug("Setting automatic to " + auto + " for " + this); + lastAutomatic = auto; + try { + setAutoMethod.invoke(source, auto); + } catch (IllegalArgumentException e) { + throw new BugException(e); + } catch (IllegalAccessException e) { + throw new BugException(e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + + /** + * Returns the current Unit. At the beginning it is the default unit of the UnitGroup. + * @return The most recently set unit. + */ + public Unit getCurrentUnit() { + return currentUnit; + } + + /** + * Sets the current Unit. The unit must be one of those included in the UnitGroup. + * @param u The unit to set active. + */ + public void setCurrentUnit(Unit u) { + checkState(true); + if (currentUnit == u) + return; + log.debug("Setting unit for " + this + " to '" + u + "'"); + currentUnit = u; + fireStateChanged(); + } + + + /** + * Returns the UnitGroup associated with the parameter value. + * + * @return The UnitGroup given to the constructor. + */ + public UnitGroup getUnitGroup() { + return units; + } + + + + /** + * Add a listener to the model. Adds the model as a listener to the value source if this + * is the first listener. + * @param l Listener to add. + */ + @Override + public void addChangeListener(EventListener l) { + checkState(true); + + if (listeners.isEmpty()) { + if (source != null) { + source.addChangeListener(this); + lastValue = getValue(); + lastAutomatic = isAutomatic(); + } + } + + listeners.add(l); + log.verbose(this + " adding listener (total " + listeners.size() + "): " + l); + } + + /** + * Remove a listener from the model. Removes the model from being a listener to the Component + * if this was the last listener of the model. + * @param l Listener to remove. + */ + @Override + public void removeChangeListener(EventListener l) { + checkState(false); + + listeners.remove(l); + if (listeners.isEmpty() && source != null) { + source.removeChangeListener(this); + } + log.verbose(this + " removing listener (total " + listeners.size() + "): " + l); + } + + + /** + * Invalidates this model by removing all listeners and removing this from + * listening to the source. After invalidation no listeners can be added to this + * model and the value cannot be set. + */ + @Override + public void invalidate() { + log.verbose("Invalidating " + this); + invalidator.invalidate(); + + if (!listeners.isEmpty()) { + log.warn("Invalidating " + this + " while still having listeners " + listeners); + } + listeners.clear(); + if (source != null) { + source.removeChangeListener(this); + } + MemoryManagement.collectable(this); + } + + + private void checkState(boolean error) { + invalidator.check(error); + } + + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (!listeners.isEmpty()) { + log.warn(this + " being garbage-collected while having listeners " + listeners); + } + }; + + + /** + * Fire a ChangeEvent to all listeners. + */ + protected void fireStateChanged() { + checkState(true); + + EventObject event = new EventObject(this); + ChangeEvent cevent = new ChangeEvent(this); + firing++; + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] ls = listeners.toArray(new EventListener[0]); + for (EventListener l : ls) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(event); + } else if ( l instanceof ChangeListener ) { + ((ChangeListener)l).stateChanged(cevent); + } + } + firing--; + } + + /** + * Called when the component changes. Checks whether the modeled value has changed, and if + * it has, updates lastValue and generates ChangeEvents for all listeners of the model. + */ + @Override + public void stateChanged(EventObject e) { + checkState(true); + + double v = getValue(); + boolean b = isAutomatic(); + if (lastValue == v && lastAutomatic == b) + return; + lastValue = v; + lastAutomatic = b; + fireStateChanged(); + } + + + /** + * Explain the DoubleModel as a String. + */ + @Override + public String toString() { + if (toString == null) { + if (source == null) { + toString = "DoubleModel[constant=" + lastValue + "]"; + } else { + toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; + } + } + return toString; + } +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/EnumModel.java b/core/src/net/sf/openrocket/gui/adaptors/EnumModel.java new file mode 100644 index 00000000..a6757a38 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/EnumModel.java @@ -0,0 +1,133 @@ +package net.sf.openrocket.gui.adaptors; + +import java.util.EventObject; + +import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; + +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.StateChangeListener; + + +public class EnumModel> extends AbstractListModel + implements ComboBoxModel, StateChangeListener { + + private final ChangeSource source; + private final String valueName; + private final String nullText; + + private final Enum[] values; + private Enum currentValue = null; + + private final Reflection.Method getMethod; + private final Reflection.Method setMethod; + + + + public EnumModel(ChangeSource source, String valueName) { + this(source,valueName,null,null); + } + + public EnumModel(ChangeSource source, String valueName, Enum[] values) { + this(source, valueName, values, null); + } + + @SuppressWarnings("unchecked") + public EnumModel(ChangeSource source, String valueName, Enum[] values, String nullText) { + Class> enumClass; + this.source = source; + this.valueName = valueName; + + try { + java.lang.reflect.Method getM = source.getClass().getMethod("get" + valueName); + enumClass = (Class>) getM.getReturnType(); + if (!enumClass.isEnum()) { + throw new IllegalArgumentException("Return type of get" + valueName + + " not an enum type"); + } + + getMethod = new Reflection.Method(getM); + setMethod = new Reflection.Method(source.getClass().getMethod("set" + valueName, + enumClass)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("get/is methods for enum '"+valueName+ + "' not present in class "+source.getClass().getCanonicalName()); + } + + if (values != null) + this.values = values; + else + this.values = enumClass.getEnumConstants(); + + this.nullText = nullText; + + stateChanged(null); // Update current value + source.addChangeListener(this); + } + + + + @Override + public Object getSelectedItem() { + if (currentValue==null) + return nullText; + return currentValue; + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + // Clear selection - huh? + return; + } + if (item instanceof String) { + if (currentValue != null) + setMethod.invoke(source, (Object)null); + return; + } + + if (!(item instanceof Enum)) { + throw new IllegalArgumentException("Not String or Enum, item="+item); + } + + // Comparison with == ok, since both are enums + if (currentValue == item) + return; + setMethod.invoke(source, item); + } + + @Override + public Object getElementAt(int index) { + if (values[index] == null) + return nullText; + return values[index]; + } + + @Override + public int getSize() { + return values.length; + } + + + + + + @SuppressWarnings("unchecked") + @Override + public void stateChanged(EventObject e) { + Enum value = (Enum) getMethod.invoke(source); + if (value != currentValue) { + currentValue = value; + this.fireContentsChanged(this, 0, values.length); + } + } + + + + @Override + public String toString() { + return "EnumModel["+source.getClass().getCanonicalName()+":"+valueName+"]"; + } + +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/IntegerModel.java b/core/src/net/sf/openrocket/gui/adaptors/IntegerModel.java new file mode 100644 index 00000000..e948918a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/IntegerModel.java @@ -0,0 +1,262 @@ +package net.sf.openrocket.gui.adaptors; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.EventListener; +import java.util.EventObject; + +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.StateChangeListener; + + +public class IntegerModel implements StateChangeListener { + private static final LogHelper log = Application.getLogger(); + + + //////////// JSpinner Model //////////// + + private class IntegerSpinnerModel extends SpinnerNumberModel { + @Override + public Object getValue() { + return IntegerModel.this.getValue(); + } + + @Override + public void setValue(Object value) { + if (firing > 0) { + // Ignore, if called when model is sending events + log.verbose("Ignoring call to SpinnerModel setValue for " + IntegerModel.this.toString() + + " value=" + value + ", currently firing events"); + return; + + } + Number num = (Number) value; + int newValue = num.intValue(); + log.user("SpinnerModel setValue called for " + IntegerModel.this.toString() + " newValue=" + newValue); + IntegerModel.this.setValue(newValue); + } + + @Override + public Object getNextValue() { + int d = IntegerModel.this.getValue(); + if (d >= maxValue) + return null; + return (d + 1); + } + + @Override + public Object getPreviousValue() { + int d = IntegerModel.this.getValue(); + if (d <= minValue) + return null; + return (d - 1); + } + + @Override + public void addChangeListener(ChangeListener l) { + IntegerModel.this.addChangeListener(l); + } + + @Override + public void removeChangeListener(ChangeListener l) { + IntegerModel.this.removeChangeListener(l); + } + } + + /** + * Returns a new SpinnerModel with the same base as the DoubleModel. + * The values given to the JSpinner are in the currently selected units. + * + * @return A compatibility layer for a SpinnerModel. + */ + public SpinnerModel getSpinnerModel() { + return new IntegerSpinnerModel(); + } + + + + + //////////// Main model ///////////// + + /* + * The main model handles all values in SI units, i.e. no conversion is made within the model. + */ + + private final ChangeSource source; + private final String valueName; + + private final Method getMethod; + private final Method setMethod; + + private final ArrayList listeners = new ArrayList(); + + private final int minValue; + private final int maxValue; + + private String toString = null; + + + private int firing = 0; // >0 when model itself is sending events + + + // Used to differentiate changes in valueName and other changes in the source: + private int lastValue = 0; + + + + /** + * Generates a new DoubleModel that changes the values of the specified source. + * The double value is read and written using the methods "get"/"set" + valueName. + * + * @param source Component whose parameter to use. + * @param valueName Name of methods used to get/set the parameter. + * @param min Minimum value allowed (in SI units) + * @param max Maximum value allowed (in SI units) + */ + public IntegerModel(ChangeSource source, String valueName, int min, int max) { + this.source = source; + this.valueName = valueName; + + this.minValue = min; + this.maxValue = max; + + try { + getMethod = source.getClass().getMethod("get" + valueName); + setMethod = source.getClass().getMethod("set" + valueName, int.class); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("get/set methods for value '" + valueName + + "' not present in class " + source.getClass().getCanonicalName()); + } + } + + public IntegerModel(ChangeSource source, String valueName, int min) { + this(source, valueName, min, Integer.MAX_VALUE); + } + + public IntegerModel(ChangeSource source, String valueName) { + this(source, valueName, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + + + + /** + * Returns the value of the variable. + */ + public int getValue() { + try { + return (Integer) getMethod.invoke(source); + } catch (IllegalArgumentException e) { + throw new BugException(e); + } catch (IllegalAccessException e) { + throw new BugException(e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + /** + * Sets the value of the variable. + */ + public void setValue(int v) { + log.debug("Setting value " + v + " for " + this); + try { + setMethod.invoke(source, v); + } catch (IllegalArgumentException e) { + throw new BugException(e); + } catch (IllegalAccessException e) { + throw new BugException(e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + + /** + * Add a listener to the model. Adds the model as a listener to the Component if this + * is the first listener. + * @param l Listener to add. + */ + public void addChangeListener(EventListener l) { + if (listeners.isEmpty()) { + source.addChangeListener(this); + lastValue = getValue(); + } + + listeners.add(l); + log.verbose(this + " adding listener (total " + listeners.size() + "): " + l); + } + + /** + * Remove a listener from the model. Removes the model from being a listener to the Component + * if this was the last listener of the model. + * @param l Listener to remove. + */ + public void removeChangeListener(ChangeListener l) { + listeners.remove(l); + if (listeners.isEmpty()) { + source.removeChangeListener(this); + } + log.verbose(this + " removing listener (total " + listeners.size() + "): " + l); + } + + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (!listeners.isEmpty()) { + log.warn(this + " being garbage-collected while having listeners " + listeners); + } + }; + + + public void fireStateChanged() { + EventListener[] list = listeners.toArray(new EventListener[0] ); + EventObject event = new EventObject(this); + ChangeEvent cevent = new ChangeEvent(this); + firing++; + for( EventListener l : list ) { + if ( l instanceof ChangeListener) { + ((ChangeListener)l).stateChanged(cevent); + } else if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(event); + } + } + firing--; + } + + /** + * Called when the source changes. Checks whether the modeled value has changed, and if + * it has, updates lastValue and generates ChangeEvents for all listeners of the model. + */ + @Override + public void stateChanged(EventObject e) { + int v = getValue(); + if (lastValue == v) + return; + lastValue = v; + fireStateChanged(); + } + + /** + * Explain the DoubleModel as a String. + */ + @Override + public String toString() { + if (toString == null) { + toString = "IntegerModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; + } + return toString; + } + +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/MaterialModel.java b/core/src/net/sf/openrocket/gui/adaptors/MaterialModel.java new file mode 100644 index 00000000..a914df00 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/MaterialModel.java @@ -0,0 +1,165 @@ +package net.sf.openrocket.gui.adaptors; + + +import java.awt.Component; + +import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; +import javax.swing.SwingUtilities; + +import net.sf.openrocket.database.Database; +import net.sf.openrocket.database.DatabaseListener; +import net.sf.openrocket.database.Databases; +import net.sf.openrocket.gui.dialogs.CustomMaterialDialog; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Reflection; + +public class MaterialModel extends AbstractListModel implements + ComboBoxModel, ComponentChangeListener, DatabaseListener { + + private static final String CUSTOM = "Custom"; + + + private final Component parentComponent; + + private final RocketComponent component; + private final Material.Type type; + private final Database database; + + private final Reflection.Method getMethod; + private final Reflection.Method setMethod; + private static final Translator trans = Application.getTranslator(); + + + public MaterialModel(Component parent, RocketComponent component, Material.Type type) { + //// Material + //this(parent, component, type, trans.get("MaterialModel.title.Material")); + this(parent, component, type, "Material"); + } + + public MaterialModel(Component parent, RocketComponent component, Material.Type type, + String name) { + this.parentComponent = parent; + this.component = component; + this.type = type; + + switch (type) { + case LINE: + this.database = Databases.LINE_MATERIAL; + break; + + case BULK: + this.database = Databases.BULK_MATERIAL; + break; + + case SURFACE: + this.database = Databases.SURFACE_MATERIAL; + break; + + default: + throw new IllegalArgumentException("Unknown material type:"+type); + } + + try { + getMethod = new Reflection.Method(component.getClass().getMethod("get"+name)); + setMethod = new Reflection.Method(component.getClass().getMethod("set"+name, + Material.class)); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("get/is methods for material " + + "not present in class "+component.getClass().getCanonicalName()); + } + + component.addComponentChangeListener(this); + database.addDatabaseListener(this); + } + + @Override + public Object getSelectedItem() { + return getMethod.invoke(component); + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + // Clear selection - huh? + return; + } + + if (item == CUSTOM) { + + // Open custom material dialog in the future, after combo box has closed + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + CustomMaterialDialog dialog = new CustomMaterialDialog( + SwingUtilities.getWindowAncestor(parentComponent), + (Material) getSelectedItem(), true, + //// Define custom material + trans.get("MaterialModel.title.Defcustmat")); + + dialog.setVisible(true); + + if (!dialog.getOkClicked()) + return; + + Material material = dialog.getMaterial(); + setMethod.invoke(component, material); + + if (dialog.isAddSelected()) { + database.add(material); + } + } + }); + + } else if (item instanceof Material) { + + setMethod.invoke(component, item); + + } else { + throw new IllegalArgumentException("Illegal item class " + item.getClass() + + " item=" + item); + } + } + + @Override + public Object getElementAt(int index) { + if (index == database.size()) { + return CUSTOM; + } else if (index >= database.size()+1) { + return null; + } + return database.get(index); + } + + @Override + public int getSize() { + return database.size() + 1; + } + + + + //////// Change listeners + + @Override + public void componentChanged(ComponentChangeEvent e) { + if (((ComponentChangeEvent)e).isMassChange()) { + this.fireContentsChanged(this, 0, 0); + } + } + + @Override + public void elementAdded(Material element, Database source) { + this.fireContentsChanged(this, 0, database.size()); + } + + @Override + public void elementRemoved(Material element, Database source) { + this.fireContentsChanged(this, 0, database.size()); + } + +} diff --git a/core/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java b/core/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java new file mode 100644 index 00000000..57852699 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java @@ -0,0 +1,170 @@ +package net.sf.openrocket.gui.adaptors; + + +import java.util.EventObject; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.ComboBoxModel; +import javax.swing.SwingUtilities; +import javax.swing.event.EventListenerList; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; + +import net.sf.openrocket.gui.dialogs.EditMotorConfigurationDialog; +import net.sf.openrocket.gui.main.BasicFrame; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.StateChangeListener; + +public class MotorConfigurationModel implements ComboBoxModel, StateChangeListener { + private static final Translator trans = Application.getTranslator(); + + private static final String EDIT = trans.get("MotorCfgModel.Editcfg"); + + + private EventListenerList listenerList = new EventListenerList(); + + private final Configuration config; + private final Rocket rocket; + + private Map map = new HashMap(); + + + public MotorConfigurationModel(Configuration config) { + this.config = config; + this.rocket = config.getRocket(); + config.addChangeListener(this); + } + + + + @Override + public Object getElementAt(int index) { + String[] ids = rocket.getMotorConfigurationIDs(); + if (index < 0 || index > ids.length) + return null; + + if (index == ids.length) + return EDIT; + + return get(ids[index]); + } + + @Override + public int getSize() { + return rocket.getMotorConfigurationIDs().length + 1; + } + + @Override + public Object getSelectedItem() { + return get(config.getMotorConfigurationID()); + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + // Clear selection - huh? + return; + } + if (item == EDIT) { + + // Open edit dialog in the future, after combo box has closed + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + new EditMotorConfigurationDialog(rocket, BasicFrame.findFrame(rocket)) + .setVisible(true); + } + }); + + return; + } + if (!(item instanceof ID)) { + throw new IllegalArgumentException("MotorConfigurationModel item="+item); + } + + ID idObject = (ID) item; + config.setMotorConfigurationID(idObject.getID()); + } + + + + //////////////// Event/listener handling //////////////// + + + @Override + public void addListDataListener(ListDataListener l) { + listenerList.add(ListDataListener.class, l); + } + + @Override + public void removeListDataListener(ListDataListener l) { + listenerList.remove(ListDataListener.class, l); + } + + protected void fireListDataEvent() { + Object[] listeners = listenerList.getListenerList(); + ListDataEvent e = null; + + for (int i = listeners.length-2; i>=0; i-=2) { + if (listeners[i] == ListDataListener.class) { + if (e == null) + e = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize()); + ((ListDataListener) listeners[i+1]).contentsChanged(e); + } + } + } + + + @Override + public void stateChanged(EventObject e) { + if (e instanceof ComponentChangeEvent) { + // Ignore unnecessary changes + if (!((ComponentChangeEvent)e).isMotorChange()) + return; + } + fireListDataEvent(); + } + + + + /* + * The ID class is an adapter, that contains the actual configuration ID, + * but gives the configuration description as its String representation. + * The get(id) method retrieves ID objects and caches them for reuse. + */ + + private ID get(String id) { + ID idObject = map.get(id); + if (idObject != null) + return idObject; + + idObject = new ID(id); + map.put(id, idObject); + return idObject; + } + + + private class ID { + private final String id; + + public ID(String id) { + this.id = id; + } + + public String getID() { + return id; + } + + @Override + public String toString() { + return rocket.getMotorConfigurationNameOrDescription(id); + } + } + +} + diff --git a/core/src/net/sf/openrocket/gui/components/BasicSlider.java b/core/src/net/sf/openrocket/gui/components/BasicSlider.java new file mode 100644 index 00000000..0ac92e79 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/BasicSlider.java @@ -0,0 +1,31 @@ +package net.sf.openrocket.gui.components; + +import javax.swing.BoundedRangeModel; +import javax.swing.JSlider; +import javax.swing.plaf.basic.BasicSliderUI; + +/** + * A simple slider that does not show the current value. GTK l&f shows the value, and cannot + * be configured otherwise(!). + * + * @author Sampo Niskanen + */ + +public class BasicSlider extends JSlider { + + public BasicSlider(BoundedRangeModel brm) { + this(brm,JSlider.HORIZONTAL,false); + } + + public BasicSlider(BoundedRangeModel brm, int orientation) { + this(brm,orientation,false); + } + + public BasicSlider(BoundedRangeModel brm, int orientation, boolean inverted) { + super(brm); + setOrientation(orientation); + setInverted(inverted); + setUI(new BasicSliderUI(this)); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/BasicTree.java b/core/src/net/sf/openrocket/gui/components/BasicTree.java new file mode 100644 index 00000000..6ec8804e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/BasicTree.java @@ -0,0 +1,145 @@ +package net.sf.openrocket.gui.components; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import javax.swing.Icon; +import javax.swing.JTree; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +public class BasicTree extends JTree { + + + public BasicTree() { + super(); + setDefaultOptions(); + } + + public BasicTree(TreeNode node) { + super(node); + setDefaultOptions(); + } + + + private void setDefaultOptions() { + this.setToggleClickCount(0); + + javax.swing.plaf.basic.BasicTreeUI plainUI = new javax.swing.plaf.basic.BasicTreeUI(); + this.setUI(plainUI); + plainUI.setExpandedIcon(TreeIcon.MINUS); + plainUI.setCollapsedIcon(TreeIcon.PLUS); + plainUI.setLeftChildIndent(15); + + + this.setBackground(Color.WHITE); + this.setShowsRootHandles(false); + } + + + /** + * Expand the entire tree structure. All nodes will be visible after the call. + */ + public void expandTree() { + for (int i = 0; i < getRowCount(); i++) + expandRow(i); + } + + + + + @Override + public void treeDidChange() { + super.treeDidChange(); + /* + * Expand the childless nodes to prevent leaf nodes from looking expandable. + */ + expandChildlessNodes(); + } + + /** + * Expand all nodes in the tree that are visible and have no children. This can be used + * to avoid the situation where a non-leaf node is marked as being expandable, but when + * expanding it it has no children. + */ + private void expandChildlessNodes() { + TreeModel model = this.getModel(); + if (model == null) { + return; + } + Object root = model.getRoot(); + expandChildlessNodes(model, new TreePath(root)); + } + + private void expandChildlessNodes(TreeModel model, TreePath path) { + Object object = path.getLastPathComponent(); + if (this.isVisible(path)) { + int count = model.getChildCount(object); + if (count == 0) { + this.expandPath(path); + } + for (int i = 0; i < count; i++) { + expandChildlessNodes(model, path.pathByAddingChild(model.getChild(object, i))); + } + } + } + + + + /** + * Plain-looking tree expand/collapse icons. + */ + private static class TreeIcon implements Icon { + public static final Icon PLUS = new TreeIcon(true); + public static final Icon MINUS = new TreeIcon(false); + + // Implementation: + + private final static int width = 9; + private final static int height = 9; + private final static BasicStroke stroke = new BasicStroke(2); + private boolean plus; + + private TreeIcon(boolean plus) { + this.plus = plus; + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) { + Graphics2D g2 = (Graphics2D) g.create(); + + // Background + g2.setColor(Color.WHITE); + g2.fillRect(x, y, width, height); + + // Border + g2.setColor(Color.DARK_GRAY); + g2.drawRect(x, y, width, height); + + // Horizontal stroke + g2.setStroke(stroke); + g2.drawLine(x + 3, y + (height + 1) / 2, x + width - 2, y + (height + 1) / 2); + + // Vertical stroke + if (plus) { + g2.drawLine(x + (width + 1) / 2, y + 3, x + (width + 1) / 2, y + height - 2); + } + + g2.dispose(); + } + + @Override + public int getIconWidth() { + return width; + } + + @Override + public int getIconHeight() { + return height; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/components/CollectionTable.java b/core/src/net/sf/openrocket/gui/components/CollectionTable.java new file mode 100644 index 00000000..ef14dc61 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/CollectionTable.java @@ -0,0 +1,75 @@ +package net.sf.openrocket.gui.components; + +import javax.swing.JTable; +import javax.swing.table.AbstractTableModel; + + +/* + * TODO: LOW: This is currently unused. + */ +public abstract class CollectionTable extends JTable { + + private final String[] columnNames; + private CollectionTableModel model; + + + protected CollectionTable(String[] columnNames) { + this.columnNames = columnNames.clone(); + } + + + protected void initializeTable() { + model = new CollectionTableModel(); + this.setModel(model); + } + + + /** + * Retrieve the object for the specified row number. + * + * @param row the row number being queried. + * @return the object at that row. + */ + protected abstract T getModelObjectAt(int row); + + protected abstract int getModelRowCount(); + + + + protected abstract Object getViewForModelObject(T object, int column); + + protected Class getViewColumnClass(int column) { + return Object.class; + } + + + + private class CollectionTableModel extends AbstractTableModel { + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public String getColumnName(int column) { + return columnNames[column]; + } + + @Override + public Class getColumnClass(int column) { + return getViewColumnClass(column); + } + + + @Override + public int getRowCount() { + return getModelRowCount(); + } + + @Override + public Object getValueAt(int row, int column) { + T value = getModelObjectAt(row); + return getViewForModelObject(value, column); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/components/ColorChooser.java b/core/src/net/sf/openrocket/gui/components/ColorChooser.java new file mode 100644 index 00000000..e149e3ca --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/ColorChooser.java @@ -0,0 +1,104 @@ +/* + * ColorChooser.java + */ +package net.sf.openrocket.gui.components; + +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import javax.swing.colorchooser.ColorSelectionModel; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * A panel implementation of a color chooser. The panel has a label and a textfield. The label identifies + * what the color is to be used for (the purpose), and the textfield is uneditable but has its background set + * to the currently chosen color as a way of visualizing the color. + * + * The chosen color may be retrieved via a call to getCurrentColor. + */ +public class ColorChooser extends JPanel { + + private static final String COLOR_CHOOSER_BUTTON_LABEL = "Color"; + + // The currently selected color + private Color curColor; + + final JColorChooser chooser; + final JLabel label; + final JTextField field; + final JPanel p; + + /** + * Construct a color chooser as a panel. + * l + * @param parent the parent panel to which this component will be added + * @param theChooser the delegated color chooser; the initial color taken from this chooser + * @param theLabel the label used as the 'purpose' of the color; placed next to a textfield + */ + public ColorChooser (JPanel parent, JColorChooser theChooser, final String theLabel) { + p = parent; + chooser = theChooser; + chooser.setPreviewPanel(this); + // Initialize the currently selected color + curColor = chooser.getColor(); + label = new JLabel(theLabel + ":"); + + parent.add(label, "align right"); + field = new JTextField(); + field.setEditable(false); + field.setBackground(curColor); + parent.add(field, "width 50:100:100"); + + final JButton button = new JButton(COLOR_CHOOSER_BUTTON_LABEL); + + ActionListener actionListener = new ActionListener() { + public void actionPerformed (ActionEvent actionEvent) { + chooser.updateUI(); + + final JDialog dialog = JColorChooser.createDialog(null, + theLabel, true, + chooser, + null, null); + + // Wait until current event dispatching completes before showing + // dialog + Runnable showDialog = new Runnable() { + public void run () { + dialog.show(); + } + }; + SwingUtilities.invokeLater(showDialog); + } + }; + button.addActionListener(actionListener); + parent.add(button, "wrap"); + + // Add listener on model to detect changes to selected color + ColorSelectionModel model = chooser.getSelectionModel(); + model.addChangeListener(new ChangeListener() { + public void stateChanged (ChangeEvent evt) { + ColorSelectionModel model = (ColorSelectionModel) evt.getSource(); + // Get the new color value + curColor = model.getSelectedColor(); + field.setBackground(curColor); + } + }); + } + + /** + * Get the user-selected color. + * + * @return the current color + */ + public Color getCurrentColor () { + return curColor; + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/components/ColorChooserButton.java b/core/src/net/sf/openrocket/gui/components/ColorChooserButton.java new file mode 100644 index 00000000..15e4adc6 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/ColorChooserButton.java @@ -0,0 +1,71 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeListener; + +import javax.swing.JButton; +import javax.swing.JColorChooser; +import javax.swing.JDialog; +import javax.swing.JPanel; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * A color chooser button. The currently selected color can be queried or set using the + * {@link #getSelectedColor()} and {@link #setSelectedColor(Color)}, and changes listened + * to by listening to property events with property name {@link #COLOR_KEY}. + * + * @author Sampo Niskanen + */ +public class ColorChooserButton extends JButton { + private static final LogHelper log = Application.getLogger(); + + public static final String COLOR_KEY = "selectedColor"; + + + public ColorChooserButton(Color initial) { + + setSelectedColor(initial); + + // Add action listener that opens color chooser dialog + this.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Activating color chooser"); + final JColorChooser chooser = new JColorChooser(getSelectedColor()); + chooser.setPreviewPanel(new JPanel()); + final JDialog dialog = JColorChooser.createDialog(ColorChooserButton.this, "Select color", true, + chooser, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e2) { + Color c = chooser.getColor(); + log.user("User selected color " + c); + setSelectedColor(chooser.getColor()); + } + }, null); + log.info("Closing color chooser"); + dialog.setVisible(true); + } + }); + + } + + + public void addColorPropertyChangeListener(PropertyChangeListener listener) { + this.addPropertyChangeListener(COLOR_KEY, listener); + } + + public void setSelectedColor(Color c) { + log.debug("Selecting color " + c); + this.setIcon(new ColorIcon(c)); + this.putClientProperty(COLOR_KEY, c); + } + + public Color getSelectedColor() { + return (Color) this.getClientProperty(COLOR_KEY); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/ColorIcon.java b/core/src/net/sf/openrocket/gui/components/ColorIcon.java new file mode 100644 index 00000000..747a4268 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/ColorIcon.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Color; + +import javax.swing.Icon; + +/** + * An Icon that displays a specific color, suitable for drawing into a button. + * + * @author Sampo Niskanen + */ +public class ColorIcon implements Icon { + private final Color color; + + public ColorIcon(Color c) { + this.color = c; + } + + @Override + public int getIconHeight() { + return 15; + } + + @Override + public int getIconWidth() { + return 25; + } + + @Override + public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) { + g.setColor(color); + g.fill3DRect(x, y, getIconWidth(), getIconHeight(), false); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/CsvOptionPanel.java b/core/src/net/sf/openrocket/gui/components/CsvOptionPanel.java new file mode 100644 index 00000000..cd979e16 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/CsvOptionPanel.java @@ -0,0 +1,126 @@ +package net.sf.openrocket.gui.components; + +import javax.swing.BorderFactory; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; + +/** + * A panel that shows options for saving CSV files. + * + * @author Sampo Niskanen + */ +public class CsvOptionPanel extends JPanel { + + private static final Translator trans = Application.getTranslator(); + + private static final String SPACE = trans.get("CsvOptionPanel.separator.space"); + private static final String TAB = trans.get("CsvOptionPanel.separator.tab"); + + private final String baseClassName; + + private final JComboBox fieldSeparator; + private final JCheckBox[] options; + private final JComboBox commentCharacter; + + /** + * Sole constructor. + * + * @param includeComments a list of comment inclusion options to provide; + * every second item is the option name and every second the tooltip + */ + public CsvOptionPanel(Class baseClass, String... includeComments) { + super(new MigLayout("fill, insets 0")); + + this.baseClassName = baseClass.getSimpleName(); + + JPanel panel; + JLabel label; + String tip; + + + // TODO: HIGH: Rename the translation keys + + // Field separator panel + panel = new JPanel(new MigLayout("fill")); + panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Fieldsep"))); + + label = new JLabel(trans.get("SimExpPan.lbl.Fieldsepstr")); + tip = trans.get("SimExpPan.lbl.longA1") + + trans.get("SimExpPan.lbl.longA2"); + label.setToolTipText(tip); + panel.add(label, "gapright unrel"); + + fieldSeparator = new JComboBox(new String[] { ",", ";", SPACE, TAB }); + fieldSeparator.setEditable(true); + fieldSeparator.setSelectedItem(Application.getPreferences().getString(Preferences.EXPORT_FIELD_SEPARATOR, ",")); + fieldSeparator.setToolTipText(tip); + panel.add(fieldSeparator, "growx"); + + this.add(panel, "growx, wrap unrel"); + + + + // Comments separator panel + panel = new JPanel(new MigLayout("fill")); + panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Comments"))); + + + // List of include comments options + if (includeComments.length % 2 == 1) { + throw new IllegalArgumentException("Invalid argument length, must be even, length=" + includeComments.length); + } + options = new JCheckBox[includeComments.length / 2]; + for (int i = 0; i < includeComments.length / 2; i++) { + options[i] = new JCheckBox(includeComments[i * 2]); + options[i].setToolTipText(includeComments[i * 2 + 1]); + options[i].setSelected(Application.getPreferences().getBoolean("csvOptions." + baseClassName + "." + i, true)); + panel.add(options[i], "wrap"); + } + + + label = new JLabel(trans.get("SimExpPan.lbl.Commentchar")); + tip = trans.get("SimExpPan.lbl.ttip.Commentchar"); + label.setToolTipText(tip); + panel.add(label, "split 2, gapright unrel"); + + commentCharacter = new JComboBox(new String[] { "#", "%", ";" }); + commentCharacter.setEditable(true); + commentCharacter.setSelectedItem(Application.getPreferences().getString(Preferences.EXPORT_COMMENT_CHARACTER, "#")); + commentCharacter.setToolTipText(tip); + panel.add(commentCharacter, "growx"); + + this.add(panel, "growx, wrap"); + } + + + public String getFieldSeparator() { + return fieldSeparator.getSelectedItem().toString(); + } + + public String getCommentCharacter() { + return commentCharacter.getSelectedItem().toString(); + } + + public boolean getSelectionOption(int index) { + return options[index].isSelected(); + } + + /** + * Store the selected options to the user preferences. + */ + public void storePreferences() { + Application.getPreferences().putString(Preferences.EXPORT_FIELD_SEPARATOR, getFieldSeparator()); + Application.getPreferences().putString(Preferences.EXPORT_COMMENT_CHARACTER, getCommentCharacter()); + for (int i = 0; i < options.length; i++) { + Application.getPreferences().putBoolean("csvOptions." + baseClassName + "." + i, options[i].isSelected()); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/DescriptionArea.java b/core/src/net/sf/openrocket/gui/components/DescriptionArea.java new file mode 100644 index 00000000..1e37bb6f --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/DescriptionArea.java @@ -0,0 +1,106 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Rectangle; + +import javax.swing.JEditorPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingUtilities; + +public class DescriptionArea extends JScrollPane { + + private final JEditorPane editorPane; + + + /** + * Construct a description area with the specified number of rows, default description font size, + * being opaque. + * + * @param rows the number of rows + */ + public DescriptionArea(int rows) { + this("", rows, -1); + } + + /** + * Construct a description area with the specified number of rows and size, being opaque. + * + * @param rows the number of rows. + * @param size the font size difference compared to the default font size. + */ + public DescriptionArea(int rows, float size) { + this("", rows, size); + } + + /** + * Construct an opaque description area with the specified number of rows, size and text, being opaque. + * + * @param text the initial text. + * @param rows the number of rows. + * @param size the font size difference compared to the default font size. + */ + public DescriptionArea(String text, int rows, float size) { + this(text, rows, size, true); + } + + /** + * Constructor with all options. + * + * @param text the text for the description area. + * @param rows the number of rows to set + * @param size the relative font size in points (positive or negative) + * @param opaque if false the background color will be set to the background color + * of a default JPanel (simulation non-opaque) + */ + public DescriptionArea(String text, int rows, float size, boolean opaque) { + super(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + + editorPane = new JEditorPane("text/html", ""); + Font font = editorPane.getFont(); + editorPane.setFont(font.deriveFont(font.getSize2D() + size)); + editorPane.setEditable(false); + + if (!opaque) { + Color bg = new JPanel().getBackground(); + editorPane.setBackground(new Color(bg.getRed(), bg.getGreen(), bg.getBlue())); + this.setOpaque(true); + } + + // Calculate correct height + editorPane.setText("abc"); + Dimension oneline = editorPane.getPreferredSize(); + editorPane.setText("abc
def"); + Dimension twolines = editorPane.getPreferredSize(); + editorPane.setText(""); + + int lineheight = twolines.height - oneline.height; + int extraheight = oneline.height - lineheight; + + Dimension dim = editorPane.getPreferredSize(); + dim.height = lineheight * rows + extraheight + 2; + this.setPreferredSize(dim); + + this.setViewportView(editorPane); + this.setText(text); + } + + public void setText(String txt) { + editorPane.setText(txt); + editorPane.revalidate(); + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + editorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1)); + } + + }); + editorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1)); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/DoubleCellEditor.java b/core/src/net/sf/openrocket/gui/components/DoubleCellEditor.java new file mode 100644 index 00000000..1602350a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/DoubleCellEditor.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Component; +import java.text.ParseException; + +import javax.swing.AbstractCellEditor; +import javax.swing.JSpinner; +import javax.swing.JTable; +import javax.swing.table.TableCellEditor; + +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; + +public class DoubleCellEditor extends AbstractCellEditor implements TableCellEditor { + + private final JSpinner editor; + private final DoubleModel model; + + public DoubleCellEditor() { + model = new DoubleModel(0); + editor = new JSpinner(model.getSpinnerModel()); + editor.setEditor(new SpinnerEditor(editor)); + // editor.addChangeListener(this); + } + + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, + boolean isSelected, int row, int column) { + + double val = (Double) value; + model.setValue(val); + + return editor; + } + + + @Override + public boolean stopCellEditing() { + try { + editor.commitEdit(); + } catch (ParseException e) { + // Ignore + } + return super.stopCellEditing(); + } + + + @Override + public Object getCellEditorValue() { + return model.getValue(); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/FlatButton.java b/core/src/net/sf/openrocket/gui/components/FlatButton.java new file mode 100644 index 00000000..d4c39ef4 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/FlatButton.java @@ -0,0 +1,69 @@ +package net.sf.openrocket.gui.components; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import javax.swing.Action; +import javax.swing.Icon; +import javax.swing.JButton; + +/** + * A JButton that appears flat until you roll over it. + * + * @author Sampo Niskanen + */ +public class FlatButton extends JButton { + + public FlatButton() { + super(); + initialize(); + } + + public FlatButton(Icon icon) { + super(icon); + initialize(); + } + + public FlatButton(String text) { + super(text); + initialize(); + } + + public FlatButton(Action a) { + super(a); + initialize(); + } + + public FlatButton(String text, Icon icon) { + super(text, icon); + initialize(); + } + + + private void initialize() { + this.addMouseListener(new MouseAdapter() { + @Override + public void mouseExited(MouseEvent e) { + flatten(); + } + + @Override + public void mouseEntered(MouseEvent e) { + raise(); + } + }); + flatten(); + } + + + private void flatten() { + this.setContentAreaFilled(false); + this.setBorderPainted(false); + } + + private void raise() { + this.setContentAreaFilled(true); + this.setBorderPainted(true); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/HtmlLabel.java b/core/src/net/sf/openrocket/gui/components/HtmlLabel.java new file mode 100644 index 00000000..59fdbfa7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/HtmlLabel.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Dimension; + +import javax.swing.JLabel; + +/** + * A JLabel that limits the minimum and maximum height of the label to the + * initial preferred height of the label. This is required in labels that use HTML + * since these often cause the panels to expand too much in height. + * + * @author Sampo Niskanen + */ +public class HtmlLabel extends JLabel { + + public HtmlLabel() { + super(); + limitSize(); + } + + public HtmlLabel(String text) { + super(text); + limitSize(); + } + + public HtmlLabel(String text, int horizontalAlignment) { + super(text, horizontalAlignment); + limitSize(); + } + + + private void limitSize() { + Dimension dim = this.getPreferredSize(); + this.setMinimumSize(new Dimension(0, dim.height)); + this.setMaximumSize(new Dimension(Integer.MAX_VALUE, dim.height)); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java b/core/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java new file mode 100644 index 00000000..aaf82928 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java @@ -0,0 +1,124 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.File; + +import javax.imageio.ImageIO; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.util.MathUtil; + +/** + * Draws a BufferedImage centered and scaled to fit to the component. + * + * @author Sampo Niskanen + */ +public class ImageDisplayComponent extends JPanel { + + private BufferedImage image; + + public ImageDisplayComponent() { + this(null); + } + + public ImageDisplayComponent(BufferedImage image) { + this.image = image; + } + + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + if (image == null) { + return; + } + + final int width = Math.max(this.getWidth(), 1); + final int height = Math.max(this.getHeight(), 1); + + final int origWidth = Math.max(image.getWidth(), 1); + final int origHeight = Math.max(image.getHeight(), 1); + + + // Determine scaling factor + double scaleX = ((double) width) / origWidth; + double scaleY = ((double) height) / origHeight; + + double scale = MathUtil.min(scaleX, scaleY); + + if (scale >= 1) { + scale = 1.0; + } + + + // Center in the middle of the component + int finalWidth = (int) Math.round(origWidth * scale); + int finalHeight = (int) Math.round(origHeight * scale); + + int posX = (width - finalWidth) / 2; + int posY = (height - finalHeight) / 2; + + + // Draw the image + int dx1 = posX; + int dy1 = posY; + int dx2 = posX + finalWidth; + int dy2 = posY + finalHeight; + int sx1 = 0; + int sy1 = 0; + int sx2 = origWidth; + int sy2 = origHeight; + + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null); + + } + + + public BufferedImage getImage() { + return image; + } + + + public void setImage(BufferedImage image) { + this.image = image; + this.repaint(); + } + + + public static void main(String[] args) throws Exception { + final BufferedImage image = ImageIO.read(new File("test.png")); + + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + + JFrame frame = new JFrame(); + + JPanel panel = new JPanel(new MigLayout("fill")); + panel.setBackground(Color.red); + frame.add(panel); + + ImageDisplayComponent c = new ImageDisplayComponent(image); + panel.add(c, "grow"); + + frame.setSize(500, 500); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + + } + }); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/SelectableLabel.java b/core/src/net/sf/openrocket/gui/components/SelectableLabel.java new file mode 100644 index 00000000..d4fd622e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/SelectableLabel.java @@ -0,0 +1,44 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Cursor; +import java.awt.Dimension; + +import javax.swing.JTextField; +import javax.swing.UIManager; +import javax.swing.plaf.basic.BasicTextFieldUI; + +public class SelectableLabel extends JTextField { + + public SelectableLabel() { + this(""); + } + + public SelectableLabel(String text) { + super(text); + + // Set basic UI since GTK l&f doesn't support null border + this.setUI(new BasicTextFieldUI()); + + this.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); + + this.setEditable(false); + this.setBorder(null); + this.setOpaque(true); + if (UIManager.getColor("Label.foreground") != null) + this.setForeground(UIManager.getColor("Label.foreground")); + if (UIManager.getColor("Label.background") != null) + this.setBackground(UIManager.getColor("Label.background")); + if (UIManager.getFont("Label.font") != null) + this.setFont(UIManager.getFont("Label.font")); + + } + + // The default preferred size is slightly too short, causing it to scroll + @Override + public Dimension getPreferredSize() { + Dimension dim = super.getPreferredSize(); + dim.width += 5; + return dim; + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/SimulationExportPanel.java b/core/src/net/sf/openrocket/gui/components/SimulationExportPanel.java new file mode 100644 index 00000000..69bc02f0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/SimulationExportPanel.java @@ -0,0 +1,432 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.Arrays; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.util.FileHelper; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.gui.util.SaveCSVWorker; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; + +public class SimulationExportPanel extends JPanel { + + private static final String SPACE = "SPACE"; + private static final String TAB = "TAB"; + private static final Translator trans = Application.getTranslator(); + + private static final int OPTION_SIMULATION_COMMENTS = 0; + private static final int OPTION_FIELD_DESCRIPTIONS = 1; + private static final int OPTION_FLIGHT_EVENTS = 2; + + private final JTable table; + private final SelectionTableModel tableModel; + private final JLabel selectedCountLabel; + + private final Simulation simulation; + private final FlightDataBranch branch; + + private final boolean[] selected; + private final FlightDataType[] types; + private final Unit[] units; + + private final CsvOptionPanel csvOptions; + + + public SimulationExportPanel(Simulation sim) { + super(new MigLayout("fill, flowy")); + + JPanel panel; + JButton button; + + + this.simulation = sim; + + // TODO: MEDIUM: Only exports primary branch + + final FlightData data = simulation.getSimulatedData(); + + // Check that data exists + if (data == null || data.getBranchCount() == 0 || + data.getBranch(0).getTypes().length == 0) { + throw new IllegalArgumentException("No data for panel"); + } + + + // Create the data model + branch = data.getBranch(0); + + types = branch.getTypes(); + Arrays.sort(types); + + selected = new boolean[types.length]; + units = new Unit[types.length]; + for (int i = 0; i < types.length; i++) { + selected[i] = ((SwingPreferences) Application.getPreferences()).isExportSelected(types[i]); + units[i] = types[i].getUnitGroup().getDefaultUnit(); + } + + + //// Create the panel + + + // Set up the variable selection table + tableModel = new SelectionTableModel(); + table = new JTable(tableModel); + table.setDefaultRenderer(Object.class, + new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Object.class))); + table.setDefaultRenderer(Boolean.class, + new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Boolean.class))); + table.setRowSelectionAllowed(false); + table.setColumnSelectionAllowed(false); + + table.setDefaultEditor(Unit.class, new UnitCellEditor() { + @Override + protected UnitGroup getUnitGroup(Unit value, int row, int column) { + return types[row].getUnitGroup(); + } + }); + + // Set column widths + TableColumnModel columnModel = table.getColumnModel(); + TableColumn col = columnModel.getColumn(0); + int w = table.getRowHeight(); + col.setMinWidth(w); + col.setPreferredWidth(w); + col.setMaxWidth(w); + + col = columnModel.getColumn(1); + col.setPreferredWidth(200); + + col = columnModel.getColumn(2); + col.setPreferredWidth(100); + + table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); + + // Add table + panel = new JPanel(new MigLayout("fill")); + panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Vartoexport"))); + + panel.add(new JScrollPane(table), "wmin 300lp, width 300lp, height 1, grow 100, wrap"); + + // Select all/none buttons + button = new JButton(trans.get("SimExpPan.but.Selectall")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + tableModel.selectAll(); + } + }); + panel.add(button, "split 2, growx 1, sizegroup selectbutton"); + + button = new JButton(trans.get("SimExpPan.but.Selectnone")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + tableModel.selectNone(); + } + }); + panel.add(button, "growx 1, sizegroup selectbutton, wrap"); + + + selectedCountLabel = new JLabel(); + updateSelectedCount(); + panel.add(selectedCountLabel); + + this.add(panel, "grow 100, wrap"); + + + // These need to be in the order of the OPTIONS_XXX indices + csvOptions = new CsvOptionPanel(SimulationExportPanel.class, + trans.get("SimExpPan.checkbox.Includesimudesc"), + trans.get("SimExpPan.checkbox.ttip.Includesimudesc"), + trans.get("SimExpPan.checkbox.Includefielddesc"), + trans.get("SimExpPan.checkbox.ttip.Includefielddesc"), + trans.get("SimExpPan.checkbox.Incflightevents"), + trans.get("SimExpPan.checkbox.ttip.Incflightevents")); + + this.add(csvOptions, "spany, split, growx 1"); + + + // Space-filling panel + panel = new JPanel(); + this.add(panel, "width 1, height 1, grow 1"); + + + // Export button + button = new JButton(trans.get("SimExpPan.but.Exporttofile")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + doExport(); + } + }); + this.add(button, "gapbottom para, gapright para, right"); + + } + + + private void doExport() { + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(FileHelper.CSV_FILE_FILTER); + chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + + if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) + return; + + File file = chooser.getSelectedFile(); + if (file == null) + return; + + file = FileHelper.ensureExtension(file, "csv"); + if (!FileHelper.confirmWrite(file, this)) { + return; + } + + + String commentChar = csvOptions.getCommentCharacter(); + String fieldSep = csvOptions.getFieldSeparator(); + boolean simulationComment = csvOptions.getSelectionOption(OPTION_SIMULATION_COMMENTS); + boolean fieldComment = csvOptions.getSelectionOption(OPTION_FIELD_DESCRIPTIONS); + boolean eventComment = csvOptions.getSelectionOption(OPTION_FLIGHT_EVENTS); + csvOptions.storePreferences(); + + // Store preferences and export + int n = 0; + ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); + for (int i = 0; i < selected.length; i++) { + ((SwingPreferences) Application.getPreferences()).setExportSelected(types[i], selected[i]); + if (selected[i]) + n++; + } + + + FlightDataType[] fieldTypes = new FlightDataType[n]; + Unit[] fieldUnits = new Unit[n]; + int pos = 0; + for (int i = 0; i < selected.length; i++) { + if (selected[i]) { + fieldTypes[pos] = types[i]; + fieldUnits[pos] = units[i]; + pos++; + } + } + + if (fieldSep.equals(SPACE)) { + fieldSep = " "; + } else if (fieldSep.equals(TAB)) { + fieldSep = "\t"; + } + + + SaveCSVWorker.export(file, simulation, branch, fieldTypes, fieldUnits, fieldSep, + commentChar, simulationComment, fieldComment, eventComment, + SwingUtilities.getWindowAncestor(this)); + } + + + private void updateSelectedCount() { + int total = selected.length; + int n = 0; + String str; + + for (int i = 0; i < selected.length; i++) { + if (selected[i]) + n++; + } + + if (n == 1) { + //// Exporting 1 variable out of + str = trans.get("SimExpPan.ExportingVar.desc1") + " " + total + "."; + } else { + //// Exporting + //// variables out of + str = trans.get("SimExpPan.ExportingVar.desc2") + " " + n + " " + + trans.get("SimExpPan.ExportingVar.desc3") + " " + total + "."; + } + + selectedCountLabel.setText(str); + } + + + + /** + * A table cell renderer that uses another renderer and sets the background and + * foreground of the returned component based on the selection of the variable. + */ + private class SelectionBackgroundCellRenderer implements TableCellRenderer { + + private final TableCellRenderer renderer; + + public SelectionBackgroundCellRenderer(TableCellRenderer renderer) { + this.renderer = renderer; + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + + Component component = renderer.getTableCellRendererComponent(table, + value, isSelected, hasFocus, row, column); + + if (selected[row]) { + component.setBackground(table.getSelectionBackground()); + component.setForeground(table.getSelectionForeground()); + } else { + component.setBackground(table.getBackground()); + component.setForeground(table.getForeground()); + } + + return component; + } + + } + + + /** + * The table model for the variable selection. + */ + private class SelectionTableModel extends AbstractTableModel { + private static final int SELECTED = 0; + private static final int NAME = 1; + private static final int UNIT = 2; + + @Override + public int getColumnCount() { + return 3; + } + + @Override + public int getRowCount() { + return types.length; + } + + @Override + public String getColumnName(int column) { + switch (column) { + case SELECTED: + return ""; + case NAME: + //// Variable + return trans.get("SimExpPan.Col.Variable"); + case UNIT: + //// Unit + return trans.get("SimExpPan.Col.Unit"); + default: + throw new IndexOutOfBoundsException("column=" + column); + } + + } + + @Override + public Class getColumnClass(int column) { + switch (column) { + case SELECTED: + return Boolean.class; + case NAME: + return FlightDataType.class; + case UNIT: + return Unit.class; + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + @Override + public Object getValueAt(int row, int column) { + + switch (column) { + case SELECTED: + return selected[row]; + + case NAME: + return types[row]; + + case UNIT: + return units[row]; + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + + } + + @Override + public void setValueAt(Object value, int row, int column) { + + switch (column) { + case SELECTED: + selected[row] = (Boolean) value; + this.fireTableRowsUpdated(row, row); + updateSelectedCount(); + break; + + case NAME: + break; + + case UNIT: + units[row] = (Unit) value; + break; + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + + } + + @Override + public boolean isCellEditable(int row, int column) { + switch (column) { + case SELECTED: + return true; + + case NAME: + return false; + + case UNIT: + return types[row].getUnitGroup().getUnitCount() > 1; + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + public void selectAll() { + Arrays.fill(selected, true); + updateSelectedCount(); + this.fireTableDataChanged(); + } + + public void selectNone() { + Arrays.fill(selected, false); + updateSelectedCount(); + this.fireTableDataChanged(); + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/StageSelector.java b/core/src/net/sf/openrocket/gui/components/StageSelector.java new file mode 100644 index 00000000..94792791 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/StageSelector.java @@ -0,0 +1,111 @@ +package net.sf.openrocket.gui.components; + +import java.awt.event.ActionEvent; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; + +import javax.swing.AbstractAction; +import javax.swing.JPanel; +import javax.swing.JToggleButton; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.StateChangeListener; + + +public class StageSelector extends JPanel implements StateChangeListener { + private static final Translator trans = Application.getTranslator(); + + private final Configuration configuration; + + private List buttons = new ArrayList(); + + public StageSelector(Configuration configuration) { + super(new MigLayout("gap 0!")); + this.configuration = configuration; + + JToggleButton button = new JToggleButton(new StageAction(0)); + this.add(button); + buttons.add(button); + + updateButtons(); + configuration.addChangeListener(this); + } + + private void updateButtons() { + int stages = configuration.getStageCount(); + if (buttons.size() == stages) + return; + + while (buttons.size() > stages) { + JToggleButton button = buttons.remove(buttons.size() - 1); + this.remove(button); + } + + while (buttons.size() < stages) { + JToggleButton button = new JToggleButton(new StageAction(buttons.size())); + this.add(button); + buttons.add(button); + } + + this.revalidate(); + } + + + + + @Override + public void stateChanged(EventObject e) { + updateButtons(); + } + + + private class StageAction extends AbstractAction implements StateChangeListener { + private final int stage; + + public StageAction(final int stage) { + this.stage = stage; + configuration.addChangeListener(this); + stateChanged(null); + } + + @Override + public Object getValue(String key) { + if (key.equals(NAME)) { + //// Stage + return trans.get("StageAction.Stage") + " " + (stage + 1); + } + return super.getValue(key); + } + + @Override + public void actionPerformed(ActionEvent e) { + configuration.setToStage(stage); + + // boolean state = (Boolean)getValue(SELECTED_KEY); + // if (state == true) { + // // Was disabled, now enabled + // configuration.setToStage(stage); + // } else { + // // Was enabled, check what to do + // if (configuration.isStageActive(stage + 1)) { + // configuration.setToStage(stage); + // } else { + // if (stage == 0) + // configuration.setAllStages(); + // else + // configuration.setToStage(stage-1); + // } + // } + // stateChanged(null); + } + + @Override + public void stateChanged(EventObject e) { + this.putValue(SELECTED_KEY, configuration.isStageActive(stage)); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/components/StyledLabel.java b/core/src/net/sf/openrocket/gui/components/StyledLabel.java new file mode 100644 index 00000000..cad8a25a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/StyledLabel.java @@ -0,0 +1,111 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Font; + +import javax.swing.JLabel; +import javax.swing.SwingConstants; + +/** + * A resizeable and styleable JLabel. The method {@link #resizeFont(float)} changes the + * current font size by the given (positive or negative) amount. The change is relative + * to the current font size. The method {@link #setFontStyle(Style)} sets the style + * (bold/italic) of the font. + *

+ * A nice small text is achievable by new ResizeLabel("My text", -2); + * + * @author Sampo Niskanen + */ + +public class StyledLabel extends JLabel { + + public enum Style { + PLAIN(Font.PLAIN), + BOLD(Font.BOLD), + ITALIC(Font.ITALIC), + BOLD_ITALIC(Font.BOLD | Font.ITALIC); + + private int style; + Style(int fontStyle) { + this.style = fontStyle; + } + public int getFontStyle() { + return style; + } + } + + + + public StyledLabel() { + this("", SwingConstants.LEADING, 0f); + } + + public StyledLabel(String text) { + this(text, SwingConstants.LEADING, 0f); + } + + public StyledLabel(float size) { + this("", SwingConstants.LEADING, size); + } + + public StyledLabel(String text, float size) { + this(text, SwingConstants.LEADING, size); + } + + public StyledLabel(String text, int horizontalAlignment, float size) { + super(text, horizontalAlignment); + resizeFont(size); + checkPreferredSize(size, Style.PLAIN); + } + + + + public StyledLabel(Style style) { + this("", SwingConstants.LEADING, 0f, style); + } + + public StyledLabel(String text, Style style) { + this(text, SwingConstants.LEADING, 0f, style); + } + + public StyledLabel(float size, Style style) { + this("", SwingConstants.LEADING, size, style); + } + + public StyledLabel(String text, float size, Style style) { + this(text, SwingConstants.LEADING, size, style); + } + + public StyledLabel(String text, int horizontalAlignment, float size, Style style) { + super(text, horizontalAlignment); + resizeFont(size); + setFontStyle(style); + checkPreferredSize(size, style); + } + + + + + private void checkPreferredSize(float size, Style style) { + String str = this.getText(); + if (str.startsWith("") && str.indexOf(" + */ +public class URLLabel extends SelectableLabel { + private static final LogHelper log = Application.getLogger(); + + /** + * Create a label showing the url it will direct to. + * + * @param url the URL. + */ + public URLLabel(String url) { + this(url, url); + } + + /** + * Create a label with separate URL and label. + * + * @param url the URL clicking will open. + * @param label the label. + */ + public URLLabel(final String url, String label) { + super(); + + setText(label); + + if (Desktop.isDesktopSupported()) { + + // Blue, underlined font + Map map = new HashMap(); + map.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + this.setFont(this.getFont().deriveFont(map)); + this.setForeground(Color.BLUE); + + this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + + + this.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + Desktop d = Desktop.getDesktop(); + try { + d.browse(new URI(url)); + } catch (URISyntaxException e1) { + throw new BugException("Illegal URL: " + url, e1); + } catch (IOException e1) { + log.error("Unable to launch browser: " + e1.getMessage(), e1); + } + } + }); + + } + } +} diff --git a/core/src/net/sf/openrocket/gui/components/UnitCellEditor.java b/core/src/net/sf/openrocket/gui/components/UnitCellEditor.java new file mode 100644 index 00000000..2ff47abc --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/UnitCellEditor.java @@ -0,0 +1,77 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.AbstractCellEditor; +import javax.swing.JComboBox; +import javax.swing.JTable; +import javax.swing.table.TableCellEditor; + +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; + + +/** + * A cell editor that returns a combo box containing a selection of units. + * Using classes must implement the {@link #getUnitGroup(Unit, int, int)} method + * to return the appropriate unit group for the selection. + * + * @author Sampo Niskanen + */ +public abstract class UnitCellEditor extends AbstractCellEditor + implements TableCellEditor, ActionListener { + + private final JComboBox editor; + + public UnitCellEditor() { + editor = new JComboBox(); + editor.setEditable(false); + editor.addActionListener(this); + } + + + @Override + public Component getTableCellEditorComponent(JTable table, Object value, + boolean isSelected, int row, int column) { + + Unit unit = (Unit) value; + UnitGroup group = getUnitGroup(unit, row, column); + + editor.removeAllItems(); + for (Unit u : group.getUnits()) { + editor.addItem(u); + } + + editor.setSelectedItem(unit); + + return editor; + } + + + @Override + public Object getCellEditorValue() { + return editor.getSelectedItem(); + } + + + + @Override + public void actionPerformed(ActionEvent e) { + // End editing when a value has been selected + this.fireEditingStopped(); + } + + + /** + * Return the unit group corresponding to the specified cell. + * + * @param value the cell's value. + * @param row the cell's row. + * @param column the cell's column. + * @return the unit group of this cell. + */ + protected abstract UnitGroup getUnitGroup(Unit value, int row, int column); + +} diff --git a/core/src/net/sf/openrocket/gui/components/UnitSelector.java b/core/src/net/sf/openrocket/gui/components/UnitSelector.java new file mode 100644 index 00000000..1bdef68e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/UnitSelector.java @@ -0,0 +1,339 @@ +package net.sf.openrocket.gui.components; + + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.ItemSelectable; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.util.ArrayList; +import java.util.EventObject; +import java.util.List; + +import javax.swing.Action; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.border.Border; +import javax.swing.border.CompoundBorder; +import javax.swing.border.EmptyBorder; +import javax.swing.border.LineBorder; + +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.StateChangeListener; + + +/** + * A Swing component that allows one to choose a unit from a UnitGroup within + * a DoubleModel model. The current unit of the model is shown as a JLabel, and + * the unit can be changed by clicking on the label. + * + * @author Sampo Niskanen + */ + +public class UnitSelector extends StyledLabel implements StateChangeListener, MouseListener, + ItemSelectable { + + private DoubleModel model; + private final Action[] extraActions; + + private UnitGroup unitGroup; + private Unit currentUnit; + + private final boolean showValue; + + private final Border normalBorder; + private final Border withinBorder; + + + private final List itemListeners = new ArrayList(); + + + /** + * Common private constructor that sets the values and sets up the borders. + * Either model or group must be null. + * + * @param model + * @param showValue + * @param group + * @param actions + */ + private UnitSelector(DoubleModel model, boolean showValue, UnitGroup group, + Action[] actions) { + super(); + + this.model = model; + this.showValue = showValue; + + if (model != null) { + this.unitGroup = model.getUnitGroup(); + this.currentUnit = model.getCurrentUnit(); + } else if (group != null) { + this.unitGroup = group; + this.currentUnit = group.getDefaultUnit(); + } else { + this.unitGroup = UnitGroup.UNITS_NONE; + this.currentUnit = UnitGroup.UNITS_NONE.getDefaultUnit(); + } + + this.extraActions = actions; + + addMouseListener(this); + + // Define borders to use: + + normalBorder = new CompoundBorder( + new LineBorder(new Color(0f, 0f, 0f, 0.08f), 1), new EmptyBorder(1, 1, 1, + 1)); + withinBorder = new CompoundBorder(new LineBorder(new Color(0f, 0f, 0f, 0.6f)), + new EmptyBorder(1, 1, 1, 1)); + + // Add model listener if showing value + if (showValue) + this.model.addChangeListener(this); + + setBorder(normalBorder); + updateText(); + } + + + + public UnitSelector(DoubleModel model, Action... actions) { + this(model, false, actions); + } + + public UnitSelector(DoubleModel model, boolean showValue, Action... actions) { + this(model, showValue, null, actions); + } + + + public UnitSelector(UnitGroup group, Action... actions) { + this(null, false, group, actions); + } + + + + + /** + * Return the DoubleModel that is backing this selector up, or null. + * Either this method or {@link #getUnitGroup()} always returns null. + * + * @return the DoubleModel being used, or null. + */ + public DoubleModel getModel() { + return model; + } + + + /** + * Set the current double model. + * + * @param model the model to set, null is NOT allowed. + */ + public void setModel(DoubleModel model) { + if (this.model != null && showValue) { + this.model.removeChangeListener(this); + } + this.model = model; + this.unitGroup = model.getUnitGroup(); + this.currentUnit = model.getCurrentUnit(); + if (showValue) { + this.model.addChangeListener(this); + } + updateText(); + } + + + + /** + * Return the unit group that is being shown, or null. Either this method + * or {@link #getModel()} always returns null. + * + * @return the UnitGroup being used, or null. + */ + public UnitGroup getUnitGroup() { + return unitGroup; + } + + + public void setUnitGroup(UnitGroup group) { + if (model != null) { + throw new IllegalStateException( + "UnitGroup cannot be set when backed up with model."); + } + + if (this.unitGroup == group) + return; + + this.unitGroup = group; + this.currentUnit = group.getDefaultUnit(); + updateText(); + } + + + /** + * Return the currently selected unit. Works both when backup up with a DoubleModel + * and UnitGroup. + * + * @return the currently selected unit. + */ + public Unit getSelectedUnit() { + return currentUnit; + } + + + /** + * Set the currently selected unit. Sets it to the DoubleModel if it is backed up + * by it. + * + * @param unit the unit to select. + */ + public void setSelectedUnit(Unit unit) { + if (!unitGroup.contains(unit)) { + throw new IllegalArgumentException("unit " + unit + + " not contained in group " + unitGroup); + } + + this.currentUnit = unit; + if (model != null) { + model.setCurrentUnit(unit); + } + updateText(); + fireItemEvent(); + } + + + + /** + * Updates the text of the label + */ + private void updateText() { + if (model != null) { + + Unit unit = model.getCurrentUnit(); + if (showValue) { + setText(unit.toStringUnit(model.getValue())); + } else { + setText(unit.getUnit()); + } + + } else if (unitGroup != null) { + + setText(currentUnit.getUnit()); + + } else { + throw new IllegalStateException("Both model and unitGroup are null."); + } + } + + + /** + * Update the component when the DoubleModel changes. + */ + @Override + public void stateChanged(EventObject e) { + updateText(); + } + + + + //////// ItemListener handling //////// + + public void addItemListener(ItemListener listener) { + itemListeners.add(listener); + } + + public void removeItemListener(ItemListener listener) { + itemListeners.remove(listener); + } + + protected void fireItemEvent() { + ItemEvent event = null; + ItemListener[] listeners = itemListeners.toArray(new ItemListener[0]); + for (ItemListener l: listeners) { + if (event == null) { + event = new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, getSelectedUnit(), + ItemEvent.SELECTED); + } + l.itemStateChanged(event); + } + } + + + + //////// Popup //////// + + private void popup() { + JPopupMenu popup = new JPopupMenu(); + + for (int i = 0; i < unitGroup.getUnitCount(); i++) { + Unit unit = unitGroup.getUnit(i); + JMenuItem item = new JMenuItem(unit.getUnit()); + item.addActionListener(new UnitSelectorItem(unit)); + popup.add(item); + } + + for (int i = 0; i < extraActions.length; i++) { + if (extraActions[i] == null && i < extraActions.length - 1) { + popup.addSeparator(); + } else { + popup.add(new JMenuItem(extraActions[i])); + } + } + + Dimension d = getSize(); + popup.show(this, 0, d.height); + } + + + /** + * ActionListener class that sets the currently selected unit. + */ + private class UnitSelectorItem implements ActionListener { + private final Unit unit; + + public UnitSelectorItem(Unit u) { + unit = u; + } + + public void actionPerformed(ActionEvent e) { + setSelectedUnit(unit); + } + } + + + @Override + public Object[] getSelectedObjects() { + return new Object[]{ getSelectedUnit() }; + } + + + + //////// Mouse handling //////// + + public void mouseClicked(MouseEvent e) { + if (unitGroup.getUnitCount() > 1) + popup(); + } + + public void mouseEntered(MouseEvent e) { + if (unitGroup.getUnitCount() > 1) + setBorder(withinBorder); + } + + public void mouseExited(MouseEvent e) { + setBorder(normalBorder); + } + + public void mousePressed(MouseEvent e) { + } // Ignore + + public void mouseReleased(MouseEvent e) { + } // Ignore + +} diff --git a/core/src/net/sf/openrocket/gui/components/compass/CompassPointer.java b/core/src/net/sf/openrocket/gui/components/compass/CompassPointer.java new file mode 100644 index 00000000..37fdad42 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/compass/CompassPointer.java @@ -0,0 +1,218 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.sf.openrocket.gui.Resettable; +import net.sf.openrocket.gui.adaptors.DoubleModel; + +/** + * A component that draws a pointer onto a compass rose. + * + * @author Sampo Niskanen + */ +public class CompassPointer extends CompassRose implements Resettable { + + private static final Color PRIMARY_POINTER_COLOR = new Color(1.0f, 0.2f, 0.2f); + private static final Color SECONDARY_POINTER_COLOR = new Color(0.2f, 0.2f, 0.2f, 0.2f); + + private final DoubleModel model; + private final ChangeListener listener; + + protected int width = -1; + protected int mid = -1; + + private DoubleModel secondaryModel; + + private float pointerLength = 0.95f; + private float pointerWidth = 0.1f; + private float pointerArrowWidth = 0.2f; + private boolean pointerArrow = true; + + + + public CompassPointer(DoubleModel model) { + super(); + this.model = model; + listener = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + CompassPointer.this.repaint(); + } + }; + model.addChangeListener(listener); + } + + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + + Graphics2D g2 = (Graphics2D) g; + + + Dimension dimension = this.getSize(); + + width = Math.min(dimension.width, dimension.height); + mid = width / 2; + width = (int) (getScaler() * width); + + + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + + if (secondaryModel != null) { + drawArrow(secondaryModel.getValue(), SECONDARY_POINTER_COLOR, g2); + } + drawArrow(model.getValue(), PRIMARY_POINTER_COLOR, g2); + + + } + + + private void drawArrow(double angle, Color color, Graphics2D g2) { + + int pLength = (int) (width * pointerLength / 2); + int pWidth = (int) (width * pointerWidth / 2); + int pArrowWidth = (int) (width * pointerArrowWidth / 2); + + int[] x = new int[8]; + int[] y = new int[8]; + + g2.setColor(color); + + + double sin = Math.sin(angle); + double cos = Math.cos(angle); + + int n = 0; + + // Top part + x[n] = 0; + y[n] = -pLength; + n++; + if (pointerArrow) { + x[n] = -pArrowWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + x[n] = -pWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + } + + // Bottom part + x[n] = -pWidth; + y[n] = pLength; + n++; + x[n] = 0; + y[n] = pLength - pWidth; + n++; + x[n] = pWidth; + y[n] = pLength; + n++; + + // Top part + if (pointerArrow) { + x[n] = pWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + x[n] = pArrowWidth; + y[n] = -pLength + 2 * pArrowWidth; + n++; + } + + // Rotate and shift + for (int i = 0; i < n; i++) { + double x2, y2; + x2 = cos * x[i] - sin * y[i]; + y2 = sin * x[i] + cos * y[i]; + + x[i] = (int) (x2 + mid); + y[i] = (int) (y2 + mid); + } + + g2.fillPolygon(x, y, n); + + g2.setColor(color.darker()); + g2.drawPolygon(x, y, n); + + } + + + public boolean isPointerArrow() { + return pointerArrow; + } + + + public void setPointerArrow(boolean useArrow) { + this.pointerArrow = useArrow; + repaint(); + } + + + public float getPointerLength() { + return pointerLength; + } + + + public void setPointerLength(float pointerLength) { + this.pointerLength = pointerLength; + repaint(); + } + + + public float getPointerWidth() { + return pointerWidth; + } + + + public void setPointerWidth(float pointerWidth) { + this.pointerWidth = pointerWidth; + repaint(); + } + + + public float getPointerArrowWidth() { + return pointerArrowWidth; + } + + + public void setPointerArrowWidth(float pointerArrowWidth) { + this.pointerArrowWidth = pointerArrowWidth; + repaint(); + } + + + + public DoubleModel getSecondaryModel() { + return secondaryModel; + } + + + public void setSecondaryModel(DoubleModel secondaryModel) { + if (this.secondaryModel != null) { + this.secondaryModel.removeChangeListener(listener); + } + this.secondaryModel = secondaryModel; + if (this.secondaryModel != null) { + this.secondaryModel.addChangeListener(listener); + } + } + + + @Override + public void resetModel() { + model.removeChangeListener(listener); + setSecondaryModel(null); + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/components/compass/CompassRose.java b/core/src/net/sf/openrocket/gui/components/compass/CompassRose.java new file mode 100644 index 00000000..49f1d059 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/compass/CompassRose.java @@ -0,0 +1,221 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.font.GlyphVector; +import java.awt.geom.Rectangle2D; + +import javax.swing.JComponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +/** + * A component that draws a compass rose. This class has no other functionality, but superclasses + * may add functionality to it. + * + * @author Sampo Niskanen + */ +public class CompassRose extends JComponent { + private static final Translator trans = Application.getTranslator(); + + + private static final Color MAIN_COLOR = new Color(0.4f, 0.4f, 1.0f); + private static final float MAIN_LENGTH = 0.95f; + private static final float MAIN_WIDTH = 0.15f; + + private static final int CIRCLE_BORDER = 2; + private static final Color CIRCLE_HIGHLIGHT = new Color(1.0f, 1.0f, 1.0f, 0.7f); + private static final Color CIRCLE_SHADE = new Color(0.0f, 0.0f, 0.0f, 0.2f); + + private static final Color MARKER_COLOR = Color.BLACK; + + + private double scaler; + + private double markerRadius; + private Font markerFont; + + + /** + * Construct a compass rose with the default settings. + */ + public CompassRose() { + this(0.8, 1.1, Font.decode("Serif-PLAIN-16")); + } + + + /** + * Construct a compass rose with the specified settings. + * + * @param scaler The scaler of the rose. The bordering circle will we this portion of the component dimensions. + * @param markerRadius The radius for the marker positions (N/E/S/W), or NaN for no markers. A value greater than one + * will position the markers outside of the bordering circle. + * @param markerFont The font used for the markers. + */ + public CompassRose(double scaler, double markerRadius, Font markerFont) { + this.scaler = scaler; + this.markerRadius = markerRadius; + this.markerFont = markerFont; + } + + + + @Override + public void paintComponent(Graphics g) { + + Graphics2D g2 = (Graphics2D) g; + + int[] x = new int[3]; + int[] y = new int[3]; + Dimension dimension = this.getSize(); + + int width = Math.min(dimension.width, dimension.height); + int mid = width / 2; + width = (int) (scaler * width); + + int mainLength = (int) (width * MAIN_LENGTH / 2); + int mainWidth = (int) (width * MAIN_WIDTH / 2); + + + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + g2.setColor(MAIN_COLOR); + + // North + x[0] = mid; + y[0] = mid; + x[1] = mid; + y[1] = mid - mainLength; + x[2] = mid - mainWidth; + y[2] = mid - mainWidth; + g2.fillPolygon(x, y, 3); + + x[2] = mid + mainWidth; + g2.drawPolygon(x, y, 3); + + // East + x[0] = mid; + y[0] = mid; + x[1] = mid + mainLength; + y[1] = mid; + x[2] = mid + mainWidth; + y[2] = mid - mainWidth; + g2.fillPolygon(x, y, 3); + + y[2] = mid + mainWidth; + g2.drawPolygon(x, y, 3); + + // South + x[0] = mid; + y[0] = mid; + x[1] = mid; + y[1] = mid + mainLength; + x[2] = mid + mainWidth; + y[2] = mid + mainWidth; + g2.fillPolygon(x, y, 3); + + x[2] = mid - mainWidth; + g2.drawPolygon(x, y, 3); + + // West + x[0] = mid; + y[0] = mid; + x[1] = mid - mainLength; + y[1] = mid; + x[2] = mid - mainWidth; + y[2] = mid + mainWidth; + g2.fillPolygon(x, y, 3); + + y[2] = mid - mainWidth; + g2.drawPolygon(x, y, 3); + + + // Border circle + g2.setColor(CIRCLE_SHADE); + g2.drawArc(mid - width / 2 + CIRCLE_BORDER, mid - width / 2 + CIRCLE_BORDER, + width - 2 * CIRCLE_BORDER, width - 2 * CIRCLE_BORDER, 45, 180); + g2.setColor(CIRCLE_HIGHLIGHT); + g2.drawArc(mid - width / 2 + CIRCLE_BORDER, mid - width / 2 + CIRCLE_BORDER, + width - 2 * CIRCLE_BORDER, width - 2 * CIRCLE_BORDER, 180 + 45, 180); + + + // Draw direction markers + if (!Double.isNaN(markerRadius) && markerFont != null) { + + int pos = (int) (width * markerRadius / 2); + + g2.setColor(MARKER_COLOR); + drawMarker(g2, mid, mid - pos, trans.get("lbl.north")); + drawMarker(g2, mid + pos, mid, trans.get("lbl.east")); + drawMarker(g2, mid, mid + pos, trans.get("lbl.south")); + drawMarker(g2, mid - pos, mid, trans.get("lbl.west")); + + } + + } + + + + private void drawMarker(Graphics2D g2, float x, float y, String str) { + GlyphVector gv = markerFont.createGlyphVector(g2.getFontRenderContext(), str); + Rectangle2D rect = gv.getVisualBounds(); + + x -= rect.getWidth() / 2; + y += rect.getHeight() / 2; + + g2.drawGlyphVector(gv, x, y); + + } + + + + + + public double getScaler() { + return scaler; + } + + + public void setScaler(double scaler) { + this.scaler = scaler; + repaint(); + } + + + public double getMarkerRadius() { + return markerRadius; + } + + + public void setMarkerRadius(double markerRadius) { + this.markerRadius = markerRadius; + repaint(); + } + + + public Font getMarkerFont() { + return markerFont; + } + + + public void setMarkerFont(Font markerFont) { + this.markerFont = markerFont; + repaint(); + } + + @Override + public Dimension getPreferredSize() { + Dimension dim = super.getPreferredSize(); + int min = Math.min(dim.width, dim.height); + dim.setSize(min, min); + return dim; + } + + +} diff --git a/core/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java b/core/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java new file mode 100644 index 00000000..c8b0fee2 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java @@ -0,0 +1,190 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JSpinner; +import javax.swing.border.BevelBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.Resettable; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.FlatButton; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.MathUtil; + + +/** + * A button that displays a current compass direction and opens a popup to edit + * the value when clicked. + * + * @author Sampo Niskanen + */ +public class CompassSelectionButton extends FlatButton implements Resettable { + + private static final Translator trans = Application.getTranslator(); + + private static final int POPUP_COMPASS_SIZE = 200; + private static final double SECTOR = 45; + + private static int minWidth = -1; + + + private final DoubleModel model; + private final ChangeListener listener; + + private JPopupMenu popup; + + + public CompassSelectionButton(final DoubleModel model) { + this.model = model; + + JPanel panel = new JPanel(new MigLayout("fill, ins 0")); + panel.setOpaque(false); + + CompassPointer pointer = new CompassPointer(model); + pointer.setPreferredSize(new Dimension(24, 24)); + pointer.setMarkerFont(null); + pointer.setPointerArrow(false); + pointer.setPointerWidth(0.45f); + pointer.setScaler(1.0f); + panel.add(pointer, "gapright rel"); + + + final JLabel label = new JLabel(); + label.setText(getLabel(model.getValue())); + panel.add(label); + + listener = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + label.setText(getLabel(model.getValue())); + } + }; + model.addChangeListener(listener); + + + if (minWidth < 0) { + calculateMinWidth(); + label.setMinimumSize(new Dimension(minWidth, 0)); + } + + + this.add(panel); + + this.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + openPopup(); + } + }); + } + + + + + private String getLabel(double value) { + String str; + + value = MathUtil.reduce360(value); + value = Math.toDegrees(value); + str = "" + Math.round(value) + Chars.DEGREE + " ("; + + if (value <= 0.5 * SECTOR || value >= 7.5 * SECTOR) { + str += trans.get("lbl.N"); + } else if (value <= 1.5 * SECTOR) { + str += trans.get("lbl.NE"); + } else if (value <= 2.5 * SECTOR) { + str += trans.get("lbl.E"); + } else if (value <= 3.5 * SECTOR) { + str += trans.get("lbl.SE"); + } else if (value <= 4.5 * SECTOR) { + str += trans.get("lbl.S"); + } else if (value <= 5.5 * SECTOR) { + str += trans.get("lbl.SW"); + } else if (value <= 6.5 * SECTOR) { + str += trans.get("lbl.W"); + } else { + str += trans.get("lbl.NW"); + } + + str += ")"; + return str; + } + + + private void openPopup() { + if (popup == null) { + popup = new JPopupMenu(); + + + final JPanel panel = new JPanel(new MigLayout("fill")); + + final CompassPointer rose = new CompassSelector(model); + rose.setPreferredSize(new Dimension(POPUP_COMPASS_SIZE, POPUP_COMPASS_SIZE)); + panel.add(rose, "spany, gapright unrel"); + + panel.add(new JPanel(), "growy, wrap"); + + JSpinner spin = new JSpinner(model.getSpinnerModel()); + panel.add(spin, "wmin 50lp, growx, gapright 0, aligny bottom"); + + panel.add(new JLabel("" + Chars.DEGREE), "wrap para"); + + JButton close = new JButton("OK"); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + popup.setVisible(false); + } + }); + panel.add(close, "span 2, growx, wrap"); + + panel.add(new JPanel(), "growy, wrap"); + + popup.add(panel); + popup.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); + } + + popup.pack(); + + Dimension popupSize = popup.getPreferredSize(); + Dimension buttonSize = this.getSize(); + + int posX = buttonSize.width / 2 - popupSize.width / 2; + int posY = buttonSize.height / 2 - popupSize.height / 2; + popup.show(this, posX, posY); + } + + private void calculateMinWidth() { + JLabel label = new JLabel(); + int max = 0; + for (double deg = 0; deg < 360; deg += 0.99999999999) { + label.setText(getLabel(Math.toRadians(deg))); + int w = label.getPreferredSize().width; + if (w > max) { + max = w; + } + } + minWidth = max + 1; + } + + + + + @Override + public void resetModel() { + model.removeChangeListener(listener); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/compass/CompassSelector.java b/core/src/net/sf/openrocket/gui/components/compass/CompassSelector.java new file mode 100644 index 00000000..deed9cf7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/compass/CompassSelector.java @@ -0,0 +1,97 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.util.MathUtil; + +/** + * Component that allows selecting a compass direction on a CompassSelector. + * + * @author Sampo Niskanen + */ +public class CompassSelector extends CompassPointer { + + private final DoubleModel model; + + public CompassSelector(DoubleModel model) { + super(model); + this.model = model; + + MouseAdapter mouse = new MouseAdapter() { + private boolean dragging = false; + + @Override + public void mousePressed(MouseEvent e) { + if (!isWithinCircle(e)) + return; + if (e.getButton() != MouseEvent.BUTTON1) + return; + dragging = true; + clicked(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + if (e.getButton() != MouseEvent.BUTTON1) + return; + dragging = false; + } + + + @Override + public void mouseDragged(MouseEvent e) { + if (!dragging) + return; + clicked(e); + } + }; + this.addMouseListener(mouse); + this.addMouseMotionListener(mouse); + + } + + private boolean isWithinCircle(MouseEvent e) { + if (mid < 0 || width < 0) { + return false; + } + + int x = e.getX() - mid; + int y = e.getY() - mid; + + double distance = Math.hypot(x, y); + return distance < width / 2; + } + + private void clicked(MouseEvent e) { + + if (mid < 0 || width < 0) { + return; + } + + int x = e.getX() - mid; + int y = e.getY() - mid; + + double distance = Math.hypot(x, y); + + double theta = Math.atan2(y, x); + theta = MathUtil.reduce360(theta + Math.PI / 2); + + // Round the value appropriately + theta = Math.toDegrees(theta); + + if (distance > 50) { + theta = Math.round(theta); + } else if (distance > 10) { + theta = 5 * Math.round(theta / 5); + } else { + // Do nothing if too close to center + return; + } + theta = Math.toRadians(theta); + + model.setValue(theta); + } + +} diff --git a/core/src/net/sf/openrocket/gui/components/compass/Tester.java b/core/src/net/sf/openrocket/gui/components/compass/Tester.java new file mode 100644 index 00000000..86a82a49 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/components/compass/Tester.java @@ -0,0 +1,66 @@ +package net.sf.openrocket.gui.components.compass; + +import java.awt.Dimension; +import java.lang.reflect.InvocationTargetException; + +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.ResourceBundleTranslator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class Tester { + + + public static void main(String[] args) throws InterruptedException, InvocationTargetException { + + Application.setBaseTranslator(new ResourceBundleTranslator("l10n.messages")); + + GUIUtil.setBestLAF(); + + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + JFrame frame = new JFrame(); + + JPanel panel = new JPanel(new MigLayout("fill")); + DoubleModel model = new DoubleModel(Math.toRadians(45), UnitGroup.UNITS_ANGLE); + DoubleModel second = new DoubleModel(Math.toRadians(30), UnitGroup.UNITS_ANGLE); + + + CompassPointer rose = new CompassSelector(model); + rose.setPreferredSize(new Dimension(300, 300)); + rose.setSecondaryModel(second); + panel.add(rose); + + rose = new CompassPointer(model); + rose.setPreferredSize(new Dimension(24, 24)); + panel.add(rose); + rose.setMarkerFont(null); + rose.setPointerArrow(false); + rose.setPointerWidth(0.45f); + rose.setScaler(1.0f); + + JSpinner spin = new JSpinner(model.getSpinnerModel()); + spin.setPreferredSize(new Dimension(50, 20)); + panel.add(spin, "wrap para"); + + + CompassSelectionButton button = new CompassSelectionButton(model); + panel.add(button); + + + frame.add(panel); + frame.pack(); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setVisible(true); + } + }); + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java b/core/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java new file mode 100644 index 00000000..af272a0d --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java @@ -0,0 +1,119 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class BodyTubeConfig extends RocketComponentConfig { + + private MotorConfig motorConfigPane = null; + private static final Translator trans = Application.getTranslator(); + + public BodyTubeConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + //// Body tube length + panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Bodytubelength"))); + + DoubleModel m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.5, 2.0)), "w 100lp, wrap"); + + + //// Body tube diameter + panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Outerdiameter"))); + + DoubleModel od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); + + JCheckBox check = new JCheckBox(od.getAutomaticAction()); + //// Automatic + check.setText(trans.get("BodyTubecfg.checkbox.Automatic")); + panel.add(check, "skip, span 2, wrap"); + + + //// Inner diameter + panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Innerdiameter"))); + + // Diameter = 2*Radius + m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); + + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), "w 100lp, wrap"); + + + //// Wall thickness + panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Wallthickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 0px"); + + //// Filled + check = new JCheckBox(new BooleanModel(component, "Filled")); + check.setText(trans.get("BodyTubecfg.checkbox.Filled")); + panel.add(check, "skip, span 2, wrap"); + + + //// Material + panel.add(materialPanel(new JPanel(new MigLayout()), Material.Type.BULK), + "cell 4 0, gapleft paragraph, aligny 0%, spany"); + + //// General and General properties + tabbedPane.insertTab(trans.get("BodyTubecfg.tab.General"), null, panel, + trans.get("BodyTubecfg.tab.Generalproperties"), 0); + motorConfigPane = new MotorConfig((BodyTube) c); + //// Motor and Motor mount configuration + tabbedPane.insertTab(trans.get("BodyTubecfg.tab.Motor"), null, motorConfigPane, + trans.get("BodyTubecfg.tab.Motormountconf"), 1); + tabbedPane.setSelectedIndex(0); + } + + @Override + public void updateFields() { + super.updateFields(); + if (motorConfigPane != null) + motorConfigPane.updateFields(); + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java b/core/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java new file mode 100644 index 00000000..135ec94e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JPanel; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + + + +public class BulkheadConfig extends RingComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public BulkheadConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel tab; + + tab = generalTab(trans.get("BulkheadCfg.tab.Diameter"), null, null, + trans.get("BulkheadCfg.tab.Thickness")); + //// General and General properties + tabbedPane.insertTab(trans.get("BulkheadCfg.tab.General"), null, tab, + trans.get("BulkheadCfg.tab.Generalproperties"), 0); + tabbedPane.setSelectedIndex(0); + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java b/core/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java new file mode 100644 index 00000000..dfe6a352 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java @@ -0,0 +1,31 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JPanel; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + + + +public class CenteringRingConfig extends RingComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public CenteringRingConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel tab; + + //// Outer diameter: and Inner diameter: and Thickness: + tab = generalTab(trans.get("CenteringRingCfg.tab.Outerdiam"), + trans.get("CenteringRingCfg.tab.Innerdiam"), null, + trans.get("CenteringRingCfg.tab.Thickness")); + //// General and General properties + tabbedPane.insertTab(trans.get("CenteringRingCfg.tab.General"), null, tab, + trans.get("CenteringRingCfg.tab.Generalproperties"), 0); + tabbedPane.setSelectedIndex(0); + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java b/core/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java new file mode 100644 index 00000000..a669b2dc --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java @@ -0,0 +1,219 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.Window; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import javax.swing.JDialog; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Reflection; + +/** + * A dialog that contains the configuration elements of one component. + * The contents of the dialog are instantiated from CONFIGDIALOGPACKAGE according + * to the current component. + * + * @author Sampo Niskanen + */ + +public class ComponentConfigDialog extends JDialog implements ComponentChangeListener { + private static final long serialVersionUID = 1L; + private static final String CONFIGDIALOGPACKAGE = "net.sf.openrocket.gui.configdialog"; + private static final String CONFIGDIALOGPOSTFIX = "Config"; + + + private static ComponentConfigDialog dialog = null; + + + private OpenRocketDocument document = null; + private RocketComponent component = null; + private RocketComponentConfig configurator = null; + + private final Window parent; + private static final Translator trans = Application.getTranslator(); + + private ComponentConfigDialog(Window parent, OpenRocketDocument document, + RocketComponent component) { + super(parent); + this.parent = parent; + + setComponent(document, component); + + GUIUtil.setDisposableDialogOptions(this, null); + GUIUtil.rememberWindowPosition(this); + } + + + /** + * Set the component being configured. The listening connections of the old configurator + * will be removed and the new ones created. + * + * @param component Component to configure. + */ + private void setComponent(OpenRocketDocument document, RocketComponent component) { + if (this.document != null) { + this.document.getRocket().removeComponentChangeListener(this); + } + + if (configurator != null) { + // Remove listeners by setting all applicable models to null + GUIUtil.setNullModels(configurator); // null-safe + } + + this.document = document; + this.component = component; + this.document.getRocket().addComponentChangeListener(this); + + configurator = getDialogContents(); + this.setContentPane(configurator); + configurator.updateFields(); + + //// configuration + setTitle(trans.get("ComponentCfgDlg.configuration1") + " " + component.getComponentName() + " " + trans.get("ComponentCfgDlg.configuration")); + + this.pack(); + } + + /** + * Return the configurator panel of the current component. + */ + private RocketComponentConfig getDialogContents() { + Constructor c = + findDialogContentsConstructor(component); + if (c != null) { + try { + return c.newInstance(document, component); + } catch (InstantiationException e) { + throw new BugException("BUG in constructor reflection", e); + } catch (IllegalAccessException e) { + throw new BugException("BUG in constructor reflection", e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + // Should never be reached, since RocketComponentConfig should catch all + // components without their own configurator. + throw new BugException("Unable to find any configurator for " + component); + } + + + private void closeDialog() { + this.setVisible(false); + this.dispose(); + this.configurator.invalidateModels(); + } + + + @Override + public void componentChanged(ComponentChangeEvent e) { + if (e.isTreeChange() || e.isUndoChange()) { + + // Hide dialog in case of tree or undo change + dialog.closeDialog(); + + } else { + /* + * TODO: HIGH: The line below has caused a NullPointerException (without null check) + * How is this possible? The null check was added to avoid this, but the + * root cause should be analyzed. + * [Openrocket-bugs] 2009-12-12 19:23:22 Automatic bug report for OpenRocket 0.9.5 + */ + if (configurator != null) + configurator.updateFields(); + } + } + + + /** + * Finds the Constructor of the given component's config dialog panel in + * CONFIGDIALOGPACKAGE. + */ + @SuppressWarnings("unchecked") + private static Constructor findDialogContentsConstructor(RocketComponent component) { + Class currentclass; + String currentclassname; + String configclassname; + + Class configclass; + Constructor c; + + currentclass = component.getClass(); + while ((currentclass != null) && (currentclass != Object.class)) { + currentclassname = currentclass.getCanonicalName(); + int index = currentclassname.lastIndexOf('.'); + if (index >= 0) + currentclassname = currentclassname.substring(index + 1); + configclassname = CONFIGDIALOGPACKAGE + "." + currentclassname + + CONFIGDIALOGPOSTFIX; + + try { + configclass = Class.forName(configclassname); + c = (Constructor) + configclass.getConstructor(OpenRocketDocument.class, RocketComponent.class); + return c; + } catch (Exception ignore) { + } + + currentclass = currentclass.getSuperclass(); + } + return null; + } + + + + + ////////// Static dialog ///////// + + /** + * A singleton configuration dialog. Will create and show a new dialog if one has not + * previously been used, or update the dialog and show it if a previous one exists. + * + * @param document the document to configure. + * @param component the component to configure. + */ + public static void showDialog(Window parent, OpenRocketDocument document, + RocketComponent component) { + if (dialog != null) + dialog.dispose(); + + dialog = new ComponentConfigDialog(parent, document, component); + dialog.setVisible(true); + + ////Modify + document.addUndoPosition(trans.get("ComponentCfgDlg.Modify") + " " + component.getComponentName()); + } + + + /* package */ + static void showDialog(RocketComponent component) { + showDialog(dialog.parent, dialog.document, component); + } + + /** + * Hides the configuration dialog. May be used even if not currently visible. + */ + public static void hideDialog() { + if (dialog != null) { + dialog.closeDialog(); + } + } + + + /** + * Returns whether the singleton configuration dialog is currently visible or not. + */ + public static boolean isDialogVisible() { + return (dialog != null) && (dialog.isVisible()); + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java b/core/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java new file mode 100644 index 00000000..321ef6c4 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java @@ -0,0 +1,189 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.SwingConstants; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.IntegerModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class EllipticalFinSetConfig extends FinSetConfig { + private static final Translator trans = Application.getTranslator(); + + public EllipticalFinSetConfig(OpenRocketDocument d, final RocketComponent component) { + super(d, component); + + DoubleModel m; + JSpinner spin; + JComboBox combo; + + JPanel mainPanel = new JPanel(new MigLayout()); + + + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + //// Number of fins + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Nbroffins"))); + + IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); + + spin = new JSpinner(im.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx, wrap"); + + + //// Base rotation + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Rotation"))); + + m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + //// Fin cant + JLabel label = new JLabel(trans.get("EllipticalFinSetCfg.Fincant")); + //// "The angle that the fins are canted with respect to the rocket + label.setToolTipText(trans.get("EllipticalFinSetCfg.ttip.Fincant")); + panel.add(label); + + m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE, + -FinSet.MAX_CANT, FinSet.MAX_CANT); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT, FinSet.MAX_CANT)), + "w 100lp, wrap"); + + + + //// Root chord + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Rootchord"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); + + + //// Height + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Height"))); + + m = new DoubleModel(component, "Height", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); + + + //// Position + //// Position relative to: + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Positionrelativeto"))); + + combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + + //// plus + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + + + //// Right portion + mainPanel.add(panel, "aligny 20%"); + + mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy"); + + + + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + + //// Cross section + //// Fin cross section: + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.FincrossSection")), "span, split"); + combo = new JComboBox( + new EnumModel(component, "CrossSection")); + panel.add(combo, "growx, wrap unrel"); + + + //// Thickness: + panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Thickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 30lp"); + + + + //// Material + materialPanel(panel, Material.Type.BULK); + + + + + + mainPanel.add(panel, "aligny 20%"); + + addFinSetButtons(); + + //// General and General properties + tabbedPane.insertTab(trans.get("EllipticalFinSetCfg.General"), null, mainPanel, + trans.get("EllipticalFinSetCfg.Generalproperties"), 0); + tabbedPane.setSelectedIndex(0); + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/core/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java new file mode 100644 index 00000000..67eb4074 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java @@ -0,0 +1,468 @@ +package net.sf.openrocket.gui.configdialog; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.Coaxial; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + + +public abstract class FinSetConfig extends RocketComponentConfig { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private JButton split = null; + + public FinSetConfig(OpenRocketDocument d, RocketComponent component) { + super(d, component); + + //// Fin tabs and Through-the-wall fin tabs + tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(), + trans.get("FinSetConfig.tab.Through-the-wall"), 0); + } + + + protected void addFinSetButtons() { + JButton convert = null; + + //// Convert buttons + if (!(component instanceof FreeformFinSet)) { + //// Convert to freeform + convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform")); + //// Convert this fin set into a freeform fin set + convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip")); + convert.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Converting " + component.getComponentName() + " into freeform fin set"); + + // Do change in future for overall safety + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + //// Convert fin set + document.addUndoPosition(trans.get("FinSetConfig.Convertfinset")); + RocketComponent freeform = + FreeformFinSet.convertFinSet((FinSet) component); + ComponentConfigDialog.showDialog(freeform); + } + }); + + ComponentConfigDialog.hideDialog(); + } + }); + } + + //// Split fins + split = new JButton(trans.get("FinSetConfig.but.Splitfins")); + //// Split the fin set into separate fins + split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip")); + split.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" + + ((FinSet) component).getFinCount()); + + // Do change in future for overall safety + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + RocketComponent parent = component.getParent(); + int index = parent.getChildPosition(component); + int count = ((FinSet) component).getFinCount(); + double base = ((FinSet) component).getBaseRotation(); + if (count <= 1) + return; + + document.addUndoPosition("Split fin set"); + parent.removeChild(index); + for (int i = 0; i < count; i++) { + FinSet copy = (FinSet) component.copy(); + copy.setFinCount(1); + copy.setBaseRotation(base + i * 2 * Math.PI / count); + copy.setName(copy.getName() + " #" + (i + 1)); + parent.addChild(copy, index + i); + } + } + }); + + ComponentConfigDialog.hideDialog(); + } + }); + split.setEnabled(((FinSet) component).getFinCount() > 1); + + if (convert == null) + addButtons(split); + else + addButtons(split, convert); + + } + + public JPanel finTabPanel() { + JPanel panel = new JPanel( + new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%", + "[150lp::][65lp::][30lp::][200lp::]", "")); + // JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel", + // "[40lp][80lp::][30lp::][100lp::]","")); + + //// Through-the-wall fin tabs: + panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD), + "spanx, wrap 30lp"); + + JLabel label; + DoubleModel length; + DoubleModel length2; + DoubleModel length_2; + JSpinner spin; + JButton autoCalc; + + length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0); + length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0); + + register(length); + register(length2); + register(length_2); + + //// Tab length + //// Tab length: + label = new JLabel(trans.get("FinSetConfig.lbl.Tablength")); + //// The length of the fin tab. + label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength")); + panel.add(label, "gapleft para, gapright 40lp, growx 1"); + + final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(mtl.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx 1"); + + panel.add(new UnitSelector(mtl), "growx 1"); + panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)), + "w 100lp, growx 5, wrap"); + + + //// Tab length + //// Tab height: + label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight")); + //// The spanwise height of the fin tab. + label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight")); + panel.add(label, "gapleft para"); + + final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(mth.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(mth), "growx"); + panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)), + "w 100lp, growx 5, wrap"); + + //// Tab position: + label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition")); + //// The position of the fin tab. + label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition")); + panel.add(label, "gapleft para"); + + final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(mts.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(mts), "growx"); + panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap"); + + + //// relative to + label = new JLabel(trans.get("FinSetConfig.lbl.relativeto")); + panel.add(label, "right, gapright unrel"); + + final EnumModel em = + new EnumModel(component, "TabRelativePosition"); + + panel.add(new JComboBox(em), "spanx 3, growx, wrap para"); + + + // Calculate fin tab height, length, and position + autoCalc = new JButton(trans.get("FinSetConfig.but.AutoCalc")); + + autoCalc.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Computing " + component.getComponentName() + " tab height."); + + RocketComponent parent = component.getParent(); + if (parent instanceof Coaxial) { + try { + document.startUndo("Compute fin tabs"); + + List rings = new ArrayList(); + //Do deep recursive iteration + Iterator iter = parent.iterator(false); + while (iter.hasNext()) { + RocketComponent rocketComponent = iter.next(); + if (rocketComponent instanceof InnerTube) { + InnerTube it = (InnerTube) rocketComponent; + if (it.isMotorMount()) { + double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius(); + //Set fin tab depth + if (depth >= 0.0d) { + mth.setValue(depth); + mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit()); + } + } + } else if (rocketComponent instanceof CenteringRing) { + rings.add((CenteringRing) rocketComponent); + } + } + //Figure out position and length of the fin tab + if (!rings.isEmpty()) { + FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem(); + em.setSelectedItem(FinSet.TabRelativePosition.FRONT); + double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP, parent), + component.getLength(), mts, parent); + mtl.setValue(len); + //Be nice to the user and set the tab relative position enum back the way they had it. + em.setSelectedItem(temp); + } + + } finally { + document.stopUndo(); + } + } + } + }); + panel.add(autoCalc, "skip 1, spanx"); + + return panel; + } + + /** + * Scenarios: + *

+ * 1. All rings ahead of start of fin. + * 2. First ring ahead of start of fin. Second ring ahead of end of fin. + * 3. First ring ahead of start of fin. Second ring behind end of fin. + * 4. First ring equal or behind start of fin. Second ring ahead of, or equal to, end of fin. + * 5. First ring equal or behind start of fin. Second ring behind end of fin. + * 6. All rings behind end of fin. + * + * @param rings an unordered list of centering rings attached to the parent of the fin set + * @param finPositionFromTop the position from the top of the parent of the start of the fin set root + * @param finLength the length of the root chord + * @param mts the model for the tab shift (position); the model's value is modified as a result of this method call + * @param relativeTo the parent component of the finset + * + * @return the length of the fin tab + */ + private static double computeFinTabLength(List rings, Double finPositionFromTop, Double finLength, DoubleModel mts, + final RocketComponent relativeTo) { + List positionsFromTop = new ArrayList(); + + //Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here. + SortableRing top = null; + SortableRing bottom = null; + + if (rings != null) { + //Sort rings from top of parent to bottom + Collections.sort(rings, new Comparator() { + @Override + public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) { + return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo) - + centeringRing1.asPositionValue(RocketComponent.Position.TOP, relativeTo))); + } + }); + + for (int i = 0; i < rings.size(); i++) { + CenteringRing centeringRing = rings.get(i); + //Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring. + if (!positionsFromTop.isEmpty() && + positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >= + centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo)) { + SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1); + adjacent.merge(centeringRing, relativeTo); + } else { + positionsFromTop.add(new SortableRing(centeringRing, relativeTo)); + } + } + + for (int i = 0; i < positionsFromTop.size(); i++) { + SortableRing sortableRing = positionsFromTop.get(i); + if (top == null) { + top = sortableRing; + } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) { + top = sortableRing; + bottom = null; + } else if (top.bottomSidePositionFromTop() <= finPositionFromTop) { + if (bottom == null) { + //If the current ring is in the upper half of the root chord, make it the top ring + if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) { + top = sortableRing; + } else { + bottom = sortableRing; + } + } + //Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring, + //and the current ring the bottom + else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) { + top = bottom; + bottom = sortableRing; + } + } else { + if (bottom == null) { + bottom = sortableRing; + } + } + } + } + + double resultFinTabLength = 0d; + + // Edge case where there are no centering rings or for some odd reason top and bottom are identical. + if (top == null || top == bottom) { + mts.setValue(0); + resultFinTabLength = finLength; + } else if (bottom == null) { + // If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then + // set the position of the fin tab starting at the bottom side of the top ring. + if (top.bottomSidePositionFromTop() >= finPositionFromTop) { + mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop); + resultFinTabLength = (finPositionFromTop + finLength - top.bottomSidePositionFromTop()); + } else { + mts.setValue(0); + double diffLen = top.positionFromTop() - finPositionFromTop; + if (diffLen < 0) { + // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire + // root chord. + resultFinTabLength = finLength; + } + else { + // Otherwise there is one ring within the span. Return the length from the start of the fin to the top + // side of the ring. + resultFinTabLength = diffLen; + } + } + } + // If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the + // fin tab align with the start of the root chord. + else if (top.bottomSidePositionFromTop() < finPositionFromTop) { + mts.setValue(0); + + double lenToBottomRing = bottom.positionFromTop - finPositionFromTop; + // If the bottom ring lies farther back (down) than the trailing edge of the fin, then the tab should + // only be as long as the fin. + if (lenToBottomRing > finLength) { + resultFinTabLength = finLength; + } + else { + resultFinTabLength = lenToBottomRing; + } + } else { + mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop); + // The bottom ring is beyond the trailing edge of the fin. + if (bottom.positionFromTop() > finLength + finPositionFromTop) { + resultFinTabLength = (finLength + finPositionFromTop - top.bottomSidePositionFromTop()); + } + // The rings are within the span of the root chord. Place the tab between them. + else { + resultFinTabLength = (bottom.positionFromTop() - top.bottomSidePositionFromTop()); + } + } + if (resultFinTabLength < 0) { + resultFinTabLength = 0d; + } + return resultFinTabLength; + } + + @Override + public void updateFields() { + super.updateFields(); + if (split != null) + split.setEnabled(((FinSet) component).getFinCount() > 1); + } + + /** + * A container class to store pertinent info about centering rings. This is used in the computation to figure + * out tab length and position. + */ + static class SortableRing { + + /** + * The length of the ring (more commonly called the thickness). + */ + private double thickness; + /** + * The position of the ring from the top of the parent. + */ + private double positionFromTop; + + /** + * Constructor. + * + * @param r the source centering ring + */ + SortableRing(CenteringRing r, RocketComponent relativeTo) { + thickness = r.getLength(); + positionFromTop = r.asPositionValue(RocketComponent.Position.TOP, relativeTo); + } + + /** + * Merge an adjacent ring. + * + * @param adjacent the adjacent ring + */ + public void merge(CenteringRing adjacent, RocketComponent relativeTo) { + double v = adjacent.asPositionValue(RocketComponent.Position.TOP, relativeTo); + if (positionFromTop < v) { + thickness = (v + adjacent.getLength()) - positionFromTop; + } else { + double tmp = positionFromTop + thickness; + positionFromTop = v; + thickness = tmp - v; + } + } + + /** + * Compute the position of the bottom edge of the ring, relative to the top of the parent. + * + * @return the distance from the top of the parent to the bottom edge of the ring + */ + public double bottomSidePositionFromTop() { + return positionFromTop + thickness; + } + + /** + * Compute the position of the top edge of the ring, relative to the top of the parent. + * + * @return the distance from the top of the parent to the top edge of the ring + */ + public double positionFromTop() { + return positionFromTop; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/core/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java new file mode 100644 index 00000000..56609bcf --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java @@ -0,0 +1,494 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.Point; +import java.awt.event.MouseEvent; +import java.awt.geom.Point2D; + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.JTable; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.table.AbstractTableModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.IntegerModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.scalefigure.FinPointFigure; +import net.sf.openrocket.gui.scalefigure.ScaleScrollPane; +import net.sf.openrocket.gui.scalefigure.ScaleSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.IllegalFinPointException; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Coordinate; + +public class FreeformFinSetConfig extends FinSetConfig { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private final FreeformFinSet finset; + private JTable table = null; + private FinPointTableModel tableModel = null; + + private FinPointFigure figure = null; + + + public FreeformFinSetConfig(OpenRocketDocument d, RocketComponent component) { + super(d, component); + this.finset = (FreeformFinSet) component; + + //// General and General properties + tabbedPane.insertTab(trans.get("FreeformFinSetCfg.tab.General"), null, generalPane(), + trans.get("FreeformFinSetCfg.tab.ttip.General"), 0); + //// Shape and Fin shape + tabbedPane.insertTab(trans.get("FreeformFinSetCfg.tab.Shape"), null, shapePane(), + trans.get("FreeformFinSetCfg.tab.ttip.Finshape"), 1); + tabbedPane.setSelectedIndex(0); + + addFinSetButtons(); + } + + + + private JPanel generalPane() { + + DoubleModel m; + JSpinner spin; + JComboBox combo; + + JPanel mainPanel = new JPanel(new MigLayout("fill")); + + JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel", "[][65lp::][30lp::]", "")); + + + + //// Number of fins: + panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Numberoffins"))); + + IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); + + spin = new JSpinner(im.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx, wrap"); + + + //// Base rotation + panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Finrotation"))); + + m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + + //// Fin cant + JLabel label = new JLabel(trans.get("FreeformFinSetCfg.lbl.Fincant")); + //// The angle that the fins are canted with respect to the rocket body. + label.setToolTipText(trans.get("FreeformFinSetCfg.lbl.ttip.Fincant")); + panel.add(label); + + m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE, + -FinSet.MAX_CANT, FinSet.MAX_CANT); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT, FinSet.MAX_CANT)), + "w 100lp, wrap 40lp"); + + + + //// Position + //// Position relative to: + panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Posrelativeto"))); + + combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx 3, growx, wrap"); + //// plus + panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + + + + + mainPanel.add(panel, "aligny 20%"); + mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy, height 150lp"); + + + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + + + + //// Cross section + //// Fin cross section: + panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.FincrossSection")), "span, split"); + combo = new JComboBox( + new EnumModel(component, "CrossSection")); + panel.add(combo, "growx, wrap unrel"); + + + //// Thickness: + panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Thickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 30lp"); + + + //// Material + materialPanel(panel, Material.Type.BULK); + + + + mainPanel.add(panel, "aligny 20%"); + + return mainPanel; + } + + + + private JPanel shapePane() { + JPanel panel = new JPanel(new MigLayout("fill")); + + + // Create the figure + figure = new FinPointFigure(finset); + ScaleScrollPane figurePane = new FinPointScrollPane(); + figurePane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); + figurePane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + + // Create the table + tableModel = new FinPointTableModel(); + table = new JTable(tableModel); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + for (int i = 0; i < Columns.values().length; i++) { + table.getColumnModel().getColumn(i). + setPreferredWidth(Columns.values()[i].getWidth()); + } + JScrollPane tablePane = new JScrollPane(table); + + + // panel.add(new JLabel("Coordinates:"), "aligny bottom, alignx 50%"); + // panel.add(new JLabel(" View:"), "wrap, aligny bottom"); + + + panel.add(tablePane, "growy, width 100lp:100lp:, height 100lp:250lp:"); + panel.add(figurePane, "gap unrel, spanx, growx, growy 1000, height 100lp:250lp:, wrap"); + + panel.add(new StyledLabel(trans.get("lbl.doubleClick1"), -2), "alignx 50%"); + + panel.add(new ScaleSelector(figurePane), "spany 2"); + panel.add(new StyledLabel(trans.get("FreeformFinSetConfig.lbl.clickDrag") + " " + + trans.get("FreeformFinSetConfig.lbl.ctrlClick"), -2), "spany 2, right, wrap"); + + + panel.add(new StyledLabel(trans.get("FreeformFinSetConfig.lbl.doubleClick2"), -2), "alignx 50%"); + + return panel; + } + + + + + + @Override + public void updateFields() { + super.updateFields(); + + if (tableModel != null) { + tableModel.fireTableDataChanged(); + } + if (figure != null) { + figure.updateFigure(); + } + } + + + + + private class FinPointScrollPane extends ScaleScrollPane { + private static final int ANY_MASK = + (MouseEvent.ALT_DOWN_MASK | MouseEvent.ALT_GRAPH_DOWN_MASK | + MouseEvent.META_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK | + MouseEvent.SHIFT_DOWN_MASK); + + private int dragIndex = -1; + + public FinPointScrollPane() { + super(figure, false); // Disallow fitting as it's buggy + } + + @Override + public void mousePressed(MouseEvent event) { + int mods = event.getModifiersEx(); + + if (event.getButton() != MouseEvent.BUTTON1 || + (mods & ANY_MASK) != 0) { + super.mousePressed(event); + return; + } + + int index = getPoint(event); + if (index >= 0) { + dragIndex = index; + return; + } + index = getSegment(event); + if (index >= 0) { + Point2D.Double point = getCoordinates(event); + finset.addPoint(index); + try { + finset.setPoint(index, point.x, point.y); + } catch (IllegalFinPointException ignore) { + } + dragIndex = index; + + return; + } + + super.mousePressed(event); + return; + } + + + @Override + public void mouseDragged(MouseEvent event) { + int mods = event.getModifiersEx(); + if (dragIndex < 0 || + (mods & (ANY_MASK | MouseEvent.BUTTON1_DOWN_MASK)) != + MouseEvent.BUTTON1_DOWN_MASK) { + super.mouseDragged(event); + return; + } + Point2D.Double point = getCoordinates(event); + + try { + finset.setPoint(dragIndex, point.x, point.y); + } catch (IllegalFinPointException ignore) { + log.debug("Ignoring IllegalFinPointException while dragging, dragIndex=" + dragIndex + + " x=" + point.x + " y=" + point.y); + } + } + + + @Override + public void mouseReleased(MouseEvent event) { + dragIndex = -1; + super.mouseReleased(event); + } + + @Override + public void mouseClicked(MouseEvent event) { + int mods = event.getModifiersEx(); + if (event.getButton() != MouseEvent.BUTTON1 || + (mods & ANY_MASK) != MouseEvent.CTRL_DOWN_MASK) { + super.mouseClicked(event); + return; + } + + int index = getPoint(event); + if (index < 0) { + super.mouseClicked(event); + return; + } + + try { + finset.removePoint(index); + } catch (IllegalFinPointException ignore) { + } + } + + + private int getPoint(MouseEvent event) { + Point p0 = event.getPoint(); + Point p1 = this.getViewport().getViewPosition(); + int x = p0.x + p1.x; + int y = p0.y + p1.y; + + return figure.getIndexByPoint(x, y); + } + + private int getSegment(MouseEvent event) { + Point p0 = event.getPoint(); + Point p1 = this.getViewport().getViewPosition(); + int x = p0.x + p1.x; + int y = p0.y + p1.y; + + return figure.getSegmentByPoint(x, y); + } + + private Point2D.Double getCoordinates(MouseEvent event) { + Point p0 = event.getPoint(); + Point p1 = this.getViewport().getViewPosition(); + int x = p0.x + p1.x; + int y = p0.y + p1.y; + + return figure.convertPoint(x, y); + } + + + } + + + + + + private enum Columns { + // NUMBER { + // @Override + // public String toString() { + // return "#"; + // } + // @Override + // public String getValue(FreeformFinSet finset, int row) { + // return "" + (row+1) + "."; + // } + // @Override + // public int getWidth() { + // return 10; + // } + // }, + X { + @Override + public String toString() { + return "X / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString(); + } + + @Override + public String getValue(FreeformFinSet finset, int row) { + return UnitGroup.UNITS_LENGTH.getDefaultUnit() + .toString(finset.getFinPoints()[row].x); + } + }, + Y { + @Override + public String toString() { + return "Y / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString(); + } + + @Override + public String getValue(FreeformFinSet finset, int row) { + return UnitGroup.UNITS_LENGTH.getDefaultUnit() + .toString(finset.getFinPoints()[row].y); + } + }; + + public abstract String getValue(FreeformFinSet finset, int row); + + @Override + public abstract String toString(); + + public int getWidth() { + return 20; + } + } + + private class FinPointTableModel extends AbstractTableModel { + + @Override + public int getColumnCount() { + return Columns.values().length; + } + + @Override + public int getRowCount() { + return finset.getPointCount(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return Columns.values()[columnIndex].getValue(finset, rowIndex); + } + + @Override + public String getColumnName(int columnIndex) { + return Columns.values()[columnIndex].toString(); + } + + @Override + public boolean isCellEditable(int rowIndex, int columnIndex) { + if (rowIndex == 0 || rowIndex == getRowCount() - 1) { + return (columnIndex == Columns.X.ordinal()); + } + + return (columnIndex == Columns.X.ordinal() || columnIndex == Columns.Y.ordinal()); + } + + @Override + public void setValueAt(Object o, int rowIndex, int columnIndex) { + if (!(o instanceof String)) + return; + + if (rowIndex < 0 || rowIndex >= finset.getFinPoints().length || + columnIndex < 0 || columnIndex >= Columns.values().length) { + throw new IllegalArgumentException("Index out of bounds, row=" + rowIndex + + " column=" + columnIndex + " fin point count=" + finset.getFinPoints().length); + } + + String str = (String) o; + try { + + double value = UnitGroup.UNITS_LENGTH.fromString(str); + Coordinate c = finset.getFinPoints()[rowIndex]; + if (columnIndex == Columns.X.ordinal()) + c = c.setX(value); + else + c = c.setY(value); + + finset.setPoint(rowIndex, c.x, c.y); + + } catch (NumberFormatException ignore) { + } catch (IllegalFinPointException ignore) { + } + } + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java b/core/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java new file mode 100644 index 00000000..b7c0afea --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java @@ -0,0 +1,324 @@ +package net.sf.openrocket.gui.configdialog; + + + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.geom.Ellipse2D; +import java.util.EventObject; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SwingUtilities; +import javax.swing.border.BevelBorder; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.Resettable; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.ClusterConfiguration; +import net.sf.openrocket.rocketcomponent.Clusterable; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.StateChangeListener; + + +public class InnerTubeConfig extends ThicknessRingComponentConfig { + private static final Translator trans = Application.getTranslator(); + + + public InnerTubeConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel tab; + + tab = new MotorConfig((MotorMount) c); + //// Motor and Motor mount configuration + tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Motor"), null, tab, + trans.get("InnerTubeCfg.tab.ttip.Motor"), 1); + + tab = clusterTab(); + //// Cluster and Cluster configuration + tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Cluster"), null, tab, + trans.get("InnerTubeCfg.tab.ttip.Cluster"), 2); + + tab = positionTab(); + //// Radial position + tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Radialpos"), null, tab, + trans.get("InnerTubeCfg.tab.ttip.Radialpos"), 3); + + tabbedPane.setSelectedIndex(0); + } + + + private JPanel clusterTab() { + JPanel panel = new JPanel(new MigLayout()); + + JPanel subPanel = new JPanel(new MigLayout()); + + // Cluster type selection + //// Select cluster configuration: + subPanel.add(new JLabel(trans.get("InnerTubeCfg.lbl.Selectclustercfg")), "spanx, wrap"); + subPanel.add(new ClusterSelectionPanel((InnerTube) component), "spanx, wrap"); + // JPanel clusterSelection = new ClusterSelectionPanel((InnerTube)component); + // clusterSelection.setBackground(Color.blue); + // subPanel.add(clusterSelection); + + panel.add(subPanel); + + + subPanel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]")); + + // Tube separation scale + //// Tube separation: + JLabel l = new JLabel(trans.get("InnerTubeCfg.lbl.TubeSep")); + //// The separation of the tubes, 1.0 = touching each other + l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); + subPanel.add(l); + DoubleModel dm = new DoubleModel(component, "ClusterScale", 1, UnitGroup.UNITS_NONE, 0); + + JSpinner spin = new JSpinner(dm.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + //// The separation of the tubes, 1.0 = touching each other + spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); + subPanel.add(spin, "growx"); + + BasicSlider bs = new BasicSlider(dm.getSliderModel(0, 1, 4)); + //// The separation of the tubes, 1.0 = touching each other + bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); + subPanel.add(bs, "skip,w 100lp, wrap"); + + // Rotation: + l = new JLabel(trans.get("InnerTubeCfg.lbl.Rotation")); + //// Rotation angle of the cluster configuration + l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); + subPanel.add(l); + dm = new DoubleModel(component, "ClusterRotation", 1, UnitGroup.UNITS_ANGLE, + -Math.PI, Math.PI); + + spin = new JSpinner(dm.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + //// Rotation angle of the cluster configuration + spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); + subPanel.add(spin, "growx"); + + subPanel.add(new UnitSelector(dm), "growx"); + bs = new BasicSlider(dm.getSliderModel(-Math.PI, 0, Math.PI)); + //// Rotation angle of the cluster configuration + bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); + subPanel.add(bs, "w 100lp, wrap para"); + + + + // Split button + //// Split cluster + JButton split = new JButton(trans.get("InnerTubeCfg.but.Splitcluster")); + //// Split the cluster into separate components.
+ //// This also duplicates all components attached to this inner tube. + split.setToolTipText(trans.get("InnerTubeCfg.lbl.longA1") + + trans.get("InnerTubeCfg.lbl.longA2")); + split.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // Do change in future for overall safety + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + RocketComponent parent = component.getParent(); + int index = parent.getChildPosition(component); + if (index < 0) { + throw new BugException("Inconsistent state: component=" + component + + " parent=" + parent + " parent.children=" + parent.getChildren()); + } + + InnerTube tube = (InnerTube) component; + if (tube.getClusterCount() <= 1) + return; + + document.addUndoPosition("Split cluster"); + + Coordinate[] coords = { Coordinate.NUL }; + coords = component.shiftCoordinates(coords); + parent.removeChild(index); + for (int i = 0; i < coords.length; i++) { + InnerTube copy = (InnerTube) component.copy(); + copy.setClusterConfiguration(ClusterConfiguration.SINGLE); + copy.setClusterRotation(0.0); + copy.setClusterScale(1.0); + copy.setRadialShift(coords[i].y, coords[i].z); + copy.setName(copy.getName() + " #" + (i + 1)); + + parent.addChild(copy, index + i); + } + } + }); + } + }); + subPanel.add(split, "spanx, split 2, gapright para, sizegroup buttons, right"); + + + // Reset button + ///// Reset settings + JButton reset = new JButton(trans.get("InnerTubeCfg.but.Resetsettings")); + //// Reset the separation and rotation to the default values + reset.setToolTipText(trans.get("InnerTubeCfg.but.ttip.Resetsettings")); + reset.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + ((InnerTube) component).setClusterScale(1.0); + ((InnerTube) component).setClusterRotation(0.0); + } + }); + subPanel.add(reset, "sizegroup buttons, right"); + + panel.add(subPanel, "grow"); + + + return panel; + } +} + + +class ClusterSelectionPanel extends JPanel { + private static final int BUTTON_SIZE = 50; + private static final int MOTOR_DIAMETER = 10; + + private static final Color SELECTED_COLOR = Color.RED; + private static final Color UNSELECTED_COLOR = Color.WHITE; + private static final Color MOTOR_FILL_COLOR = Color.GREEN; + private static final Color MOTOR_BORDER_COLOR = Color.BLACK; + + public ClusterSelectionPanel(Clusterable component) { + super(new MigLayout("gap 0 0", + "[" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!]", + "[" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!]")); + + for (int i = 0; i < ClusterConfiguration.CONFIGURATIONS.length; i++) { + ClusterConfiguration config = ClusterConfiguration.CONFIGURATIONS[i]; + + JComponent button = new ClusterButton(component, config); + if (i % 4 == 3) + add(button, "wrap"); + else + add(button); + } + + } + + + private class ClusterButton extends JPanel implements StateChangeListener, MouseListener, + Resettable { + private Clusterable component; + private ClusterConfiguration config; + + public ClusterButton(Clusterable c, ClusterConfiguration config) { + component = c; + this.config = config; + setMinimumSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); + setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); + setMaximumSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); + setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); + // setBorder(BorderFactory.createLineBorder(Color.BLACK, 1)); + component.addChangeListener(this); + addMouseListener(this); + } + + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + Rectangle area = g2.getClipBounds(); + + if (component.getClusterConfiguration() == config) + g2.setColor(SELECTED_COLOR); + else + g2.setColor(UNSELECTED_COLOR); + + g2.fillRect(area.x, area.y, area.width, area.height); + + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + List points = config.getPoints(); + Ellipse2D.Float circle = new Ellipse2D.Float(); + for (int i = 0; i < points.size() / 2; i++) { + double x = points.get(i * 2); + double y = points.get(i * 2 + 1); + + double px = BUTTON_SIZE / 2 + x * MOTOR_DIAMETER; + double py = BUTTON_SIZE / 2 - y * MOTOR_DIAMETER; + circle.setFrameFromCenter(px, py, px + MOTOR_DIAMETER / 2, py + MOTOR_DIAMETER / 2); + + g2.setColor(MOTOR_FILL_COLOR); + g2.fill(circle); + g2.setColor(MOTOR_BORDER_COLOR); + g2.draw(circle); + } + } + + + @Override + public void stateChanged(EventObject e) { + repaint(); + } + + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1) { + component.setClusterConfiguration(config); + } + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mousePressed(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + + @Override + public void resetModel() { + component.removeChangeListener(this); + removeMouseListener(this); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java b/core/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java new file mode 100644 index 00000000..947b205a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java @@ -0,0 +1,162 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class LaunchLugConfig extends RocketComponentConfig { + + private MotorConfig motorConfigPane = null; + private static final Translator trans = Application.getTranslator(); + + public LaunchLugConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel primary = new JPanel(new MigLayout("fill")); + + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + //// Body tube length + //// Length: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Length"))); + + DoubleModel m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.1)), "w 100lp, wrap para"); + + + //// Body tube diameter + //// Outer diameter: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Outerdiam"))); + + DoubleModel od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap rel"); + + + //// Inner diameter: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Innerdiam"))); + + // Diameter = 2*Radius + m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); + + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), "w 100lp, wrap rel"); + + + //// Wall thickness + //// Thickness: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Thickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 20lp"); + + + //// Radial position: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Radialpos"))); + + m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, + -Math.PI, Math.PI); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + + + primary.add(panel, "grow, gapright 20lp"); + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + + + //// Position relative to: + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Posrelativeto"))); + + JComboBox combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + + //// plus + panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap para"); + + + + //// Material + materialPanel(panel, Material.Type.BULK); + + + primary.add(panel, "grow"); + + //// General and General properties + tabbedPane.insertTab(trans.get("LaunchLugCfg.tab.General"), null, primary, + trans.get("LaunchLugCfg.tab.Generalprop"), 0); + tabbedPane.setSelectedIndex(0); + } + + @Override + public void updateFields() { + super.updateFields(); + if (motorConfigPane != null) + motorConfigPane.updateFields(); + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java b/core/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java new file mode 100644 index 00000000..b199d209 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java @@ -0,0 +1,160 @@ +package net.sf.openrocket.gui.configdialog; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + + +public class MassComponentConfig extends RocketComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public MassComponentConfig(OpenRocketDocument d, RocketComponent component) { + super(d, component); + + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + + + //// Mass + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Mass"))); + + DoubleModel m = new DoubleModel(component, "ComponentMass", UnitGroup.UNITS_MASS, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.5)), "w 100lp, wrap"); + + + + //// Mass length + //// Length + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Length"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); + + + //// Tube diameter + //// Diameter: + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Diameter"))); + + DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); + + + //// Position + //// Position relative to: + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.PosRelativeto"))); + + JComboBox combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + //// plus + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + //// General and General properties + tabbedPane.insertTab(trans.get("MassComponentCfg.tab.General"), null, panel, + trans.get("MassComponentCfg.tab.ttip.General"), 0); + //// Radial position and Radial position configuration + tabbedPane.insertTab(trans.get("MassComponentCfg.tab.Radialpos"), null, positionTab(), + trans.get("MassComponentCfg.tab.ttip.Radialpos"), 1); + tabbedPane.setSelectedIndex(0); + } + + + protected JPanel positionTab() { + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + //// Radial position + //// Radial distance: + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Radialdistance"))); + + DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); + + + //// Radial direction: + panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Radialdirection"))); + + m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + //// Reset button + JButton button = new JButton(trans.get("MassComponentCfg.but.Reset")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ((MassComponent) component).setRadialDirection(0.0); + ((MassComponent) component).setRadialPosition(0.0); + } + }); + panel.add(button, "spanx, right"); + + return panel; + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/MotorConfig.java b/core/src/net/sf/openrocket/gui/configdialog/MotorConfig.java new file mode 100644 index 00000000..227a475c --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/MotorConfig.java @@ -0,0 +1,243 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.Component; +import java.awt.Container; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.dialogs.motor.MotorChooserDialog; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class MotorConfig extends JPanel { + + private final Rocket rocket; + private final MotorMount mount; + private final Configuration configuration; + private JPanel panel; + private JLabel motorLabel; + private static final Translator trans = Application.getTranslator(); + + public MotorConfig(MotorMount motorMount) { + super(new MigLayout("fill")); + + this.rocket = ((RocketComponent) motorMount).getRocket(); + this.mount = motorMount; + this.configuration = ((RocketComponent) motorMount).getRocket().getDefaultConfiguration(); + + BooleanModel model; + + model = new BooleanModel(motorMount, "MotorMount"); + JCheckBox check = new JCheckBox(model); + ////This component is a motor mount + check.setText(trans.get("MotorCfg.checkbox.compmotormount")); + this.add(check, "wrap"); + + + panel = new JPanel(new MigLayout("fill")); + this.add(panel, "grow, wrap"); + + + // Motor configuration selector + //// Motor configuration: + panel.add(new JLabel(trans.get("MotorCfg.lbl.Motorcfg")), "shrink"); + + JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration)); + panel.add(combo, "growx"); + + configuration.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + updateFields(); + } + }); + + //// New button + JButton button = new JButton(trans.get("MotorCfg.but.New")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String id = rocket.newMotorConfigurationID(); + configuration.setMotorConfigurationID(id); + } + }); + panel.add(button, "wrap unrel"); + + + // Current motor: + panel.add(new JLabel(trans.get("MotorCfg.lbl.Currentmotor")), "shrink"); + + motorLabel = new JLabel(); + motorLabel.setFont(motorLabel.getFont().deriveFont(Font.BOLD)); + updateFields(); + panel.add(motorLabel, "wrap unrel"); + + + + // Overhang + //// Motor overhang: + panel.add(new JLabel(trans.get("MotorCfg.lbl.Motoroverhang"))); + + DoubleModel dm = new DoubleModel(motorMount, "MotorOverhang", UnitGroup.UNITS_LENGTH); + + JSpinner spin = new JSpinner(dm.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "span, split, width :65lp:"); + + panel.add(new UnitSelector(dm), "width :30lp:"); + panel.add(new BasicSlider(dm.getSliderModel(-0.02, 0.06)), "w 100lp, wrap unrel"); + + + + // Select ignition event + //// Ignition at: + panel.add(new JLabel(trans.get("MotorCfg.lbl.Ignitionat")), ""); + + combo = new JComboBox(new EnumModel(mount, "IgnitionEvent")); + panel.add(combo, "growx, wrap"); + + // ... and delay + //// plus + panel.add(new JLabel(trans.get("MotorCfg.lbl.plus")), "gap indent, skip 1, span, split"); + + dm = new DoubleModel(mount, "IgnitionDelay", 0); + spin = new JSpinner(dm.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "gap rel rel"); + + //// seconds + panel.add(new JLabel(trans.get("MotorCfg.lbl.seconds")), "wrap unrel"); + + + + // Check stage count + RocketComponent c = (RocketComponent) mount; + c = c.getRocket(); + int stages = c.getChildCount(); + + if (stages == 1) { + //// The current design has only one stage. + //// Stages can be added by clicking \"New stage\". + + panel.add(new StyledLabel(trans.get("MotorCfg.lbl.longA1") + " " + + trans.get("MotorCfg.lbl.longA2"), -1), + "spanx, right, wrap para"); + } else { + //// The current design has + //// stages. + panel.add(new StyledLabel(trans.get("MotorCfg.lbl.longB1") + " " + stages + " " + + trans.get("MotorCfg.lbl.longB2"), -1), + "skip 1, spanx, wrap para"); + } + + + // Select etc. buttons + //// Select motor + button = new JButton(trans.get("MotorCfg.but.Selectmotor")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String id = configuration.getMotorConfigurationID(); + + MotorChooserDialog dialog = new MotorChooserDialog(mount.getMotor(id), + mount.getMotorDelay(id), mount.getMotorMountDiameter(), + SwingUtilities.getWindowAncestor(MotorConfig.this)); + dialog.setVisible(true); + Motor m = dialog.getSelectedMotor(); + double d = dialog.getSelectedDelay(); + + if (m != null) { + if (id == null) { + id = rocket.newMotorConfigurationID(); + configuration.setMotorConfigurationID(id); + } + mount.setMotor(id, m); + mount.setMotorDelay(id, d); + } + updateFields(); + } + }); + panel.add(button, "span, split, growx"); + + //// Remove motor + button = new JButton(trans.get("MotorCfg.but.Removemotor")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + mount.setMotor(configuration.getMotorConfigurationID(), null); + updateFields(); + } + }); + panel.add(button, "growx, wrap"); + + + + + + // Set enabled status + + setDeepEnabled(panel, motorMount.isMotorMount()); + check.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + setDeepEnabled(panel, mount.isMotorMount()); + } + }); + + } + + public void updateFields() { + String id = configuration.getMotorConfigurationID(); + Motor m = mount.getMotor(id); + if (m == null) { + //// None + motorLabel.setText(trans.get("MotorCfg.lbl.motorLabel")); + } else { + String str = ""; + if (m instanceof ThrustCurveMotor) + str = ((ThrustCurveMotor) m).getManufacturer() + " "; + str += m.getDesignation(mount.getMotorDelay(id)); + motorLabel.setText(str); + } + } + + + private static void setDeepEnabled(Component component, boolean enabled) { + component.setEnabled(enabled); + if (component instanceof Container) { + for (Component c : ((Container) component).getComponents()) { + setDeepEnabled(c, enabled); + } + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java b/core/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java new file mode 100644 index 00000000..682f6d35 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java @@ -0,0 +1,183 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class NoseConeConfig extends RocketComponentConfig { + + private JComboBox typeBox; + + private DescriptionArea description; + + private JLabel shapeLabel; + private JSpinner shapeSpinner; + private JSlider shapeSlider; + private static final Translator trans = Application.getTranslator(); + + // Prepended to the description from NoseCone.DESCRIPTIONS + private static final String PREDESC = ""; + + public NoseConeConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + DoubleModel m; + JPanel panel = new JPanel(new MigLayout("", "[][65lp::][30lp::]")); + + + + + //// Shape selection + //// Nose cone shape: + panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Noseconeshape"))); + + Transition.Shape selected = ((NoseCone) component).getType(); + Transition.Shape[] typeList = Transition.Shape.values(); + + typeBox = new JComboBox(typeList); + typeBox.setEditable(false); + typeBox.setSelectedItem(selected); + typeBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Transition.Shape s = (Transition.Shape) typeBox.getSelectedItem(); + ((NoseCone) component).setType(s); + description.setText(PREDESC + s.getNoseConeDescription()); + updateEnabled(); + } + }); + panel.add(typeBox, "span, wrap rel"); + + + + + //// Shape parameter + //// Shape parameter: + shapeLabel = new JLabel(trans.get("NoseConeCfg.lbl.Shapeparam")); + panel.add(shapeLabel); + + m = new DoubleModel(component, "ShapeParameter"); + + shapeSpinner = new JSpinner(m.getSpinnerModel()); + shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner)); + panel.add(shapeSpinner, "growx"); + + DoubleModel min = new DoubleModel(component, "ShapeParameterMin"); + DoubleModel max = new DoubleModel(component, "ShapeParameterMax"); + shapeSlider = new BasicSlider(m.getSliderModel(min, max)); + panel.add(shapeSlider, "skip, w 100lp, wrap para"); + + updateEnabled(); + + + //// Length + //// Nose cone length: + panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Noseconelength"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.7)), "w 100lp, wrap"); + + //// Diameter + //// Base diameter: + panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Basediam"))); + + m = new DoubleModel(component, "AftRadius", 2.0, UnitGroup.UNITS_LENGTH, 0); // Diameter = 2*Radius + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); + + JCheckBox check = new JCheckBox(m.getAutomaticAction()); + //// Automatic + check.setText(trans.get("NoseConeCfg.checkbox.Automatic")); + panel.add(check, "skip, span 2, wrap"); + + + //// Wall thickness: + panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Wallthickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 0px"); + + + check = new JCheckBox(new BooleanModel(component, "Filled")); + //// Filled + check.setText(trans.get("NoseConeCfg.checkbox.Filled")); + panel.add(check, "skip, span 2, wrap"); + + + panel.add(new JLabel(""), "growy"); + + + + //// Description + + JPanel panel2 = new JPanel(new MigLayout("ins 0")); + + description = new DescriptionArea(5); + description.setText(PREDESC + ((NoseCone) component).getType().getNoseConeDescription()); + panel2.add(description, "wmin 250lp, spanx, growx, wrap para"); + + + //// Material + + + materialPanel(panel2, Material.Type.BULK); + panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany"); + + + //// General and General properties + tabbedPane.insertTab(trans.get("NoseConeCfg.tab.General"), null, panel, + trans.get("NoseConeCfg.tab.ttip.General"), 0); + //// Shoulder and Shoulder properties + tabbedPane.insertTab(trans.get("NoseConeCfg.tab.Shoulder"), null, shoulderTab(), + trans.get("NoseConeCfg.tab.ttip.Shoulder"), 1); + tabbedPane.setSelectedIndex(0); + } + + + private void updateEnabled() { + boolean e = ((NoseCone) component).getType().usesParameter(); + shapeLabel.setEnabled(e); + shapeSpinner.setEnabled(e); + shapeSlider.setEnabled(e); + } + + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java b/core/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java new file mode 100644 index 00000000..311b42af --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java @@ -0,0 +1,293 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.IntegerModel; +import net.sf.openrocket.gui.adaptors.MaterialModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.HtmlLabel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class ParachuteConfig extends RecoveryDeviceConfig { + private static final Translator trans = Application.getTranslator(); + + public ParachuteConfig(OpenRocketDocument d, final RocketComponent component) { + super(d, component); + + JPanel primary = new JPanel(new MigLayout()); + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + + //// Canopy + panel.add(new StyledLabel(trans.get("ParachuteCfg.lbl.Canopy"), Style.BOLD), "wrap unrel"); + + //// Diameter: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Diameter"))); + + DoubleModel m = new DoubleModel(component, "Diameter", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)), "w 100lp, wrap"); + + //// Material: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Material"))); + + JComboBox combo = new JComboBox(new MaterialModel(panel, component, + Material.Type.SURFACE)); + //// The component material affects the weight of the component. + combo.setToolTipText(trans.get("ParachuteCfg.combo.MaterialModel")); + panel.add(combo, "spanx 3, growx, wrap paragraph"); + + // materialPanel(panel, Material.Type.SURFACE, "Material:", null); + + + + // CD + //// Drag coefficient CD: + JLabel label = new HtmlLabel(trans.get("ParachuteCfg.lbl.longA1")); + String tip = trans.get("ParachuteCfg.lbl.longB1") + + trans.get("ParachuteCfg.lbl.longB2") + " " + + trans.get("ParachuteCfg.lbl.longB3"); + label.setToolTipText(tip); + panel.add(label); + + m = new DoubleModel(component, "CD", UnitGroup.UNITS_COEFFICIENT, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setToolTipText(tip); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + //// Reset button + JButton button = new JButton(trans.get("ParachuteCfg.but.Reset")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Parachute p = (Parachute) component; + p.setCD(Parachute.DEFAULT_CD); + } + }); + panel.add(button, "spanx, wrap 30lp"); + + + + //// Shroud lines + panel.add(new StyledLabel(trans.get("ParachuteCfg.lbl.Shroudlines"), Style.BOLD), "wrap unrel"); + + //// Number of lines: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Numberoflines"))); + IntegerModel im = new IntegerModel(component, "LineCount", 0); + + spin = new JSpinner(im.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx, wrap"); + + //// Line length: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Linelength"))); + + m = new DoubleModel(component, "LineLength", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)), "w 100lp, wrap"); + + //// Material: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Material"))); + + combo = new JComboBox(new MaterialModel(panel, component, Material.Type.LINE, + "LineMaterial")); + panel.add(combo, "spanx 3, growx, wrap"); + + + + primary.add(panel, "grow, gapright 20lp"); + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + + + + //// Position + //// Position relative to: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Posrelativeto"))); + + combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + + //// plus + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + + //// Spatial length + //// Packed length: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Packedlength"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); + + + //// Tube diameter + //// Packed diameter: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Packeddiam"))); + + DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 30lp"); + + + //// Deployment + //// Deploys at: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Deploysat")), ""); + + combo = new JComboBox(new EnumModel(component, "DeployEvent")); + panel.add(combo, "spanx 3, growx, wrap"); + + // ... and delay + //// plus + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.plusdelay")), "right"); + + m = new DoubleModel(component, "DeployDelay", 0); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "spanx, split"); + + //// seconds + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.seconds")), "wrap paragraph"); + + // Altitude: + label = new JLabel(trans.get("ParachuteCfg.lbl.Altitude")); + altitudeComponents.add(label); + panel.add(label); + + m = new DoubleModel(component, "DeployAltitude", UnitGroup.UNITS_DISTANCE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + altitudeComponents.add(spin); + panel.add(spin, "growx"); + UnitSelector unit = new UnitSelector(m); + altitudeComponents.add(unit); + panel.add(unit, "growx"); + BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000)); + altitudeComponents.add(slider); + panel.add(slider, "w 100lp, wrap"); + + + primary.add(panel, "grow"); + + updateFields(); + + //// General and General properties + tabbedPane.insertTab(trans.get("ParachuteCfg.tab.General"), null, primary, trans.get("ParachuteCfg.tab.ttip.General"), 0); + //// Radial position and Radial position configuration + tabbedPane.insertTab(trans.get("ParachuteCfg.tab.Radialpos"), null, positionTab(), + trans.get("ParachuteCfg.tab.ttip.Radialpos"), 1); + tabbedPane.setSelectedIndex(0); + } + + + + + + protected JPanel positionTab() { + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + //// Radial position + //// Radial distance: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Radialdistance"))); + + DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); + + + //// Radial direction: + panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Radialdirection"))); + + m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + //// Reset button + JButton button = new JButton(trans.get("ParachuteCfg.but.Reset")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ((MassObject) component).setRadialDirection(0.0); + ((MassObject) component).setRadialPosition(0.0); + } + }); + panel.add(button, "spanx, right"); + + return panel; + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java b/core/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java new file mode 100644 index 00000000..f93613c9 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java @@ -0,0 +1,37 @@ +package net.sf.openrocket.gui.configdialog; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.JComponent; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.RocketComponent; + + +public abstract class RecoveryDeviceConfig extends RocketComponentConfig { + + protected final List altitudeComponents = new ArrayList(); + + public RecoveryDeviceConfig(OpenRocketDocument d, RocketComponent component) { + super(d, component); + } + + + + @Override + public void updateFields() { + super.updateFields(); + + if (altitudeComponents == null) + return; + + boolean enabled = (((RecoveryDevice) component).getDeployEvent() + == RecoveryDevice.DeployEvent.ALTITUDE); + + for (JComponent c : altitudeComponents) { + c.setEnabled(enabled); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java b/core/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java new file mode 100644 index 00000000..23906d6f --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java @@ -0,0 +1,243 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.RingComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class RingComponentConfig extends RocketComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public RingComponentConfig(OpenRocketDocument d, RocketComponent component) { + super(d, component); + } + + + protected JPanel generalTab(String outer, String inner, String thickness, String length) { + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + DoubleModel m; + JSpinner spin; + DoubleModel od = null; + + + //// Outer diameter + if (outer != null) { + panel.add(new JLabel(outer)); + + //// OuterRadius + od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); + + if (od.isAutomaticAvailable()) { + JCheckBox check = new JCheckBox(od.getAutomaticAction()); + //// Automatic + check.setText(trans.get("ringcompcfg.Automatic")); + panel.add(check, "skip, span 2, wrap"); + } + } + + + //// Inner diameter + if (inner != null) { + panel.add(new JLabel(inner)); + + //// InnerRadius + m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + if (od == null) + panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); + else + panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), + "w 100lp, wrap"); + + if (m.isAutomaticAvailable()) { + JCheckBox check = new JCheckBox(m.getAutomaticAction()); + //// Automatic + check.setText(trans.get("ringcompcfg.Automatic")); + panel.add(check, "skip, span 2, wrap"); + } + } + + + //// Wall thickness + if (thickness != null) { + panel.add(new JLabel(thickness)); + + //// Thickness + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap"); + } + + + //// Inner tube length + if (length != null) { + panel.add(new JLabel(length)); + + //// Length + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); + } + + + //// Position + + //// Position relative to: + panel.add(new JLabel(trans.get("ringcompcfg.Positionrelativeto"))); + + JComboBox combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx 3, growx, wrap"); + + //// plus + panel.add(new JLabel(trans.get("ringcompcfg.plus")), "right"); + + //// PositionValue + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + + //// Material + JPanel sub = materialPanel(new JPanel(new MigLayout()), Material.Type.BULK); + + if (component instanceof EngineBlock) { + final DescriptionArea desc = new DescriptionArea(6); + //// An engine block stops the motor from moving forwards in the motor mount tube.

In order to add a motor, create a body tube or inner tube and mark it as a motor mount in the Motor tab. + desc.setText(trans.get("ringcompcfg.EngineBlock.desc")); + sub.add(desc, "width 1px, growx, wrap"); + } + panel.add(sub, "cell 4 0, gapleft paragraph, aligny 0%, spany"); + + return panel; + } + + + protected JPanel positionTab() { + JPanel panel = new JPanel(new MigLayout("align 20% 20%, gap rel unrel", + "[][65lp::][30lp::]", "")); + + //// Radial position + JLabel l = new JLabel(trans.get("ringcompcfg.Radialdistance")); + //// Distance from the rocket centerline + l.setToolTipText(trans.get("ringcompcfg.Distancefrom")); + panel.add(l); + + DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + //// Distance from the rocket centerline + spin.setToolTipText(trans.get("ringcompcfg.Distancefrom")); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + BasicSlider bs = new BasicSlider(m.getSliderModel(0, 0.1, 1.0)); + //// Distance from the rocket centerline + bs.setToolTipText(trans.get("ringcompcfg.Distancefrom")); + panel.add(bs, "w 100lp, wrap"); + + + //// Radial direction + l = new JLabel(trans.get("ringcompcfg.Radialdirection")); + //// The radial direction from the rocket centerline + l.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); + panel.add(l); + + m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + //// The radial direction from the rocket centerline + spin.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + bs = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)); + //// The radial direction from the rocket centerline + bs.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); + panel.add(bs, "w 100lp, wrap"); + + + //// Reset button + JButton button = new JButton(trans.get("ringcompcfg.but.Reset")); + //// Reset the component to the rocket centerline + button.setToolTipText(trans.get("ringcompcfg.but.Resetcomponant")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ((RingComponent) component).setRadialDirection(0.0); + ((RingComponent) component).setRadialPosition(0.0); + } + }); + panel.add(button, "spanx, right, wrap para"); + + + DescriptionArea note = new DescriptionArea(3); + //// Note: An inner tube will not affect the aerodynamics of the rocket even if it is located outside of the body tube. + note.setText(trans.get("ringcompcfg.note.desc")); + panel.add(note, "spanx, growx"); + + + return panel; + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/core/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java new file mode 100644 index 00000000..95853b02 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java @@ -0,0 +1,640 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JColorChooser; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.MaterialModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.ColorIcon; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.util.ColorConversion; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.ComponentAssembly; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Invalidatable; +import net.sf.openrocket.util.LineStyle; + +public class RocketComponentConfig extends JPanel { + + private static final Translator trans = Application.getTranslator(); + + protected final OpenRocketDocument document; + protected final RocketComponent component; + protected final JTabbedPane tabbedPane; + + private final List invalidatables = new ArrayList(); + + + protected final JTextField componentNameField; + protected JTextArea commentTextArea; + private final TextFieldListener textFieldListener; + private JButton colorButton; + private JCheckBox colorDefault; + private JPanel buttonPanel; + + private JLabel massLabel; + + + public RocketComponentConfig(OpenRocketDocument document, RocketComponent component) { + setLayout(new MigLayout("fill", "[grow, fill]")); + this.document = document; + this.component = component; + + //// Component name: + JLabel label = new JLabel(trans.get("RocketCompCfg.lbl.Componentname")); + //// The component name. + label.setToolTipText(trans.get("RocketCompCfg.ttip.Thecomponentname")); + this.add(label, "split, gapright 10"); + + componentNameField = new JTextField(15); + textFieldListener = new TextFieldListener(); + componentNameField.addActionListener(textFieldListener); + componentNameField.addFocusListener(textFieldListener); + //// The component name. + componentNameField.setToolTipText(trans.get("RocketCompCfg.ttip.Thecomponentname")); + this.add(componentNameField, "growx, growy 0, wrap"); + + + tabbedPane = new JTabbedPane(); + this.add(tabbedPane, "growx, growy 1, wrap"); + + //// Override and Mass and CG override options + tabbedPane.addTab(trans.get("RocketCompCfg.tab.Override"), null, overrideTab(), + trans.get("RocketCompCfg.tab.MassandCGoverride")); + if (component.isMassive()) + //// Figure and Figure style options + tabbedPane.addTab(trans.get("RocketCompCfg.tab.Figure"), null, figureTab(), + trans.get("RocketCompCfg.tab.Figstyleopt")); + //// Comment and Specify a comment for the component + tabbedPane.addTab(trans.get("RocketCompCfg.tab.Comment"), null, commentTab(), + trans.get("RocketCompCfg.tab.Specifyacomment")); + + addButtons(); + + updateFields(); + } + + + protected void addButtons(JButton... buttons) { + if (buttonPanel != null) { + this.remove(buttonPanel); + } + + buttonPanel = new JPanel(new MigLayout("fill, ins 0")); + + //// Mass: + massLabel = new StyledLabel(trans.get("RocketCompCfg.lbl.Mass") + " ", -1); + buttonPanel.add(massLabel, "growx"); + + for (JButton b : buttons) { + buttonPanel.add(b, "right, gap para"); + } + + //// Close button + JButton closeButton = new JButton(trans.get("dlg.but.close")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + ComponentConfigDialog.hideDialog(); + } + }); + buttonPanel.add(closeButton, "right, gap 30lp"); + + updateFields(); + + this.add(buttonPanel, "spanx, growx"); + } + + + /** + * Called when a change occurs, so that the fields can be updated if necessary. + * When overriding this method, the supermethod must always be called. + */ + public void updateFields() { + // Component name + componentNameField.setText(component.getName()); + + // Component color and "Use default color" checkbox + if (colorButton != null && colorDefault != null) { + colorButton.setIcon(new ColorIcon(getColor())); + + if ((component.getColor() == null) != colorDefault.isSelected()) + colorDefault.setSelected(component.getColor() == null); + } + + // Mass label + if (component.isMassive()) { + //// Component mass: + String text = trans.get("RocketCompCfg.lbl.Componentmass") + " "; + text += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( + component.getComponentMass()); + + String overridetext = null; + if (component.isMassOverridden()) { + //// (overridden to + overridetext = trans.get("RocketCompCfg.lbl.overriddento") + " " + UnitGroup.UNITS_MASS.getDefaultUnit(). + toStringUnit(component.getOverrideMass()) + ")"; + } + + for (RocketComponent c = component.getParent(); c != null; c = c.getParent()) { + if (c.isMassOverridden() && c.getOverrideSubcomponents()) { + ///// (overridden by + overridetext = trans.get("RocketCompCfg.lbl.overriddenby") + " " + c.getName() + ")"; + } + } + + if (overridetext != null) + text = text + " " + overridetext; + + massLabel.setText(text); + } else { + massLabel.setText(""); + } + } + + + protected JPanel materialPanel(JPanel panel, Material.Type type) { + ////Component material: and Component finish: + return materialPanel(panel, type, trans.get("RocketCompCfg.lbl.Componentmaterial"), + trans.get("RocketCompCfg.lbl.Componentfinish")); + } + + protected JPanel materialPanel(JPanel panel, Material.Type type, + String materialString, String finishString) { + JLabel label = new JLabel(materialString); + //// The component material affects the weight of the component. + label.setToolTipText(trans.get("RocketCompCfg.lbl.ttip.componentmaterialaffects")); + panel.add(label, "spanx 4, wrap rel"); + + JComboBox combo = new JComboBox(new MaterialModel(panel, component, type)); + //// The component material affects the weight of the component. + combo.setToolTipText(trans.get("RocketCompCfg.combo.ttip.componentmaterialaffects")); + panel.add(combo, "spanx 4, growx, wrap paragraph"); + + + if (component instanceof ExternalComponent) { + label = new JLabel(finishString); + ////The component finish affects the aerodynamic drag of the component.
+ String tip = trans.get("RocketCompCfg.lbl.longA1") + //// The value indicated is the average roughness height of the surface. + + trans.get("RocketCompCfg.lbl.longA2"); + label.setToolTipText(tip); + panel.add(label, "spanx 4, wmin 220lp, wrap rel"); + + combo = new JComboBox(new EnumModel(component, "Finish")); + combo.setToolTipText(tip); + panel.add(combo, "spanx 4, growx, split"); + + //// Set for all + JButton button = new JButton(trans.get("RocketCompCfg.but.Setforall")); + //// Set this finish for all components of the rocket. + button.setToolTipText(trans.get("RocketCompCfg.but.ttip.Setforall")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Finish f = ((ExternalComponent) component).getFinish(); + try { + document.startUndo("Set rocket finish"); + + // Do changes + Iterator iter = component.getRoot().iterator(); + while (iter.hasNext()) { + RocketComponent c = iter.next(); + if (c instanceof ExternalComponent) { + ((ExternalComponent) c).setFinish(f); + } + } + } finally { + document.stopUndo(); + } + } + }); + panel.add(button, "wrap paragraph"); + } + + return panel; + } + + + private JPanel overrideTab() { + JPanel panel = new JPanel(new MigLayout("align 50% 20%, fillx, gap rel unrel", + "[][65lp::][30lp::][]", "")); + //// Override the mass or center of gravity of the + panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.Overridemassorcenter") + " " + + component.getComponentName() + ":", Style.BOLD), "spanx, wrap 20lp"); + + JCheckBox check; + BooleanModel bm; + UnitSelector us; + BasicSlider bs; + + //// Mass + bm = new BooleanModel(component, "MassOverridden"); + check = new JCheckBox(bm); + //// Override mass: + check.setText(trans.get("RocketCompCfg.checkbox.Overridemass")); + panel.add(check, "growx 1, gapright 20lp"); + + DoubleModel m = new DoubleModel(component, "OverrideMass", UnitGroup.UNITS_MASS, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + bm.addEnableComponent(spin, true); + panel.add(spin, "growx 1"); + + us = new UnitSelector(m); + bm.addEnableComponent(us, true); + panel.add(us, "growx 1"); + + bs = new BasicSlider(m.getSliderModel(0, 0.03, 1.0)); + bm.addEnableComponent(bs); + panel.add(bs, "growx 5, w 100lp, wrap"); + + + //// CG override + bm = new BooleanModel(component, "CGOverridden"); + check = new JCheckBox(bm); + //// Override center of gravity:" + check.setText(trans.get("RocketCompCfg.checkbox.Overridecenterofgrav")); + panel.add(check, "growx 1, gapright 20lp"); + + m = new DoubleModel(component, "OverrideCGX", UnitGroup.UNITS_LENGTH, 0); + // Calculate suitable length for slider + DoubleModel length; + if (component instanceof ComponentAssembly) { + double l = 0; + + Iterator iterator = component.iterator(false); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + if (c.getRelativePosition() == RocketComponent.Position.AFTER) + l += c.getLength(); + } + length = new DoubleModel(l); + } else { + length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + } + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + bm.addEnableComponent(spin, true); + panel.add(spin, "growx 1"); + + us = new UnitSelector(m); + bm.addEnableComponent(us, true); + panel.add(us, "growx 1"); + + bs = new BasicSlider(m.getSliderModel(new DoubleModel(0), length)); + bm.addEnableComponent(bs); + panel.add(bs, "growx 5, w 100lp, wrap 35lp"); + + + // Override subcomponents checkbox + bm = new BooleanModel(component, "OverrideSubcomponents"); + check = new JCheckBox(bm); + //// Override mass and CG of all subcomponents + check.setText(trans.get("RocketCompCfg.checkbox.OverridemassandCG")); + panel.add(check, "gap para, spanx, wrap para"); + + //// The overridden mass does not include motors.
+ panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.longB1") + + //// The center of gravity is measured from the front end of the + trans.get("RocketCompCfg.lbl.longB2") + " " + + component.getComponentName().toLowerCase() + ".", -1), + "spanx, wrap, gap para, height 0::30lp"); + + return panel; + } + + + private JPanel commentTab() { + JPanel panel = new JPanel(new MigLayout("fill")); + + //// Comments on the + panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.Commentsonthe") + " " + component.getComponentName() + ":", + Style.BOLD), "wrap"); + + // TODO: LOW: Changes in comment from other sources not reflected in component + commentTextArea = new JTextArea(component.getComment()); + commentTextArea.setLineWrap(true); + commentTextArea.setWrapStyleWord(true); + commentTextArea.setEditable(true); + GUIUtil.setTabToFocusing(commentTextArea); + commentTextArea.addFocusListener(textFieldListener); + + panel.add(new JScrollPane(commentTextArea), "width 10px, height 10px, growx, growy"); + + return panel; + } + + + + private JPanel figureTab() { + JPanel panel = new JPanel(new MigLayout("align 20% 20%")); + + //// Figure style: + panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.Figurestyle"), Style.BOLD), "wrap para"); + + //// Component color: + panel.add(new JLabel(trans.get("RocketCompCfg.lbl.Componentcolor")), "gapleft para, gapright 10lp"); + + colorButton = new JButton(new ColorIcon(getColor())); + colorButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + net.sf.openrocket.util.Color c = component.getColor(); + if (c == null) { + c = Application.getPreferences().getDefaultColor(component.getClass()); + } + + //// Choose color + Color awtColor = ColorConversion.toAwtColor(c); + awtColor = JColorChooser.showDialog(tabbedPane, trans.get("RocketCompCfg.lbl.Choosecolor"), awtColor); + c = ColorConversion.fromAwtColor(awtColor); + if (c != null) { + component.setColor(c); + } + } + }); + panel.add(colorButton, "gapright 10lp"); + + //// Use default color + colorDefault = new JCheckBox(trans.get("RocketCompCfg.checkbox.Usedefaultcolor")); + if (component.getColor() == null) + colorDefault.setSelected(true); + colorDefault.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (colorDefault.isSelected()) + component.setColor(null); + else + component.setColor(((SwingPreferences) Application.getPreferences()).getDefaultColor(component.getClass())); + } + }); + panel.add(colorDefault, "wrap para"); + + //// Component line style: + panel.add(new JLabel(trans.get("RocketCompCfg.lbl.Complinestyle")), "gapleft para, gapright 10lp"); + + LineStyle[] list = new LineStyle[LineStyle.values().length + 1]; + System.arraycopy(LineStyle.values(), 0, list, 1, LineStyle.values().length); + + JComboBox combo = new JComboBox(new EnumModel(component, "LineStyle", + //// Default style + list, trans.get("LineStyle.Defaultstyle"))); + panel.add(combo, "spanx 2, growx, wrap 50lp"); + + //// Save as default style + JButton button = new JButton(trans.get("RocketCompCfg.but.Saveasdefstyle")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (component.getColor() != null) { + ((SwingPreferences) Application.getPreferences()).setDefaultColor(component.getClass(), component.getColor()); + component.setColor(null); + } + if (component.getLineStyle() != null) { + Application.getPreferences().setDefaultLineStyle(component.getClass(), component.getLineStyle()); + component.setLineStyle(null); + } + } + }); + panel.add(button, "gapleft para, spanx 3, growx, wrap"); + + return panel; + } + + + private Color getColor() { + net.sf.openrocket.util.Color c = component.getColor(); + if (c == null) { + c = Application.getPreferences().getDefaultColor(component.getClass()); + } + return ColorConversion.toAwtColor(c); + } + + + + protected JPanel shoulderTab() { + JPanel panel = new JPanel(new MigLayout("fill")); + JPanel sub; + DoubleModel m, m2; + DoubleModel m0 = new DoubleModel(0); + BooleanModel bm; + JCheckBox check; + JSpinner spin; + + + //// Fore shoulder, not for NoseCone + + if (!(component instanceof NoseCone)) { + sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + //// Fore shoulder + sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.border.Foreshoulder"))); + + + //// Radius + //// Diameter: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); + + m = new DoubleModel(component, "ForeShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, "ForeRadius", 2, UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); + + + //// Length: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); + + m = new DoubleModel(component, "ForeShoulderLength", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); + + + //// Thickness: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); + + m = new DoubleModel(component, "ForeShoulderThickness", UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, "ForeShoulderRadius", UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); + + + //// Capped + bm = new BooleanModel(component, "ForeShoulderCapped"); + check = new JCheckBox(bm); + //// End capped + check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); + //// Whether the end of the shoulder is capped. + check.setToolTipText(trans.get("RocketCompCfg.ttip.Endcapped")); + sub.add(check, "spanx"); + + + panel.add(sub); + } + + + //// Aft shoulder + sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + if (component instanceof NoseCone) + //// Nose cone shoulder + sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Noseconeshoulder"))); + else + //// Aft shoulder + sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Aftshoulder"))); + + + //// Radius + //// Diameter: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); + + m = new DoubleModel(component, "AftShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, "AftRadius", 2, UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); + + + //// Length: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); + + m = new DoubleModel(component, "AftShoulderLength", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); + + + //// Thickness: + sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); + + m = new DoubleModel(component, "AftShoulderThickness", UnitGroup.UNITS_LENGTH, 0); + m2 = new DoubleModel(component, "AftShoulderRadius", UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + sub.add(spin, "growx"); + + sub.add(new UnitSelector(m), "growx"); + sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); + + + //// Capped + bm = new BooleanModel(component, "AftShoulderCapped"); + check = new JCheckBox(bm); + //// End capped + check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); + //// Whether the end of the shoulder is capped. + check.setToolTipText(trans.get("RocketCompCfg.ttip.Endcapped")); + sub.add(check, "spanx"); + + + panel.add(sub); + + + return panel; + } + + + + + /* + * Private inner class to handle events in componentNameField. + */ + private class TextFieldListener implements ActionListener, FocusListener { + @Override + public void actionPerformed(ActionEvent e) { + setName(); + } + + @Override + public void focusGained(FocusEvent e) { + } + + @Override + public void focusLost(FocusEvent e) { + setName(); + } + + private void setName() { + if (!component.getName().equals(componentNameField.getText())) { + component.setName(componentNameField.getText()); + } + if (!component.getComment().equals(commentTextArea.getText())) { + component.setComment(commentTextArea.getText()); + } + } + } + + + protected void register(Invalidatable model) { + this.invalidatables.add(model); + } + + public void invalidateModels() { + for (Invalidatable i : invalidatables) { + i.invalidate(); + } + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/configdialog/RocketConfig.java b/core/src/net/sf/openrocket/gui/configdialog/RocketConfig.java new file mode 100644 index 00000000..c692f498 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/RocketConfig.java @@ -0,0 +1,103 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusEvent; +import java.awt.event.FocusListener; + +import javax.swing.JLabel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + +public class RocketConfig extends RocketComponentConfig { + private static final Translator trans = Application.getTranslator(); + + private TextFieldListener textFieldListener; + + private JTextArea designerTextArea; + private JTextArea revisionTextArea; + + private final Rocket rocket; + + public RocketConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + rocket = (Rocket) c; + + this.removeAll(); + setLayout(new MigLayout("fill")); + + //// Design name: + this.add(new JLabel(trans.get("RocketCfg.lbl.Designname")), "top, pad 4lp, gapright 10lp"); + this.add(componentNameField, "growx, wrap para"); + + //// Designer: + this.add(new JLabel(trans.get("RocketCfg.lbl.Designer")), "top, pad 4lp, gapright 10lp"); + + textFieldListener = new TextFieldListener(); + designerTextArea = new JTextArea(rocket.getDesigner()); + designerTextArea.setLineWrap(true); + designerTextArea.setWrapStyleWord(true); + designerTextArea.setEditable(true); + GUIUtil.setTabToFocusing(designerTextArea); + designerTextArea.addFocusListener(textFieldListener); + this.add(new JScrollPane(designerTextArea), "wmin 400lp, height 60lp:60lp:, grow 30, wrap para"); + + //// Comments: + this.add(new JLabel(trans.get("RocketCfg.lbl.Comments")), "top, pad 4lp, gapright 10lp"); + this.add(new JScrollPane(commentTextArea), "wmin 400lp, height 155lp:155lp:, grow 100, wrap para"); + + //// Revision history: + this.add(new JLabel(trans.get("RocketCfg.lbl.Revisionhistory")), "top, pad 4lp, gapright 10lp"); + revisionTextArea = new JTextArea(rocket.getRevision()); + revisionTextArea.setLineWrap(true); + revisionTextArea.setWrapStyleWord(true); + revisionTextArea.setEditable(true); + GUIUtil.setTabToFocusing(revisionTextArea); + revisionTextArea.addFocusListener(textFieldListener); + + this.add(new JScrollPane(revisionTextArea), "wmin 400lp, height 60lp:60lp:, grow 30, wrap para"); + + + addButtons(); + } + + + + private class TextFieldListener implements ActionListener, FocusListener { + @Override + public void actionPerformed(ActionEvent e) { + setName(); + } + + @Override + public void focusGained(FocusEvent e) { + } + + @Override + public void focusLost(FocusEvent e) { + setName(); + } + + private void setName() { + if (!rocket.getDesigner().equals(designerTextArea.getText())) { + rocket.setDesigner(designerTextArea.getText()); + } + if (!rocket.getRevision().equals(revisionTextArea.getText())) { + rocket.setRevision(revisionTextArea.getText()); + } + } + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java b/core/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java new file mode 100644 index 00000000..fbc99f85 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java @@ -0,0 +1,130 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class ShockCordConfig extends RocketComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public ShockCordConfig(OpenRocketDocument d, RocketComponent component) { + super(d, component); + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + JLabel label; + DoubleModel m; + JSpinner spin; + String tip; + + + ////// Left side + + // Cord length + //// Shock cord length + label = new JLabel(trans.get("ShockCordCfg.lbl.Shockcordlength")); + panel.add(label); + + m = new DoubleModel(component, "CordLength", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 1, 10)), "w 100lp, wrap"); + + + // Material + //// Shock cord material: + materialPanel(panel, Material.Type.LINE, trans.get("ShockCordCfg.lbl.Shockcordmaterial"), null); + + + + ///// Right side + JPanel panel2 = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany"); + + + //// Position + //// Position relative to: + panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.Posrelativeto"))); + + JComboBox combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel2.add(combo, "spanx, growx, wrap"); + + //// plus + panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel2.add(spin, "growx"); + + panel2.add(new UnitSelector(m), "growx"); + panel2.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + + //// Spatial length + //// Packed length: + panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.Packedlength"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel2.add(spin, "growx"); + + panel2.add(new UnitSelector(m), "growx"); + panel2.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); + + + //// Tube diameter + //// Packed diameter: + panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.Packeddiam"))); + + DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel2.add(spin, "growx"); + + panel2.add(new UnitSelector(od), "growx"); + panel2.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); + + + + //// General and General properties + tabbedPane.insertTab(trans.get("ShockCordCfg.tab.General"), null, panel, trans.get("ShockCordCfg.tab.ttip.General"), 0); + // tabbedPane.insertTab("Radial position", null, positionTab(), + // "Radial position configuration", 1); + tabbedPane.setSelectedIndex(0); + } + + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java b/core/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java new file mode 100644 index 00000000..32192765 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java @@ -0,0 +1,32 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JPanel; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + + + +public class SleeveConfig extends RingComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public SleeveConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel tab; + //// Outer diameter: + //// Inner diameter: + //// Wall thickness: + //// Length: + tab = generalTab(trans.get("SleeveCfg.tab.Outerdiam"), trans.get("SleeveCfg.tab.Innerdiam"), + trans.get("SleeveCfg.tab.Wallthickness"), trans.get("SleeveCfg.tab.Length")); + //// General and General properties + tabbedPane.insertTab(trans.get("SleeveCfg.tab.General"), null, tab, + trans.get("SleeveCfg.tab.Generalproperties"), 0); + tabbedPane.setSelectedIndex(0); + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java b/core/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java new file mode 100644 index 00000000..8d7be304 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java @@ -0,0 +1,291 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.MaterialModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.HtmlLabel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class StreamerConfig extends RecoveryDeviceConfig { + private static final Translator trans = Application.getTranslator(); + + public StreamerConfig(OpenRocketDocument d, final RocketComponent component) { + super(d, component); + + JPanel primary = new JPanel(new MigLayout()); + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + + //// Strip length: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Striplength"))); + + DoubleModel m = new DoubleModel(component, "StripLength", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.6, 1.5)), "w 100lp, wrap"); + + //// Strip width: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Stripwidth"))); + + m = new DoubleModel(component, "StripWidth", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.2)), "w 100lp, wrap 20lp"); + + + + //// Strip area: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Striparea"))); + + m = new DoubleModel(component, "Area", UnitGroup.UNITS_AREA, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.25)), "w 100lp, wrap"); + + //// Aspect ratio: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Aspectratio"))); + + m = new DoubleModel(component, "AspectRatio", UnitGroup.UNITS_NONE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + // panel.add(new UnitSelector(m),"growx"); + panel.add(new BasicSlider(m.getSliderModel(2, 15)), "skip, w 100lp, wrap 20lp"); + + + //// Material: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Material"))); + + JComboBox combo = new JComboBox(new MaterialModel(panel, component, + Material.Type.SURFACE)); + //// The component material affects the weight of the component. + combo.setToolTipText(trans.get("StreamerCfg.combo.ttip.MaterialModel")); + panel.add(combo, "spanx 3, growx, wrap 20lp"); + + + + // CD + //// Drag coefficient CD: + JLabel label = new HtmlLabel(trans.get("StreamerCfg.lbl.longA1")); + //// The drag coefficient relative to the total area of the streamer.
+ String tip = trans.get("StreamerCfg.lbl.longB1") + + //// "A larger drag coefficient yields a slowed descent rate. + trans.get("StreamerCfg.lbl.longB2"); + label.setToolTipText(tip); + panel.add(label); + + m = new DoubleModel(component, "CD", UnitGroup.UNITS_COEFFICIENT, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setToolTipText(tip); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + JCheckBox check = new JCheckBox(m.getAutomaticAction()); + //// Automatic + check.setText(trans.get("StreamerCfg.lbl.Automatic")); + panel.add(check, "skip, span, wrap"); + + //// The drag coefficient is relative to the area of the streamer. + panel.add(new StyledLabel(trans.get("StreamerCfg.lbl.longC1"), + -2), "span, wrap"); + + + + primary.add(panel, "grow, gapright 20lp"); + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + + + + + //// Position + //// Position relative to: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Posrelativeto"))); + + combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + + //// plus + panel.add(new JLabel(trans.get("StreamerCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap"); + + + //// Spatial length: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Packedlength"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); + + + //// Tube diameter + //// Packed diameter: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Packeddiam"))); + + DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 30lp"); + + + //// Deployment + //// Deploys at: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Deploysat")), ""); + + combo = new JComboBox(new EnumModel(component, "DeployEvent")); + panel.add(combo, "spanx 3, growx, wrap"); + + // ... and delay + //// plus + panel.add(new JLabel(trans.get("StreamerCfg.lbl.plusdelay")), "right"); + + m = new DoubleModel(component, "DeployDelay", 0); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "spanx, split"); + + //// seconds + panel.add(new JLabel(trans.get("StreamerCfg.lbl.seconds")), "wrap paragraph"); + + // Altitude: + label = new JLabel(trans.get("StreamerCfg.lbl.Altitude")); + altitudeComponents.add(label); + panel.add(label); + + m = new DoubleModel(component, "DeployAltitude", UnitGroup.UNITS_DISTANCE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + altitudeComponents.add(spin); + panel.add(spin, "growx"); + UnitSelector unit = new UnitSelector(m); + altitudeComponents.add(unit); + panel.add(unit, "growx"); + BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000)); + altitudeComponents.add(slider); + panel.add(slider, "w 100lp, wrap"); + + + primary.add(panel, "grow"); + + updateFields(); + + //// General and General properties + tabbedPane.insertTab(trans.get("StreamerCfg.tab.General"), null, primary, + trans.get("StreamerCfg.tab.ttip.General"), 0); + //// Radial position and Radial position configuration + tabbedPane.insertTab(trans.get("StreamerCfg.tab.Radialpos"), null, positionTab(), + trans.get("StreamerCfg.tab.ttip.Radialpos"), 1); + tabbedPane.setSelectedIndex(0); + } + + + + + + protected JPanel positionTab() { + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + //// Radial position + //// Radial distance: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Radialdistance"))); + + DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); + + JSpinner spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); + + + //// Radial direction + //// Radial direction: + panel.add(new JLabel(trans.get("StreamerCfg.lbl.Radialdirection"))); + + m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + //// Reset button + JButton button = new JButton(trans.get("StreamerCfg.but.Reset")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ((MassComponent) component).setRadialDirection(0.0); + ((MassComponent) component).setRadialPosition(0.0); + } + }); + panel.add(button, "spanx, right"); + + return panel; + } +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java b/core/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java new file mode 100644 index 00000000..3013b7c9 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JPanel; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + + + +public class ThicknessRingComponentConfig extends RingComponentConfig { + private static final Translator trans = Application.getTranslator(); + + public ThicknessRingComponentConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + JPanel tab; + + //// Outer diameter: + //// Inner diameter: + //// Wall thickness: + //// Length: + tab = generalTab(trans.get("ThicknessRingCompCfg.tab.Outerdiam"), + trans.get("ThicknessRingCompCfg.tab.Innerdiam"), + trans.get("ThicknessRingCompCfg.tab.Wallthickness"), trans.get("ThicknessRingCompCfg.tab.Length")); + //// General and General properties + tabbedPane.insertTab(trans.get("ThicknessRingCompCfg.tab.General"), null, tab, + trans.get("ThicknessRingCompCfg.tab.Generalprop"), 0); + tabbedPane.setSelectedIndex(0); + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java b/core/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java new file mode 100644 index 00000000..5e0286d8 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java @@ -0,0 +1,209 @@ +package net.sf.openrocket.gui.configdialog; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +public class TransitionConfig extends RocketComponentConfig { + + private static final Translator trans = Application.getTranslator(); + private JComboBox typeBox; + //private JLabel description; + + private JLabel shapeLabel; + private JSpinner shapeSpinner; + private BasicSlider shapeSlider; + private DescriptionArea description; + + + // Prepended to the description from Transition.DESCRIPTIONS + private static final String PREDESC = ""; + + + public TransitionConfig(OpenRocketDocument d, RocketComponent c) { + super(d, c); + + DoubleModel m; + JSpinner spin; + JCheckBox checkbox; + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + + + //// Shape selection + //// Transition shape: + panel.add(new JLabel(trans.get("TransitionCfg.lbl.Transitionshape"))); + + Transition.Shape selected = ((Transition) component).getType(); + Transition.Shape[] typeList = Transition.Shape.values(); + + typeBox = new JComboBox(typeList); + typeBox.setEditable(false); + typeBox.setSelectedItem(selected); + typeBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Transition.Shape s = (Transition.Shape) typeBox.getSelectedItem(); + ((Transition) component).setType(s); + description.setText(PREDESC + s.getTransitionDescription()); + updateEnabled(); + } + }); + panel.add(typeBox, "span, split 2"); + + //// Clipped + checkbox = new JCheckBox(new BooleanModel(component, "Clipped")); + //// Clipped + checkbox.setText(trans.get("TransitionCfg.checkbox.Clipped")); + panel.add(checkbox, "wrap"); + + + //// Shape parameter: + shapeLabel = new JLabel(trans.get("TransitionCfg.lbl.Shapeparam")); + panel.add(shapeLabel); + + m = new DoubleModel(component, "ShapeParameter"); + + shapeSpinner = new JSpinner(m.getSpinnerModel()); + shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner)); + panel.add(shapeSpinner, "growx"); + + DoubleModel min = new DoubleModel(component, "ShapeParameterMin"); + DoubleModel max = new DoubleModel(component, "ShapeParameterMax"); + shapeSlider = new BasicSlider(m.getSliderModel(min, max)); + panel.add(shapeSlider, "skip, w 100lp, wrap"); + + updateEnabled(); + + + //// Length + //// Transition length: + panel.add(new JLabel(trans.get("TransitionCfg.lbl.Transitionlength"))); + + m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.3)), "w 100lp, wrap"); + + + //// Transition diameter 1 + //// Fore diameter: + panel.add(new JLabel(trans.get("TransitionCfg.lbl.Forediam"))); + + DoubleModel od = new DoubleModel(component, "ForeRadius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); + + checkbox = new JCheckBox(od.getAutomaticAction()); + //// Automatic + checkbox.setText(trans.get("TransitionCfg.checkbox.Automatic")); + panel.add(checkbox, "skip, span 2, wrap"); + + + //// Transition diameter 2 + //// Aft diameter: + panel.add(new JLabel(trans.get("TransitionCfg.lbl.Aftdiam"))); + + od = new DoubleModel(component, "AftRadius", 2, UnitGroup.UNITS_LENGTH, 0); + // Diameter = 2*Radius + + spin = new JSpinner(od.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(od), "growx"); + panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); + + checkbox = new JCheckBox(od.getAutomaticAction()); + //// Automatic + checkbox.setText(trans.get("TransitionCfg.checkbox.Automatic")); + panel.add(checkbox, "skip, span 2, wrap"); + + + //// Wall thickness: + panel.add(new JLabel(trans.get("TransitionCfg.lbl.Wallthickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 0px"); + + //// Filled + checkbox = new JCheckBox(new BooleanModel(component, "Filled")); + //// Filled + checkbox.setText(trans.get("TransitionCfg.checkbox.Filled")); + panel.add(checkbox, "skip, span 2, wrap"); + + + + //// Description + + JPanel panel2 = new JPanel(new MigLayout("ins 0")); + + description = new DescriptionArea(5); + description.setText(PREDESC + ((Transition) component).getType(). + getTransitionDescription()); + panel2.add(description, "wmin 250lp, spanx, growx, wrap para"); + + + //// Material + + + materialPanel(panel2, Material.Type.BULK); + panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany"); + + //// General and General properties + tabbedPane.insertTab(trans.get("TransitionCfg.tab.General"), null, panel, + trans.get("TransitionCfg.tab.Generalproperties"), 0); + //// Shoulder and Shoulder properties + tabbedPane.insertTab(trans.get("TransitionCfg.tab.Shoulder"), null, shoulderTab(), + trans.get("TransitionCfg.tab.Shoulderproperties"), 1); + tabbedPane.setSelectedIndex(0); + } + + + + private void updateEnabled() { + boolean e = ((Transition) component).getType().usesParameter(); + shapeLabel.setEnabled(e); + shapeSpinner.setEnabled(e); + shapeSlider.setEnabled(e); + } + +} diff --git a/core/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java b/core/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java new file mode 100644 index 00000000..47650ffd --- /dev/null +++ b/core/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java @@ -0,0 +1,244 @@ +package net.sf.openrocket.gui.configdialog; + + +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSeparator; +import javax.swing.JSpinner; +import javax.swing.SwingConstants; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.IntegerModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + + +public class TrapezoidFinSetConfig extends FinSetConfig { + private static final Translator trans = Application.getTranslator(); + + public TrapezoidFinSetConfig(OpenRocketDocument d, final RocketComponent component) { + super(d, component); + + DoubleModel m; + JSpinner spin; + JComboBox combo; + + JPanel mainPanel = new JPanel(new MigLayout()); + + + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + //// Number of fins: + JLabel label = new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Nbroffins")); + //// The number of fins in the fin set. + label.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Nbroffins")); + panel.add(label); + + IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); + + spin = new JSpinner(im.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + //// The number of fins in the fin set. + spin.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Nbroffins")); + panel.add(spin, "growx, wrap"); + + + //// Base rotation + //// Fin rotation: + label = new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Finrotation")); + //// The angle of the first fin in the fin set. + label.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Finrotation")); + panel.add(label); + + m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); + + + //// Fin cant: + label = new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Fincant")); + //// The angle that the fins are canted with respect to the rocket + label.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Fincant")); + panel.add(label); + + m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE, + -FinSet.MAX_CANT, FinSet.MAX_CANT); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT, FinSet.MAX_CANT)), + "w 100lp, wrap"); + + + //// Root chord: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Rootchord"))); + + m = new DoubleModel(component, "RootChord", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); + + + + //// Tip chord: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Tipchord"))); + + m = new DoubleModel(component, "TipChord", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); + + + //// Height: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Height"))); + + m = new DoubleModel(component, "Height", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); + + + + //// Sweep length: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Sweeplength"))); + + m = new DoubleModel(component, "Sweep", UnitGroup.UNITS_LENGTH); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + + // sweep slider from -1.1*TipChord to 1.1*RootChord + DoubleModel tc = new DoubleModel(component, "TipChord", -1.1, UnitGroup.UNITS_LENGTH); + DoubleModel rc = new DoubleModel(component, "RootChord", 1.1, UnitGroup.UNITS_LENGTH); + panel.add(new BasicSlider(m.getSliderModel(tc, rc)), "w 100lp, wrap"); + + + //// Sweep angle: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Sweepangle"))); + + m = new DoubleModel(component, "SweepAngle", UnitGroup.UNITS_ANGLE, + -TrapezoidFinSet.MAX_SWEEP_ANGLE, TrapezoidFinSet.MAX_SWEEP_ANGLE); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(-Math.PI / 4, Math.PI / 4)), + "w 100lp, wrap paragraph"); + + + + + + mainPanel.add(panel, "aligny 20%"); + + mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy"); + + + + panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); + + + + //// Fin cross section: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.FincrossSection"))); + combo = new JComboBox( + new EnumModel(component, "CrossSection")); + panel.add(combo, "span, growx, wrap"); + + + //// Thickness: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Thickness"))); + + m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap para"); + + + //// Position + //// Position relative to: + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Posrelativeto"))); + + combo = new JComboBox( + new EnumModel(component, "RelativePosition", + new RocketComponent.Position[] { + RocketComponent.Position.TOP, + RocketComponent.Position.MIDDLE, + RocketComponent.Position.BOTTOM, + RocketComponent.Position.ABSOLUTE + })); + panel.add(combo, "spanx, growx, wrap"); + //// plus + panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.plus")), "right"); + + m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + panel.add(spin, "growx"); + + panel.add(new UnitSelector(m), "growx"); + panel.add(new BasicSlider(m.getSliderModel( + new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), + new DoubleModel(component.getParent(), "Length"))), + "w 100lp, wrap para"); + + + + //// Material + materialPanel(panel, Material.Type.BULK); + + + + + mainPanel.add(panel, "aligny 20%"); + + //// General and General properties + tabbedPane.insertTab(trans.get("TrapezoidFinSetCfg.tab.General"), null, mainPanel, + trans.get("TrapezoidFinSetCfg.tab.Generalproperties"), 0); + tabbedPane.setSelectedIndex(0); + + addFinSetButtons(); + + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java b/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java new file mode 100644 index 00000000..7aaadd24 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/AboutDialog.java @@ -0,0 +1,125 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.components.URLLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.Chars; + +public class AboutDialog extends JDialog { + + public static final String OPENROCKET_URL = "http://openrocket.sourceforge.net/"; + private static final Translator trans = Application.getTranslator(); + + private static final String CREDITS = "

" + + "OpenRocket has been developed by:

" + + "Sampo Niskanen (main developer)
" + + "Doug Pedrick (RockSim file format, printing)
" + + "Boris du Reau (internationalization, translation lead)
" + + "Richard Graham (geodetic computations)

" + + "Translations by:

" + + "Tripoli France (French)
" + + "Stefan Lobas / ERIG e.V. (German)
" + + "Tripoli Spain (Spanish)

" + + "OpenRocket utilizes the following libraries:

" + + "MiG Layout (http://www.miglayout.com/)
" + + "JFreeChart (http://www.jfree.org/jfreechart/)
" + + "iText (http://www.itextpdf.com/)"; + + + public AboutDialog(JFrame parent) { + super(parent, true); + + final String version = BuildProperties.getVersion(); + + JPanel panel = new JPanel(new MigLayout("fill")); + JPanel sub; + + + // OpenRocket logo + panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket")), "top"); + + + // OpenRocket version info + copyright + sub = new JPanel(new MigLayout("fill")); + + sub.add(new StyledLabel("OpenRocket", 20), "ax 50%, growy, wrap para"); + sub.add(new StyledLabel(trans.get("lbl.version").trim() + " " + version, 3), "ax 50%, growy, wrap rel"); + sub.add(new StyledLabel("Copyright " + Chars.COPY + " 2007-2011 Sampo Niskanen"), "ax 50%, growy, wrap para"); + + sub.add(new URLLabel(OPENROCKET_URL), "ax 50%, growy, wrap para"); + panel.add(sub, "grow"); + + + // Translation information (if present) + String translation = trans.get("lbl.translation").trim(); + String translator = trans.get("lbl.translator").trim(); + String translatorWebsite = trans.get("lbl.translatorWebsite").trim(); + String translatorIcon = trans.get("lbl.translatorIcon").trim(); + + if (translator.length() > 0 || translatorWebsite.length() > 0 || translatorIcon.length() > 0) { + sub = new JPanel(new MigLayout("fill")); + + sub.add(new StyledLabel(translation, Style.BOLD), "ax 50%, growy, wrap para"); + + if (translatorIcon.length() > 0) { + sub.add(new JLabel(Icons.loadImageIcon("pix/translators/" + translatorIcon, translator)), + "ax 50%, growy, wrap para"); + } + if (translator.length() > 0) { + sub.add(new JLabel(translator), "ax 50%, growy, wrap rel"); + } + if (translatorWebsite.length() > 0) { + sub.add(new URLLabel(translatorWebsite), "ax 50%, growy, wrap para"); + } + + panel.add(sub); + } + + + DescriptionArea info = new DescriptionArea(5); + info.setText(CREDITS); + panel.add(info, "newline, width 10px, height 100lp, grow, spanx, wrap para"); + + // JTextArea area = new JTextArea(CREATORS); + // area.setEditable(false); + // area.setLineWrap(true); + // area.setWrapStyleWord(true); + // panel.add(new JScrollPane(area), "width 10px, height 100lp, grow, spanx, wrap para"); + + + //Close button + JButton close = new JButton(trans.get("button.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + AboutDialog.this.dispose(); + } + }); + panel.add(close, "spanx, right"); + + this.add(panel); + this.setTitle("OpenRocket " + version); + this.pack(); + this.setResizable(false); + this.setLocationRelativeTo(parent); + + GUIUtil.setDisposableDialogOptions(this, close); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java b/core/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java new file mode 100644 index 00000000..9f203f36 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java @@ -0,0 +1,352 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Desktop; +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.util.List; +import java.util.Locale; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.communication.BugReporter; +import net.sf.openrocket.gui.components.SelectableLabel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogLevelBufferLogger; +import net.sf.openrocket.logging.LogLine; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.JarUtil; + +public class BugReportDialog extends JDialog { + + private static final String REPORT_EMAIL = "openrocket-bugs@lists.sourceforge.net"; + private static final Translator trans = Application.getTranslator(); + + + public BugReportDialog(Window parent, String labelText, final String message, final boolean sendIfUnchanged) { + //// Bug report + super(parent, trans.get("bugreport.dlg.title"), Dialog.ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill")); + + // Some fscking Swing bug that makes html labels initially way too high + JLabel label = new JLabel(labelText); + Dimension d = label.getPreferredSize(); + d.width = 100000; + label.setMaximumSize(d); + panel.add(label, "gapleft para, wrap para"); + + //// If connected to the Internet, you can simply click + //// Send bug report. + label = new JLabel(trans.get("bugreport.dlg.connectedInternet")); + d = label.getPreferredSize(); + d.width = 100000; + label.setMaximumSize(d); + panel.add(label, "gapleft para, wrap"); + + //// Otherwise, send the text below to the address: + panel.add(new JLabel(trans.get("bugreport.dlg.otherwise") + " "), + "gapleft para, split 2, gapright rel"); + panel.add(new SelectableLabel(REPORT_EMAIL), "growx, wrap para"); + + + final JTextArea textArea = new JTextArea(message, 20, 70); + textArea.setEditable(true); + panel.add(new JScrollPane(textArea), "grow, wrap"); + + + panel.add(new StyledLabel(trans.get("bugreport.lbl.Theinformation"), -1), "wrap para"); + + + + ////Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + BugReportDialog.this.dispose(); + } + }); + panel.add(close, "right, sizegroup buttons, split"); + + + //// Mail button + // if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Action.MAIL)) { + // JButton mail = new JButton("Open email"); + // mail.setToolTipText("Open email client with the suitable email ready."); + // mail.addActionListener(new ActionListener() { + // @Override + // public void actionPerformed(ActionEvent e) { + // String text = textArea.getText(); + // openEmail(text); + // } + // }); + // panel.add(mail, "right, sizegroup buttons"); + // } + + + //// Send bug report button + JButton send = new JButton(trans.get("bugreport.dlg.but.Sendbugreport")); + //// Automatically send the bug report to the OpenRocket developers. + send.setToolTipText(trans.get("bugreport.dlg.but.Sendbugreport.Ttip")); + send.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String text = textArea.getText(); + if (text.equals(message) && !sendIfUnchanged) { + JOptionPane.showMessageDialog(BugReportDialog.this, + trans.get("bugreport.dlg.provideDescription"), + trans.get("bugreport.dlg.provideDescription.title"), JOptionPane.ERROR_MESSAGE); + return; + } + + try { + + BugReporter.sendBugReport(text); + + // Success if we came here + //bugreport.dlg.successmsg + /*JOptionPane.showMessageDialog(BugReportDialog.this, + new Object[] { "Bug report successfully sent.", + "Thank you for helping make OpenRocket better!" }, + "Bug report sent", JOptionPane.INFORMATION_MESSAGE);*/ + JOptionPane.showMessageDialog(BugReportDialog.this, + new Object[] { trans.get("bugreport.dlg.successmsg1"), + trans.get("bugreport.dlg.successmsg2") }, + trans.get("bugreport.dlg.successmsg3"), JOptionPane.INFORMATION_MESSAGE); + + } catch (Exception ex) { + // Sending the message failed. + JOptionPane.showMessageDialog(BugReportDialog.this, + //// OpenRocket was unable to send the bug report: + new Object[] { trans.get("bugreport.dlg.failedmsg1"), + ex.getClass().getSimpleName() + ": " + ex.getMessage(), " ", + //// Please send the report manually to + trans.get("bugreport.dlg.failedmsg2") + " " + REPORT_EMAIL }, + //// Error sending report + trans.get("bugreport.dlg.failedmsg3"), JOptionPane.ERROR_MESSAGE); + } + } + }); + panel.add(send, "right, sizegroup buttons"); + + this.add(panel); + + this.validate(); + this.pack(); + this.pack(); + this.setLocationRelativeTo(parent); + + GUIUtil.setDisposableDialogOptions(this, send); + } + + + + /** + * Show a general bug report dialog allowing the user to input information about + * the bug they encountered. + * + * @param parent the parent window (may be null). + */ + public static void showBugReportDialog(Window parent) { + + StringBuilder sb = new StringBuilder(); + + sb.append("---------- Bug report ----------\n"); + sb.append('\n'); + sb.append("Include detailed steps on how to trigger the bug:\n"); + sb.append('\n'); + sb.append("1. \n"); + sb.append("2. \n"); + sb.append("3. \n"); + sb.append('\n'); + + sb.append("What does the software do and what in your opinion should it do in the " + + "case described above:\n"); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + + sb.append("Include your email address (optional; it helps if we can " + + "contact you in case we need additional information):\n"); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + + + sb.append("(Do not modify anything below this line.)\n"); + sb.append("---------- System information ----------\n"); + addSystemInformation(sb); + sb.append("---------- Error log ----------\n"); + addErrorLog(sb); + sb.append("---------- End of bug report ----------\n"); + sb.append('\n'); + + BugReportDialog reportDialog = new BugReportDialog(parent, + trans.get("bugreport.reportDialog.txt"), sb.toString(), false); + reportDialog.setVisible(true); + } + + + /** + * Show a dialog presented when an uncaught exception occurs. + * + * @param parent the parent window (may be null). + * @param t the thread that encountered the exception (may be null). + * @param e the exception. + */ + public static void showExceptionDialog(Window parent, Thread t, Throwable e) { + StringBuilder sb = new StringBuilder(); + + sb.append("---------- Bug report ----------\n"); + sb.append('\n'); + sb.append("Please include a description about what actions you were " + + "performing when the exception occurred:\n"); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + + + sb.append("Include your email address (optional; it helps if we can " + + "contact you in case we need additional information):\n"); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + sb.append('\n'); + + sb.append("(Do not modify anything below this line.)\n"); + sb.append("---------- Exception stack trace ----------\n"); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + sb.append(sw.getBuffer()); + sb.append('\n'); + + + sb.append("---------- Thread information ----------\n"); + if (t == null) { + sb.append("Thread is not specified."); + } else { + sb.append(t + "\n"); + } + sb.append('\n'); + + + sb.append("---------- System information ----------\n"); + addSystemInformation(sb); + sb.append("---------- Error log ----------\n"); + addErrorLog(sb); + sb.append("---------- End of bug report ----------\n"); + sb.append('\n'); + + BugReportDialog reportDialog = + //// Please include a short description about what you were doing when the exception occurred. + new BugReportDialog(parent, trans.get("bugreport.reportDialog.txt2"), sb.toString(), true); + reportDialog.setVisible(true); + } + + + private static void addSystemInformation(StringBuilder sb) { + sb.append("OpenRocket version: " + BuildProperties.getVersion() + "\n"); + sb.append("OpenRocket source: " + BuildProperties.getBuildSource() + "\n"); + sb.append("OpenRocket location: " + JarUtil.getCurrentJarFile() + "\n"); + sb.append("Current default locale: " + Locale.getDefault() + "\n"); + sb.append("System properties:\n"); + + // Sort the keys + SortedSet keys = new TreeSet(); + for (Object key : System.getProperties().keySet()) { + keys.add((String) key); + } + + for (String key : keys) { + String value = System.getProperty(key); + sb.append(" " + key + "="); + if (key.equals("line.separator")) { + for (char c : value.toCharArray()) { + sb.append(String.format("\\u%04x", (int) c)); + } + } else { + sb.append(value); + } + sb.append('\n'); + } + } + + + private static void addErrorLog(StringBuilder sb) { + LogLevelBufferLogger buffer = Application.getLogBuffer(); + List logs = buffer.getLogs(); + for (LogLine l : logs) { + sb.append(l.toString()).append('\n'); + } + } + + + + /** + * Open the default email client with the suitable bug report. + * Note that this does not work on some systems even if Desktop.isSupported() + * claims so. + * + * @param text the bug report text. + * @return whether opening the client succeeded. + */ + private boolean openEmail(String text) { + String version; + + try { + text = URLEncoder.encode(text, "UTF-8"); + version = URLEncoder.encode(BuildProperties.getVersion(), "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new BugException(e); + } + + + + String mailto = "mailto:" + REPORT_EMAIL + + "?subject=Bug%20report%20for%20OpenRocket%20" + version + + "?body=" + text; + URI uri; + try { + uri = new URI(mailto); + } catch (URISyntaxException e) { + e.printStackTrace(); + return false; + } + + Desktop desktop = Desktop.getDesktop(); + try { + desktop.mail(uri); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + + return true; + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java b/core/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java new file mode 100644 index 00000000..635e6468 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java @@ -0,0 +1,653 @@ +package net.sf.openrocket.gui.dialogs; + +import static net.sf.openrocket.unit.Unit.NOUNIT2; +import static net.sf.openrocket.util.Chars.ALPHA; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTabbedPane; +import javax.swing.JTable; +import javax.swing.JToggleButton; +import javax.swing.ListSelectionModel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.table.TableCellRenderer; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.gui.adaptors.Column; +import net.sf.openrocket.gui.adaptors.ColumnTableModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.StageSelector; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.scalefigure.RocketPanel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +public class ComponentAnalysisDialog extends JDialog implements ChangeListener { + + private static ComponentAnalysisDialog singletonDialog = null; + private static final Translator trans = Application.getTranslator(); + + + private final FlightConditions conditions; + private final Configuration configuration; + private final DoubleModel theta, aoa, mach, roll; + private final JToggleButton worstToggle; + private boolean fakeChange = false; + private AerodynamicCalculator aerodynamicCalculator; + private final MassCalculator massCalculator = new BasicMassCalculator(); + + private final ColumnTableModel cpTableModel; + private final ColumnTableModel dragTableModel; + private final ColumnTableModel rollTableModel; + + private final JList warningList; + + + private final List cpData = new ArrayList(); + private final List cgData = new ArrayList(); + private final List dragData = new ArrayList(); + private double totalCD = 0; + private final List rollData = new ArrayList(); + + + public ComponentAnalysisDialog(final RocketPanel rocketPanel) { + ////Component analysis + super(SwingUtilities.getWindowAncestor(rocketPanel), + trans.get("componentanalysisdlg.componentanalysis")); + + JTable table; + + JPanel panel = new JPanel(new MigLayout("fill", "[][35lp::][fill][fill]")); + add(panel); + + this.configuration = rocketPanel.getConfiguration(); + this.aerodynamicCalculator = rocketPanel.getAerodynamicCalculator().newInstance(); + + + conditions = new FlightConditions(configuration); + + rocketPanel.setCPAOA(0); + aoa = new DoubleModel(rocketPanel, "CPAOA", UnitGroup.UNITS_ANGLE, 0, Math.PI); + rocketPanel.setCPMach(Application.getPreferences().getDefaultMach()); + mach = new DoubleModel(rocketPanel, "CPMach", UnitGroup.UNITS_COEFFICIENT, 0); + rocketPanel.setCPTheta(rocketPanel.getFigure().getRotation()); + theta = new DoubleModel(rocketPanel, "CPTheta", UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI); + rocketPanel.setCPRoll(0); + roll = new DoubleModel(rocketPanel, "CPRoll", UnitGroup.UNITS_ROLL); + + //// Wind direction: + panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.winddir")), "width 100lp!"); + panel.add(new UnitSelector(theta, true), "width 50lp!"); + BasicSlider slider = new BasicSlider(theta.getSliderModel(0, 2 * Math.PI)); + panel.add(slider, "growx, split 2"); + //// Worst button + worstToggle = new JToggleButton(trans.get("componentanalysisdlg.ToggleBut.worst")); + worstToggle.setSelected(true); + worstToggle.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + stateChanged(null); + } + }); + slider.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (!fakeChange) + worstToggle.setSelected(false); + } + }); + panel.add(worstToggle, ""); + + + warningList = new JList(); + JScrollPane scrollPane = new JScrollPane(warningList); + ////Warnings: + scrollPane.setBorder(BorderFactory.createTitledBorder(trans.get("componentanalysisdlg.TitledBorder.warnings"))); + panel.add(scrollPane, "gap paragraph, spany 4, width 300lp!, growy 1, height :100lp:, wrap"); + + ////Angle of attack: + panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.angleofattack")), "width 100lp!"); + panel.add(new UnitSelector(aoa, true), "width 50lp!"); + panel.add(new BasicSlider(aoa.getSliderModel(0, Math.PI)), "growx, wrap"); + + //// Mach number: + panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.machnumber")), "width 100lp!"); + panel.add(new UnitSelector(mach, true), "width 50lp!"); + panel.add(new BasicSlider(mach.getSliderModel(0, 3)), "growx, wrap"); + + //// Roll rate: + panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.rollrate")), "width 100lp!"); + panel.add(new UnitSelector(roll, true), "width 50lp!"); + panel.add(new BasicSlider(roll.getSliderModel(-20 * 2 * Math.PI, 20 * 2 * Math.PI)), + "growx, wrap paragraph"); + + + // Stage and motor selection: + //// Active stages: + panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.activestages")), "spanx, split, gapafter rel"); + panel.add(new StageSelector(configuration), "gapafter paragraph"); + + //// Motor configuration: + JLabel label = new JLabel(trans.get("componentanalysisdlg.lbl.motorconf")); + label.setHorizontalAlignment(JLabel.RIGHT); + panel.add(label, "growx, right"); + panel.add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap"); + + + + // Tabbed pane + + JTabbedPane tabbedPane = new JTabbedPane(); + panel.add(tabbedPane, "spanx, growx, growy"); + + + // Create the CP data table + cpTableModel = new ColumnTableModel( + + //// Component + new Column(trans.get("componentanalysisdlg.TabStability.Col.Component")) { + @Override + public Object getValueAt(int row) { + RocketComponent c = cpData.get(row).getComponent(); + if (c instanceof Rocket) { + return "Total"; + } + return c.toString(); + } + + @Override + public int getDefaultWidth() { + return 200; + } + }, + new Column("CG / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) { + private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); + + @Override + public Object getValueAt(int row) { + return unit.toString(cgData.get(row).x); + } + }, + new Column("Mass / " + UnitGroup.UNITS_MASS.getDefaultUnit().getUnit()) { + private Unit unit = UnitGroup.UNITS_MASS.getDefaultUnit(); + + @Override + public Object getValueAt(int row) { + return unit.toString(cgData.get(row).weight); + } + }, + new Column("CP / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) { + private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); + + @Override + public Object getValueAt(int row) { + return unit.toString(cpData.get(row).getCP().x); + } + }, + new Column("CN" + ALPHA + "") { + @Override + public Object getValueAt(int row) { + return NOUNIT2.toString(cpData.get(row).getCP().weight); + } + } + + ) { + @Override + public int getRowCount() { + return cpData.size(); + } + }; + + table = new JTable(cpTableModel); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.setSelectionBackground(Color.LIGHT_GRAY); + table.setSelectionForeground(Color.BLACK); + cpTableModel.setColumnWidths(table.getColumnModel()); + + table.setDefaultRenderer(Object.class, new CustomCellRenderer()); + // table.setShowHorizontalLines(false); + // table.setShowVerticalLines(true); + + JScrollPane scrollpane = new JScrollPane(table); + scrollpane.setPreferredSize(new Dimension(600, 200)); + + //// Stability and Stability information + tabbedPane.addTab(trans.get("componentanalysisdlg.TabStability"), + null, scrollpane, trans.get("componentanalysisdlg.TabStability.ttip")); + + + + // Create the drag data table + dragTableModel = new ColumnTableModel( + //// Component + new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Component")) { + @Override + public Object getValueAt(int row) { + RocketComponent c = dragData.get(row).getComponent(); + if (c instanceof Rocket) { + return "Total"; + } + return c.toString(); + } + + @Override + public int getDefaultWidth() { + return 200; + } + }, + //// Pressure CD + new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Pressure")) { + @Override + public Object getValueAt(int row) { + return dragData.get(row).getPressureCD(); + } + }, + //// Base CD + new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Base")) { + @Override + public Object getValueAt(int row) { + return dragData.get(row).getBaseCD(); + } + }, + //// Friction CD + new Column(trans.get("componentanalysisdlg.dragTableModel.Col.friction")) { + @Override + public Object getValueAt(int row) { + return dragData.get(row).getFrictionCD(); + } + }, + //// Total CD + new Column(trans.get("componentanalysisdlg.dragTableModel.Col.total")) { + @Override + public Object getValueAt(int row) { + return dragData.get(row).getCD(); + } + } + ) { + @Override + public int getRowCount() { + return dragData.size(); + } + }; + + + table = new JTable(dragTableModel); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.setSelectionBackground(Color.LIGHT_GRAY); + table.setSelectionForeground(Color.BLACK); + dragTableModel.setColumnWidths(table.getColumnModel()); + + table.setDefaultRenderer(Object.class, new DragCellRenderer(new Color(0.5f, 1.0f, 0.5f))); + // table.setShowHorizontalLines(false); + // table.setShowVerticalLines(true); + + scrollpane = new JScrollPane(table); + scrollpane.setPreferredSize(new Dimension(600, 200)); + + //// Drag characteristics and Drag characteristics tooltip + tabbedPane.addTab(trans.get("componentanalysisdlg.dragTabchar"), null, scrollpane, + trans.get("componentanalysisdlg.dragTabchar.ttip")); + + + + + // Create the roll data table + rollTableModel = new ColumnTableModel( + //// Component + new Column(trans.get("componentanalysisdlg.rollTableModel.Col.component")) { + @Override + public Object getValueAt(int row) { + RocketComponent c = rollData.get(row).getComponent(); + if (c instanceof Rocket) { + return "Total"; + } + return c.toString(); + } + }, + //// Roll forcing coefficient + new Column(trans.get("componentanalysisdlg.rollTableModel.Col.rollforc")) { + @Override + public Object getValueAt(int row) { + return rollData.get(row).getCrollForce(); + } + }, + //// Roll damping coefficient + new Column(trans.get("componentanalysisdlg.rollTableModel.Col.rolldamp")) { + @Override + public Object getValueAt(int row) { + return rollData.get(row).getCrollDamp(); + } + }, + //// Total Cl + new Column(trans.get("componentanalysisdlg.rollTableModel.Col.total")) { + @Override + public Object getValueAt(int row) { + return rollData.get(row).getCroll(); + } + } + ) { + @Override + public int getRowCount() { + return rollData.size(); + } + }; + + + table = new JTable(rollTableModel); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + table.setSelectionBackground(Color.LIGHT_GRAY); + table.setSelectionForeground(Color.BLACK); + rollTableModel.setColumnWidths(table.getColumnModel()); + + scrollpane = new JScrollPane(table); + scrollpane.setPreferredSize(new Dimension(600, 200)); + + //// Roll dynamics and Roll dynamics tooltip + tabbedPane.addTab(trans.get("componentanalysisdlg.rollTableModel"), null, scrollpane, + trans.get("componentanalysisdlg.rollTableModel.ttip")); + + + + + + // Add the data updater to listen to changes in aoa and theta + mach.addChangeListener(this); + theta.addChangeListener(this); + aoa.addChangeListener(this); + roll.addChangeListener(this); + configuration.addChangeListener(this); + this.stateChanged(null); + + + + // Remove listeners when closing window + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + System.out.println("Closing method called: " + this); + theta.removeChangeListener(ComponentAnalysisDialog.this); + aoa.removeChangeListener(ComponentAnalysisDialog.this); + mach.removeChangeListener(ComponentAnalysisDialog.this); + roll.removeChangeListener(ComponentAnalysisDialog.this); + configuration.removeChangeListener(ComponentAnalysisDialog.this); + System.out.println("SETTING NAN VALUES"); + rocketPanel.setCPAOA(Double.NaN); + rocketPanel.setCPTheta(Double.NaN); + rocketPanel.setCPMach(Double.NaN); + rocketPanel.setCPRoll(Double.NaN); + singletonDialog = null; + } + }); + + //// Reference length: + panel.add(new StyledLabel(trans.get("componentanalysisdlg.lbl.reflenght"), -1), + "span, split, gapleft para, gapright rel"); + DoubleModel dm = new DoubleModel(conditions, "RefLength", UnitGroup.UNITS_LENGTH); + UnitSelector sel = new UnitSelector(dm, true); + sel.resizeFont(-1); + panel.add(sel, "gapright para"); + + //// Reference area: + panel.add(new StyledLabel(trans.get("componentanalysisdlg.lbl.refarea"), -1), "gapright rel"); + dm = new DoubleModel(conditions, "RefArea", UnitGroup.UNITS_AREA); + sel = new UnitSelector(dm, true); + sel.resizeFont(-1); + panel.add(sel, "wrap"); + + + + // Buttons + JButton button; + + // TODO: LOW: printing + // button = new JButton("Print"); + // button.addActionListener(new ActionListener() { + // public void actionPerformed(ActionEvent e) { + // try { + // table.print(); + // } catch (PrinterException e1) { + // JOptionPane.showMessageDialog(ComponentAnalysisDialog.this, + // "An error occurred while printing.", "Print error", + // JOptionPane.ERROR_MESSAGE); + // } + // } + // }); + // panel.add(button,"tag ok"); + + //button = new JButton("Close"); + //Close button + button = new JButton(trans.get("dlg.but.close")); + button.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + ComponentAnalysisDialog.this.dispose(); + } + }); + panel.add(button, "span, split, tag cancel"); + + + this.setLocationByPlatform(true); + setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + pack(); + + GUIUtil.setDisposableDialogOptions(this, null); + } + + + + /** + * Updates the data in the table and fires a table data change event. + */ + @Override + public void stateChanged(ChangeEvent e) { + AerodynamicForces forces; + WarningSet set = new WarningSet(); + conditions.setAOA(aoa.getValue()); + conditions.setTheta(theta.getValue()); + conditions.setMach(mach.getValue()); + conditions.setRollRate(roll.getValue()); + conditions.setReference(configuration); + + if (worstToggle.isSelected()) { + aerodynamicCalculator.getWorstCP(configuration, conditions, null); + if (!MathUtil.equals(conditions.getTheta(), theta.getValue())) { + fakeChange = true; + theta.setValue(conditions.getTheta()); // Fires a stateChanged event + fakeChange = false; + return; + } + } + + Map aeroData = + aerodynamicCalculator.getForceAnalysis(configuration, conditions, set); + Map massData = + massCalculator.getCGAnalysis(configuration, MassCalcType.LAUNCH_MASS); + + + cpData.clear(); + cgData.clear(); + dragData.clear(); + rollData.clear(); + for (RocketComponent c : configuration) { + forces = aeroData.get(c); + Coordinate cg = massData.get(c); + + if (forces == null) + continue; + if (forces.getCP() != null) { + cpData.add(forces); + cgData.add(cg); + } + if (!Double.isNaN(forces.getCD())) { + dragData.add(forces); + } + if (c instanceof FinSet) { + rollData.add(forces); + } + } + forces = aeroData.get(configuration.getRocket()); + if (forces != null) { + cpData.add(forces); + cgData.add(massData.get(configuration.getRocket())); + dragData.add(forces); + rollData.add(forces); + totalCD = forces.getCD(); + } else { + totalCD = 0; + } + + // Set warnings + if (set.isEmpty()) { + warningList.setListData(new String[] { + "No warnings." + }); + } else { + warningList.setListData(new Vector(set)); + } + + cpTableModel.fireTableDataChanged(); + dragTableModel.fireTableDataChanged(); + rollTableModel.fireTableDataChanged(); + } + + + private class CustomCellRenderer extends JLabel implements TableCellRenderer { + private final Font normalFont; + private final Font boldFont; + + public CustomCellRenderer() { + super(); + normalFont = getFont(); + boldFont = normalFont.deriveFont(Font.BOLD); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + + this.setText(value.toString()); + + if ((row < 0) || (row >= cpData.size())) + return this; + + if (cpData.get(row).getComponent() instanceof Rocket) { + this.setFont(boldFont); + } else { + this.setFont(normalFont); + } + return this; + } + } + + + + private class DragCellRenderer extends JLabel implements TableCellRenderer { + private final Font normalFont; + private final Font boldFont; + + + public DragCellRenderer(Color baseColor) { + super(); + normalFont = getFont(); + boldFont = normalFont.deriveFont(Font.BOLD); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + + if (value instanceof Double) { + + // A drag coefficient + double cd = (Double) value; + this.setText(String.format("%.2f (%.0f%%)", cd, 100 * cd / totalCD)); + + float r = (float) (cd / 1.5); + + float hue = MathUtil.clamp(0.3333f * (1 - 2.0f * r), 0, 0.3333f); + float sat = MathUtil.clamp(0.8f * r + 0.1f * (1 - r), 0, 1); + float val = 1.0f; + + this.setBackground(Color.getHSBColor(hue, sat, val)); + this.setOpaque(true); + this.setHorizontalAlignment(SwingConstants.CENTER); + + } else { + + // Other + this.setText(value.toString()); + this.setOpaque(false); + this.setHorizontalAlignment(SwingConstants.LEFT); + + } + + if ((row < 0) || (row >= dragData.size())) + return this; + + if ((dragData.get(row).getComponent() instanceof Rocket) || (column == 4)) { + this.setFont(boldFont); + } else { + this.setFont(normalFont); + } + return this; + } + } + + + ///////// Singleton implementation + + public static void showDialog(RocketPanel rocketpanel) { + if (singletonDialog != null) + singletonDialog.dispose(); + singletonDialog = new ComponentAnalysisDialog(rocketpanel); + singletonDialog.setVisible(true); + } + + public static void hideDialog() { + if (singletonDialog != null) + singletonDialog.dispose(); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java b/core/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java new file mode 100644 index 00000000..5e5aece8 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java @@ -0,0 +1,183 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Dialog; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.JTextField; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.startup.Application; + +public class CustomMaterialDialog extends JDialog { + + private final Material originalMaterial; + + private boolean okClicked = false; + private JComboBox typeBox; + private JTextField nameField; + private DoubleModel density; + private JSpinner densitySpinner; + private UnitSelector densityUnit; + private JCheckBox addBox; + private static final Translator trans = Application.getTranslator(); + + public CustomMaterialDialog(Window parent, Material material, boolean saveOption, + String title) { + this(parent, material, saveOption, title, null); + } + + + public CustomMaterialDialog(Window parent, Material material, boolean saveOption, + String title, String note) { + //// Custom material + super(parent, trans.get("custmatdlg.title.Custommaterial"), Dialog.ModalityType.APPLICATION_MODAL); + + this.originalMaterial = material; + + JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel")); + + + // Add title and note + if (title != null) { + panel.add(new JLabel("" + title + ":"), + "gapleft para, span, wrap" + (note == null ? " para":"")); + } + if (note != null) { + panel.add(new StyledLabel(note, -1), "span, wrap para"); + } + + + //// Material name + panel.add(new JLabel(trans.get("custmatdlg.lbl.Materialname"))); + nameField = new JTextField(15); + if (material != null) { + nameField.setText(material.getName()); + } + panel.add(nameField, "span, growx, wrap"); + + + // Material type (if not known) + panel.add(new JLabel(trans.get("custmatdlg.lbl.Materialtype"))); + if (material == null) { + typeBox = new JComboBox(Material.Type.values()); + typeBox.setSelectedItem(Material.Type.BULK); + typeBox.setEditable(false); + typeBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateDensityModel(); + } + }); + panel.add(typeBox, "span, growx, wrap"); + } else { + panel.add(new JLabel(material.getType().toString()), "span, growx, wrap"); + } + + + // Material density: + panel.add(new JLabel(trans.get("custmatdlg.lbl.Materialdensity"))); + densitySpinner = new JSpinner(); + panel.add(densitySpinner, "w 70lp"); + densityUnit = new UnitSelector((DoubleModel)null); + panel.add(densityUnit, "w 30lp"); + panel.add(new JPanel(), "growx, wrap"); + updateDensityModel(); + + + // Save option + if (saveOption) { + //// Add material to database + addBox = new JCheckBox(trans.get("custmatdlg.checkbox.Addmaterial")); + panel.add(addBox,"span, wrap"); + } + + //// OK button + JButton okButton = new JButton(trans.get("dlg.but.ok")); + + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + okClicked = true; + CustomMaterialDialog.this.setVisible(false); + } + }); + panel.add(okButton,"span, split, tag ok"); + + //// Cancel + JButton closeButton = new JButton(trans.get("dlg.but.cancel")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + okClicked = false; + CustomMaterialDialog.this.setVisible(false); + } + }); + panel.add(closeButton,"tag cancel"); + + this.setContentPane(panel); + this.pack(); + this.setLocationByPlatform(true); + GUIUtil.setDisposableDialogOptions(this, okButton); + } + + + public boolean getOkClicked() { + return okClicked; + } + + + public boolean isAddSelected() { + return addBox.isSelected(); + } + + + public Material getMaterial() { + Material.Type type; + String name; + double density; + + if (typeBox != null) { + type = (Material.Type) typeBox.getSelectedItem(); + } else { + type = originalMaterial.getType(); + } + + name = nameField.getText().trim(); + + density = this.density.getValue(); + + return Material.newMaterial(type, name, density, true); + } + + + private void updateDensityModel() { + if (originalMaterial != null) { + if (density == null) { + density = new DoubleModel(originalMaterial.getDensity(), + originalMaterial.getType().getUnitGroup(), 0); + densitySpinner.setModel(density.getSpinnerModel()); + densityUnit.setModel(density); + } + } else { + Material.Type type = (Material.Type) typeBox.getSelectedItem(); + density = new DoubleModel(0, type.getUnitGroup(), 0); + densitySpinner.setModel(density.getSpinnerModel()); + densityUnit.setModel(density); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java b/core/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java new file mode 100644 index 00000000..5c36f5d8 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java @@ -0,0 +1,537 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Rectangle; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableModel; +import javax.swing.table.TableRowSorter; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.adaptors.Column; +import net.sf.openrocket.gui.adaptors.ColumnTableModel; +import net.sf.openrocket.gui.components.SelectableLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.DelegatorLogger; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.LogLevel; +import net.sf.openrocket.logging.LogLevelBufferLogger; +import net.sf.openrocket.logging.LogLine; +import net.sf.openrocket.logging.StackTraceWriter; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.NumericComparator; + +public class DebugLogDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + + private static final int POLL_TIME = 250; + private static final String STACK_TRACE_MARK = "\uFF01"; + private static final Translator trans = Application.getTranslator(); + + private static final EnumMap backgroundColors = new EnumMap(LogLevel.class); + static { + for (LogLevel l : LogLevel.values()) { + // Just to ensure every level has a bg color + backgroundColors.put(l, Color.ORANGE); + } + final int hi = 255; + final int lo = 150; + backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo)); + backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo)); + backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi)); + backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo)); + backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo)); + backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2)); + } + + /** Buffer containing the log lines displayed */ + private final List buffer = new ArrayList(); + + /** Queue of log lines to be added to the displayed buffer */ + private final Queue queue = new ConcurrentLinkedQueue(); + + private final DelegatorLogger delegator; + private final LogListener logListener; + + private final EnumMap filterButtons = new EnumMap(LogLevel.class); + private final JCheckBox followBox; + private final Timer timer; + + + private final JTable table; + private final ColumnTableModel model; + private final TableRowSorter sorter; + + private final SelectableLabel numberLabel; + private final SelectableLabel timeLabel; + private final SelectableLabel levelLabel; + private final SelectableLabel locationLabel; + private final SelectableLabel messageLabel; + private final JTextArea stackTraceLabel; + + public DebugLogDialog(Window parent) { + //// OpenRocket debug log + super(parent, trans.get("debuglogdlg.OpenRocketdebuglog")); + + // Start listening to log lines + LogHelper applicationLog = Application.getLogger(); + if (applicationLog instanceof DelegatorLogger) { + log.info("Adding log listener"); + delegator = (DelegatorLogger) applicationLog; + logListener = new LogListener(); + delegator.addLogger(logListener); + } else { + log.warn("Application log is not a DelegatorLogger"); + delegator = null; + logListener = null; + } + + // Fetch old log lines + LogLevelBufferLogger bufferLogger = Application.getLogBuffer(); + if (bufferLogger != null) { + buffer.addAll(bufferLogger.getLogs()); + } else { + log.warn("Application does not have a log buffer"); + } + + + // Create the UI + JPanel mainPanel = new JPanel(new MigLayout("fill")); + this.add(mainPanel); + + + JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + split.setDividerLocation(0.7); + mainPanel.add(split, "grow"); + + // Top panel + JPanel panel = new JPanel(new MigLayout("fill")); + split.add(panel); + + //// Display log lines: + panel.add(new JLabel(trans.get("debuglogdlg.Displayloglines")), "gapright para, split"); + for (LogLevel l : LogLevel.values()) { + JCheckBox box = new JCheckBox(l.toString()); + // By default display DEBUG and above + box.setSelected(l.atLeast(LogLevel.DEBUG)); + box.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + sorter.setRowFilter(new LogFilter()); + } + }); + panel.add(box, "gapright unrel"); + filterButtons.put(l, box); + } + + //// Follow + followBox = new JCheckBox(trans.get("debuglogdlg.Follow")); + followBox.setSelected(true); + panel.add(followBox, "skip, gapright para, right"); + + //// Clear button + JButton clear = new JButton(trans.get("debuglogdlg.but.clear")); + clear.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Clearing log buffer"); + buffer.clear(); + queue.clear(); + model.fireTableDataChanged(); + } + }); + panel.add(clear, "right, wrap"); + + + + // Create the table model + model = new ColumnTableModel( + + new Column("#") { + @Override + public Object getValueAt(int row) { + return buffer.get(row).getLogCount(); + } + + @Override + public int getDefaultWidth() { + return 60; + } + }, + //// Time + new Column(trans.get("debuglogdlg.col.Time")) { + @Override + public Object getValueAt(int row) { + return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0); + } + + @Override + public int getDefaultWidth() { + return 60; + } + }, + //// Level + new Column(trans.get("debuglogdlg.col.Level")) { + @Override + public Object getValueAt(int row) { + return buffer.get(row).getLevel(); + } + + @Override + public int getDefaultWidth() { + return 60; + } + }, + new Column("") { + @Override + public Object getValueAt(int row) { + if (buffer.get(row).getCause() != null) { + return STACK_TRACE_MARK; + } else { + return ""; + } + } + + @Override + public int getExactWidth() { + return 16; + } + }, + //// Location + new Column(trans.get("debuglogdlg.col.Location")) { + @Override + public Object getValueAt(int row) { + TraceException e = buffer.get(row).getTrace(); + if (e != null) { + return e.getMessage(); + } else { + return ""; + } + } + + @Override + public int getDefaultWidth() { + return 200; + } + }, + //// Message + new Column(trans.get("debuglogdlg.col.Message")) { + @Override + public Object getValueAt(int row) { + return buffer.get(row).getMessage(); + } + + @Override + public int getDefaultWidth() { + return 580; + } + } + + ) { + @Override + public int getRowCount() { + return buffer.size(); + } + }; + + table = new JTable(model); + table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + table.setSelectionBackground(Color.LIGHT_GRAY); + table.setSelectionForeground(Color.BLACK); + model.setColumnWidths(table.getColumnModel()); + table.setDefaultRenderer(Object.class, new Renderer()); + + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + int row = table.getSelectedRow(); + if (row >= 0) { + row = sorter.convertRowIndexToModel(row); + } + updateSelected(row); + } + }); + + sorter = new TableRowSorter(model); + sorter.setComparator(0, NumericComparator.INSTANCE); + sorter.setComparator(1, NumericComparator.INSTANCE); + sorter.setComparator(4, new LocationComparator()); + table.setRowSorter(sorter); + sorter.setRowFilter(new LogFilter()); + + + panel.add(new JScrollPane(table), "span, grow, width " + + (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) + + "px, height 400px"); + + + panel = new JPanel(new MigLayout("fill")); + split.add(panel); + + //// Log line number: + panel.add(new JLabel(trans.get("debuglogdlg.lbl.Loglinenbr")), "split, gapright rel"); + numberLabel = new SelectableLabel(); + panel.add(numberLabel, "width 70lp, gapright para"); + + //// Time: + panel.add(new JLabel(trans.get("debuglogdlg.lbl.Time")), "split, gapright rel"); + timeLabel = new SelectableLabel(); + panel.add(timeLabel, "width 70lp, gapright para"); + + //// Level: + panel.add(new JLabel(trans.get("debuglogdlg.lbl.Level")), "split, gapright rel"); + levelLabel = new SelectableLabel(); + panel.add(levelLabel, "width 70lp, gapright para"); + + //// Location: + panel.add(new JLabel(trans.get("debuglogdlg.lbl.Location")), "split, gapright rel"); + locationLabel = new SelectableLabel(); + panel.add(locationLabel, "growx, wrap unrel"); + + //// Log message: + panel.add(new JLabel(trans.get("debuglogdlg.lbl.Logmessage")), "split, gapright rel"); + messageLabel = new SelectableLabel(); + panel.add(messageLabel, "growx, wrap para"); + + //// Stack trace: + panel.add(new JLabel(trans.get("debuglogdlg.lbl.Stacktrace")), "wrap rel"); + stackTraceLabel = new JTextArea(8, 80); + stackTraceLabel.setEditable(false); + GUIUtil.changeFontSize(stackTraceLabel, -2); + panel.add(new JScrollPane(stackTraceLabel), "grow"); + + + //Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + DebugLogDialog.this.dispose(); + } + }); + mainPanel.add(close, "newline para, right, tag ok"); + + + // Use timer to purge the queue so as not to overwhelm the EDT with events + timer = new Timer(POLL_TIME, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + purgeQueue(); + } + }); + timer.setRepeats(true); + timer.start(); + + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + log.user("Closing debug log dialog"); + timer.stop(); + if (delegator != null) { + log.info("Removing log listener"); + delegator.removeLogger(logListener); + } + } + }); + + GUIUtil.setDisposableDialogOptions(this, close); + followBox.requestFocus(); + } + + + + private void updateSelected(int row) { + if (row < 0) { + + numberLabel.setText(""); + timeLabel.setText(""); + levelLabel.setText(""); + locationLabel.setText(""); + messageLabel.setText(""); + stackTraceLabel.setText(""); + + } else { + + LogLine line = buffer.get(row); + numberLabel.setText("" + line.getLogCount()); + timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0)); + levelLabel.setText(line.getLevel().toString()); + TraceException e = line.getTrace(); + if (e != null) { + locationLabel.setText(e.getMessage()); + } else { + locationLabel.setText("-"); + } + messageLabel.setText(line.getMessage()); + Throwable t = line.getCause(); + if (t != null) { + StackTraceWriter stw = new StackTraceWriter(); + PrintWriter pw = new PrintWriter(stw); + t.printStackTrace(pw); + pw.flush(); + stackTraceLabel.setText(stw.toString()); + stackTraceLabel.setCaretPosition(0); + } else { + stackTraceLabel.setText(""); + } + + } + } + + + /** + * Check whether a row signifies a number of missing rows. This check is "heuristic" + * and checks whether the timestamp is zero and the message starts with "---". + */ + private boolean isExcludedRow(int row) { + LogLine line = buffer.get(row); + return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---")); + } + + + /** + * Purge the queue of incoming log lines. This is called periodically from the EDT, and + * it adds any lines in the queue to the buffer, and fires a table event. + */ + private void purgeQueue() { + int start = buffer.size(); + + LogLine line; + while ((line = queue.poll()) != null) { + buffer.add(line); + } + + int end = buffer.size() - 1; + if (end >= start) { + model.fireTableRowsInserted(start, end); + if (followBox.isSelected()) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + Rectangle rect = table.getCellRect(1000000000, 1, true); + table.scrollRectToVisible(rect); + } + }); + } + } + } + + + /** + * A logger that adds log lines to the queue. This method may be called from any + * thread, and therefore must be thread-safe. + */ + private class LogListener extends LogHelper { + @Override + public void log(LogLine line) { + queue.add(line); + } + } + + private class LogFilter extends RowFilter { + + @Override + public boolean include(RowFilter.Entry entry) { + int index = entry.getIdentifier(); + LogLine line = buffer.get(index); + return filterButtons.get(line.getLevel()).isSelected(); + } + + } + + + private class Renderer extends JLabel implements TableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus, + int row, int column) { + Color fg, bg; + + row = sorter.convertRowIndexToModel(row); + + if (STACK_TRACE_MARK.equals(value)) { + fg = Color.RED; + } else { + fg = table1.getForeground(); + } + bg = backgroundColors.get(buffer.get(row).getLevel()); + + if (isSelected) { + bg = bg.darker(); + } else if (isExcludedRow(row)) { + bg = bg.brighter(); + } + + this.setForeground(fg); + this.setBackground(bg); + + this.setOpaque(true); + this.setText(value.toString()); + + return this; + } + } + + + private class LocationComparator implements Comparator { + private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$"); + + @Override + public int compare(Object o1, Object o2) { + String s1 = o1.toString(); + String s2 = o2.toString(); + + Matcher m1 = splitPattern.matcher(s1); + Matcher m2 = splitPattern.matcher(s2); + + if (m1.matches() && m2.matches()) { + String class1 = m1.group(1); + String pos1 = m1.group(2); + String class2 = m2.group(1); + String pos2 = m2.group(2); + + if (class1.equals(class2)) { + return NumericComparator.INSTANCE.compare(pos1, pos2); + } else { + return class1.compareTo(class2); + } + } + + return s1.compareTo(s2); + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/DetailDialog.java b/core/src/net/sf/openrocket/gui/dialogs/DetailDialog.java new file mode 100644 index 00000000..d1f0f547 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/DetailDialog.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Component; + +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.sf.openrocket.gui.util.GUIUtil; + +public class DetailDialog { + + public static void showDetailedMessageDialog(Component parentComponent, Object message, + String details, String title, int messageType) { + + if (details != null) { + JTextArea textArea = null; + textArea = new JTextArea(5, 40); + textArea.setText(details); + textArea.setCaretPosition(0); + textArea.setEditable(false); + GUIUtil.changeFontSize(textArea, -2); + JOptionPane.showMessageDialog(parentComponent, + new Object[] { message, new JScrollPane(textArea) }, + title, messageType, null); + } else { + JOptionPane.showMessageDialog(parentComponent, message, title, messageType, null); + } + + } + + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java b/core/src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java new file mode 100644 index 00000000..0a41ce66 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java @@ -0,0 +1,506 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.dialogs.motor.MotorChooserDialog; +import net.sf.openrocket.gui.main.BasicFrame; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Chars; + +public class EditMotorConfigurationDialog extends JDialog { + + private final Rocket rocket; + + private final MotorMount[] mounts; + + private final JTable configurationTable; + private final MotorConfigurationTableModel configurationTableModel; + + + private final JButton newConfButton, removeConfButton; + private final JButton selectMotorButton, removeMotorButton; + private final JTextField configurationNameField; + + + private String currentID = null; + private MotorMount currentMount = null; + + // Positive when user is modifying configuration name + private int configurationNameModification = 0; + private static final Translator trans = Application.getTranslator(); + + public EditMotorConfigurationDialog(final Rocket rocket, Window parent) { + //// Edit motor configurations + super(parent, trans.get("edtmotorconfdlg.title.Editmotorconf")); + + if (parent != null) + this.setModalityType(ModalityType.DOCUMENT_MODAL); + else + this.setModalityType(ModalityType.APPLICATION_MODAL); + + this.rocket = rocket; + + ArrayList mountList = new ArrayList(); + Iterator iterator = rocket.iterator(); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + if (c instanceof MotorMount) { + mountList.add((MotorMount) c); + } + } + mounts = mountList.toArray(new MotorMount[0]); + + + + JPanel panel = new JPanel(new MigLayout("fill, flowy")); + + + //// Motor mount selection + //// Motor mounts: + JLabel label = new JLabel(trans.get("edtmotorconfdlg.lbl.Motormounts")); + panel.add(label, "gapbottom para"); + + //// Select which components function as motor mounts: + label = new JLabel(trans.get("edtmotorconfdlg.selectcomp")); + panel.add(label, "ay 100%, w 1px, growx"); + + + JTable table = new JTable(new MotorMountTableModel()); + table.setTableHeader(null); + table.setShowVerticalLines(false); + table.setRowSelectionAllowed(false); + table.setColumnSelectionAllowed(false); + + TableColumnModel columnModel = table.getColumnModel(); + TableColumn col0 = columnModel.getColumn(0); + int w = table.getRowHeight() + 2; + col0.setMinWidth(w); + col0.setPreferredWidth(w); + col0.setMaxWidth(w); + + table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); + + JScrollPane scroll = new JScrollPane(table); + panel.add(scroll, "w 200lp, h 150lp, grow, wrap 20lp"); + + + + + + //// Motor selection + //// Motor configurations: + label = new JLabel(trans.get("edtmotorconfdlg.lbl.Motorconfig")); + panel.add(label, "spanx, gapbottom para"); + + //// Configuration name: + label = new JLabel(trans.get("edtmotorconfdlg.lbl.Configname")); + //// Leave name empty for default. + String tip = trans.get("edtmotorconfdlg.lbl.Leavenamedefault"); + label.setToolTipText(tip); + panel.add(label, ""); + + configurationNameField = new JTextField(10); + configurationNameField.setToolTipText(tip); + configurationNameField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + update(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + update(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + update(); + } + + private void update() { + if (configurationNameModification != 0) + return; + + String text = configurationNameField.getText(); + if (currentID != null) { + configurationNameModification++; + rocket.setMotorConfigurationName(currentID, text); + int row = configurationTable.getSelectedRow(); + configurationTableModel.fireTableCellUpdated(row, 0); + updateEnabled(); + configurationNameModification--; + } + } + }); + panel.add(configurationNameField, "cell 2 1, gapright para"); + + //// New configuration + newConfButton = new JButton(trans.get("edtmotorconfdlg.but.Newconfiguration")); + newConfButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String id = rocket.newMotorConfigurationID(); + rocket.getDefaultConfiguration().setMotorConfigurationID(id); + configurationTableModel.fireTableDataChanged(); + updateEnabled(); + } + }); + panel.add(newConfButton, "cell 3 1"); + + //// Remove configuration + removeConfButton = new JButton(trans.get("edtmotorconfdlg.but.Removeconfiguration")); + removeConfButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (currentID == null) + return; + rocket.removeMotorConfigurationID(currentID); + rocket.getDefaultConfiguration().setMotorConfigurationID(null); + configurationTableModel.fireTableDataChanged(); + updateEnabled(); + } + }); + panel.add(removeConfButton, "cell 4 1"); + + + + + configurationTableModel = new MotorConfigurationTableModel(); + configurationTable = new JTable(configurationTableModel); + configurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + configurationTable.setCellSelectionEnabled(true); + + configurationTable.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + + if (e.getClickCount() == 1) { + + // Single click updates selection + updateEnabled(); + + } else if (e.getClickCount() == 2) { + + // Double-click edits motor + selectMotor(); + + } + + } + }); + + + scroll = new JScrollPane(configurationTable); + panel.add(scroll, "cell 1 2, spanx, w 500lp, h 150lp, grow"); + + //// Select motor + selectMotorButton = new JButton(trans.get("edtmotorconfdlg.but.Selectmotor")); + selectMotorButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + selectMotor(); + } + }); + panel.add(selectMotorButton, "spanx, flowx, split 2, ax 50%"); + + //// Remove motor button + removeMotorButton = new JButton(trans.get("edtmotorconfdlg.but.removemotor")); + removeMotorButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + removeMotor(); + } + }); + panel.add(removeMotorButton, "ax 50%"); + + + + //// Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + EditMotorConfigurationDialog.this.dispose(); + } + }); + panel.add(close, "spanx, right"); + + this.add(panel); + this.validate(); + this.pack(); + + updateEnabled(); + + this.setLocationByPlatform(true); + GUIUtil.setDisposableDialogOptions(this, close); + + // Undo description + final OpenRocketDocument document = BasicFrame.findDocument(rocket); + if (document != null) { + //// Edit motor configurations + document.startUndo(trans.get("edtmotorconfdlg.title.Editmotorconf")); + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + document.stopUndo(); + } + }); + } + } + + + + + private void updateEnabled() { + int column = configurationTable.getSelectedColumn(); + int row = configurationTable.getSelectedRow(); + + if (column < 0 || row < 0) { + currentID = null; + currentMount = null; + } else { + + currentID = findID(row); + if (column == 0) { + currentMount = null; + } else { + currentMount = findMount(column); + } + rocket.getDefaultConfiguration().setMotorConfigurationID(currentID); + + } + + if (configurationNameModification == 0) { + // Don't update name field when user is modifying it + configurationNameModification++; + + configurationNameField.setEnabled(currentID != null); + if (currentID == null) { + configurationNameField.setText(""); + } else { + configurationNameField.setText(rocket.getMotorConfigurationName(currentID)); + } + + configurationNameModification--; + } + removeConfButton.setEnabled(currentID != null); + selectMotorButton.setEnabled(currentMount != null && currentID != null); + removeMotorButton.setEnabled(currentMount != null && currentID != null); + } + + + + + private void selectMotor() { + if (currentID == null || currentMount == null) + return; + + MotorChooserDialog dialog = new MotorChooserDialog(currentMount.getMotor(currentID), + currentMount.getMotorDelay(currentID), currentMount.getMotorMountDiameter(), this); + dialog.setVisible(true); + Motor m = dialog.getSelectedMotor(); + double d = dialog.getSelectedDelay(); + + if (m != null) { + currentMount.setMotor(currentID, m); + currentMount.setMotorDelay(currentID, d); + } + + int row = configurationTable.getSelectedRow(); + configurationTableModel.fireTableRowsUpdated(row, row); + updateEnabled(); + } + + + private void removeMotor() { + if (currentID == null || currentMount == null) + return; + + currentMount.setMotor(currentID, null); + + int row = configurationTable.getSelectedRow(); + configurationTableModel.fireTableRowsUpdated(row, row); + updateEnabled(); + } + + + private String findID(int row) { + return rocket.getMotorConfigurationIDs()[row + 1]; + } + + + private MotorMount findMount(int column) { + MotorMount mount = null; + + int count = column; + for (MotorMount m : mounts) { + if (m.isMotorMount()) + count--; + if (count <= 0) { + mount = m; + break; + } + } + + if (mount == null) { + throw new IndexOutOfBoundsException("motor mount not found, column=" + column); + } + return mount; + } + + + /** + * The table model for selecting whether components are motor mounts or not. + */ + private class MotorMountTableModel extends AbstractTableModel { + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public int getRowCount() { + return mounts.length; + } + + @Override + public Class getColumnClass(int column) { + switch (column) { + case 0: + return Boolean.class; + + case 1: + return String.class; + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + @Override + public Object getValueAt(int row, int column) { + switch (column) { + case 0: + return new Boolean(mounts[row].isMotorMount()); + + case 1: + return mounts[row].toString(); + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + @Override + public boolean isCellEditable(int row, int column) { + return column == 0; + } + + @Override + public void setValueAt(Object value, int row, int column) { + if (column != 0 || !(value instanceof Boolean)) { + throw new IllegalArgumentException("column=" + column + ", value=" + value); + } + + mounts[row].setMotorMount((Boolean) value); + configurationTableModel.fireTableStructureChanged(); + updateEnabled(); + } + } + + + + /** + * The table model for selecting and editing the motor configurations. + */ + private class MotorConfigurationTableModel extends AbstractTableModel { + + @Override + public int getColumnCount() { + int count = 1; + for (MotorMount m : mounts) { + if (m.isMotorMount()) + count++; + } + return count; + } + + @Override + public int getRowCount() { + return rocket.getMotorConfigurationIDs().length - 1; + } + + @Override + public Object getValueAt(int row, int column) { + + String id = findID(row); + + if (column == 0) { + return rocket.getMotorConfigurationNameOrDescription(id); + } + + MotorMount mount = findMount(column); + Motor motor = mount.getMotor(id); + if (motor == null) + //// None + return "None"; + + String str = motor.getDesignation(mount.getMotorDelay(id)); + int count = mount.getMotorCount(); + if (count > 1) { + str = "" + count + Chars.TIMES + " " + str; + } + return str; + } + + + @Override + public String getColumnName(int column) { + if (column == 0) { + //// Configuration name + return trans.get("edtmotorconfdlg.lbl.Configname"); + } + + MotorMount mount = findMount(column); + String name = mount.toString(); + int count = mount.getMotorCount(); + if (count > 1) { + name = name + " (" + Chars.TIMES + count + ")"; + } + return name; + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java b/core/src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java new file mode 100644 index 00000000..95ad56df --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java @@ -0,0 +1,266 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Dialog; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.JarUtil; + +public class ExampleDesignDialog extends JDialog { + + private static final String DIRECTORY = "datafiles/examples/"; + private static final String PATTERN = ".*\\.[oO][rR][kK]$"; + private static final Translator trans = Application.getTranslator(); + + private static final FilenameFilter FILTER = new FilenameFilter() { + @Override + public boolean accept(File dir, String name) { + return name.matches(PATTERN); + } + }; + + + private boolean open = false; + private final JList designSelection; + + private ExampleDesignDialog(ExampleDesign[] designs, Window parent) { + //// Open example design + super(parent, trans.get("exdesigndlg.lbl.Openexampledesign"), Dialog.ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill")); + + //// Select example designs to open: + panel.add(new JLabel(trans.get("exdesigndlg.lbl.Selectexample")), "wrap"); + + designSelection = new JList(designs); + designSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + designSelection.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() >= 2) { + open = true; + ExampleDesignDialog.this.setVisible(false); + } + } + }); + panel.add(new JScrollPane(designSelection), "grow, wmin 300lp, wrap para"); + + //// Open button + JButton openButton = new JButton(trans.get("exdesigndlg.but.open")); + openButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + open = true; + ExampleDesignDialog.this.setVisible(false); + } + }); + panel.add(openButton, "split 2, sizegroup buttons, growx"); + + //// Cancel button + JButton cancelButton = new JButton(trans.get("dlg.but.cancel")); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + open = false; + ExampleDesignDialog.this.setVisible(false); + } + }); + panel.add(cancelButton, "sizegroup buttons, growx"); + + this.add(panel); + this.pack(); + this.setLocationByPlatform(true); + + GUIUtil.setDisposableDialogOptions(this, openButton); + } + + + /** + * Open a dialog to allow opening the example designs. + * + * @param parent the parent window of the dialog. + * @return an array of URL's to open, or null if the operation + * was cancelled. + */ + public static URL[] selectExampleDesigns(Window parent) { + + ExampleDesign[] designs; + + designs = getJarFileNames(); + if (designs == null || designs.length == 0) { + designs = getDirFileNames(); + } + if (designs == null || designs.length == 0) { + //// Example designs could not be found. + JOptionPane.showMessageDialog(parent, trans.get("exdesigndlg.lbl.Exampledesignsnotfound"), + //// Examples not found + trans.get("exdesigndlg.lbl.Examplesnotfound"), JOptionPane.ERROR_MESSAGE); + return null; + } + + Arrays.sort(designs); + + ExampleDesignDialog dialog = new ExampleDesignDialog(designs, parent); + dialog.setVisible(true); + + if (!dialog.open) { + return null; + } + + Object[] selected = dialog.designSelection.getSelectedValues(); + URL[] urls = new URL[selected.length]; + for (int i=0; i list = new ArrayList(); + int dirLength = DIRECTORY.length(); + + // Find and open the jar file this class is contained in + File file = JarUtil.getCurrentJarFile(); + if (file == null) + return null; + + + // Generate URL pointing to JAR file + URL fileUrl; + try { + fileUrl = file.toURI().toURL(); + } catch (MalformedURLException e1) { + e1.printStackTrace(); + throw new BugException(e1); + } + + // Iterate over JAR entries searching for designs + JarFile jarFile = null; + try { + jarFile = new JarFile(file); + + // Loop through JAR entries searching for files to load + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.startsWith(DIRECTORY) && FILTER.accept(null, name)) { + String urlName = "jar:" + fileUrl + "!/" + name; + URL url = new URL(urlName); + list.add(new ExampleDesign(url, + name.substring(dirLength, name.length()-4))); + } + } + + } catch (IOException e) { + // Could be normal condition if not package in JAR + return null; + } finally { + if (jarFile != null) { + try { + jarFile.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return list.toArray(new ExampleDesign[0]); + } + + + + /** + * Data holder class. + */ + private static class ExampleDesign implements Comparable { + + private final URL url; + private final String name; + + public ExampleDesign(URL url, String name) { + this.url = url; + this.name = name; + } + + @Override + public String toString() { + return name; + } + + public URL getURL() { + return url; + } + + @Override + public int compareTo(ExampleDesign o) { + return this.name.compareTo(o.name); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java b/core/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java new file mode 100644 index 00000000..c3e6141e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java @@ -0,0 +1,84 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +public class LicenseDialog extends JDialog { + private static final String LICENSE_FILENAME = "LICENSE.TXT"; + private static final Translator trans = Application.getTranslator(); + + private static final String DEFAULT_LICENSE_TEXT = + "\n" + + "Error: Unable to load " + LICENSE_FILENAME + "!\n" + + "\n" + + "OpenRocket is licensed under the GNU GPL version 3, with additional permissions.\n" + + "See http://openrocket.sourceforge.net/ for details."; + + public LicenseDialog(JFrame parent) { + super(parent, true); + + JPanel panel = new JPanel(new MigLayout("fill")); + + panel.add(new StyledLabel("OpenRocket license", 10), "ax 50%, wrap para"); + + String licenseText; + try { + + BufferedReader reader = new BufferedReader( + new InputStreamReader(ClassLoader.getSystemResourceAsStream(LICENSE_FILENAME))); + StringBuffer sb = new StringBuffer(); + for (String s = reader.readLine(); s != null; s = reader.readLine()) { + sb.append(s); + sb.append('\n'); + } + licenseText = sb.toString(); + + } catch (IOException e) { + + licenseText = DEFAULT_LICENSE_TEXT; + + } + + JTextArea text = new JTextArea(licenseText); + text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + text.setRows(20); + text.setColumns(80); + text.setEditable(false); + panel.add(new JScrollPane(text),"grow, wrap para"); + + //Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + LicenseDialog.this.dispose(); + } + }); + panel.add(close, "right"); + + this.add(panel); + this.setTitle("OpenRocket license"); + this.pack(); + this.setLocationRelativeTo(parent); + + GUIUtil.setDisposableDialogOptions(this, close); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java b/core/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java new file mode 100644 index 00000000..f19e590d --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java @@ -0,0 +1,114 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.SplashScreen; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.Timer; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.database.ThrustCurveMotorSetDatabase; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * A progress dialog displayed while loading motors. + * + * @author Sampo Niskanen + */ +public class MotorDatabaseLoadingDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + private MotorDatabaseLoadingDialog(Window parent) { + //// Loading motors + super(parent, trans.get("MotorDbLoadDlg.title"), ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill")); + //// Loading motors... + panel.add(new JLabel(trans.get("MotorDbLoadDlg.Loadingmotors")), "wrap para"); + + JProgressBar progress = new JProgressBar(); + progress.setIndeterminate(true); + panel.add(progress, "growx"); + + this.add(panel); + this.pack(); + this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + this.setLocationByPlatform(true); + GUIUtil.setWindowIcons(this); + } + + + /** + * Check whether the motor database is loaded and block until it is. + * An uncloseable modal dialog window is opened while loading unless the splash screen + * is still being displayed. + * + * @param parent the parent window for the dialog, or null + */ + public static void check(Window parent) { + // TODO - ugly blind cast + final ThrustCurveMotorSetDatabase db = (ThrustCurveMotorSetDatabase) Application.getMotorSetDatabase(); + if (db.isLoaded()) + return; + + if (SplashScreen.getSplashScreen() == null) { + + log.info(1, "Motor database not loaded yet, displaying dialog"); + + final MotorDatabaseLoadingDialog dialog = new MotorDatabaseLoadingDialog(parent); + + final Timer timer = new Timer(100, new ActionListener() { + private int count = 0; + + @Override + public void actionPerformed(ActionEvent e) { + count++; + if (db.isLoaded()) { + log.debug("Database loaded, closing dialog"); + dialog.setVisible(false); + } else if (count % 10 == 0) { + log.debug("Database not loaded, count=" + count); + } + } + }); + + db.setInUse(); + timer.start(); + dialog.setVisible(true); + timer.stop(); + + } else { + + log.info(1, "Motor database not loaded yet, splash screen still present, delaying until loaded"); + + db.setInUse(); + int count = 0; + while (!db.isLoaded()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // No-op + } + + count++; + if (count % 10 == 0) { + log.debug("Database not loaded, count=" + count); + } + } + + } + + log.info("Motor database now loaded"); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/PrintDialog.java b/core/src/net/sf/openrocket/gui/dialogs/PrintDialog.java new file mode 100644 index 00000000..729586d3 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/PrintDialog.java @@ -0,0 +1,390 @@ +/* + * PrintPanel.java + */ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Color; +import java.awt.Desktop; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Enumeration; +import java.util.Iterator; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.print.PrintController; +import net.sf.openrocket.gui.print.PrintSettings; +import net.sf.openrocket.gui.print.PrintableContext; +import net.sf.openrocket.gui.print.TemplateProperties; +import net.sf.openrocket.gui.print.components.CheckTreeManager; +import net.sf.openrocket.gui.print.components.RocketPrintTree; +import net.sf.openrocket.gui.util.FileHelper; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; + +/** + * This class isolates the Swing components used to create a panel that is added to a standard Java print dialog. + */ +public class PrintDialog extends JDialog implements TreeSelectionListener { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private final Desktop desktop; + + private final RocketPrintTree stagedTree; + private final RocketPrintTree noStagedTree; + private OpenRocketDocument document; + private RocketPrintTree currentTree; + + private JButton previewButton; + private JButton saveAsPDF; + private JButton cancel; + + + private final static SwingPreferences prefs = (SwingPreferences) Application.getPreferences(); + + /** + * Constructor. + * + * @param orDocument the OR rocket container + */ + public PrintDialog(Window parent, OpenRocketDocument orDocument) { + super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); + + + JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel")); + this.add(panel); + + + // before any Desktop APIs are used, first check whether the API is + // supported by this particular VM on this particular host + if (Desktop.isDesktopSupported()) { + desktop = Desktop.getDesktop(); + } else { + desktop = null; + } + + document = orDocument; + Rocket rocket = orDocument.getRocket(); + + noStagedTree = RocketPrintTree.create(rocket.getName()); + noStagedTree.setShowsRootHandles(false); + CheckTreeManager ctm = new net.sf.openrocket.gui.print.components.CheckTreeManager(noStagedTree); + ctm.addTreeSelectionListener(this); + + final int stages = rocket.getStageCount(); + + + JLabel label = new JLabel(trans.get("lbl.selectElements")); + panel.add(label, "wrap unrel"); + + // Create the tree + if (stages > 1) { + stagedTree = RocketPrintTree.create(rocket.getName(), rocket.getChildren()); + ctm = new CheckTreeManager(stagedTree); + stagedTree.setShowsRootHandles(false); + ctm.addTreeSelectionListener(this); + } else { + stagedTree = noStagedTree; + } + currentTree = stagedTree; + + // Add the tree to the UI + final JScrollPane scrollPane = new JScrollPane(stagedTree); + panel.add(scrollPane, "width 400lp, height 200lp, grow, wrap para"); + + + // Checkboxes and buttons + final JCheckBox sortByStage = new JCheckBox(trans.get("checkbox.showByStage")); + sortByStage.setEnabled(stages > 1); + sortByStage.setSelected(stages > 1); + sortByStage.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (sortByStage.isEnabled()) { + if (((JCheckBox) e.getSource()).isSelected()) { + scrollPane.setViewportView(stagedTree); + stagedTree.setExpandsSelectedPaths(true); + currentTree = stagedTree; + } + else { + scrollPane.setViewportView(noStagedTree); + noStagedTree.setExpandsSelectedPaths(true); + currentTree = noStagedTree; + } + } + } + }); + panel.add(sortByStage, "aligny top, split"); + + + panel.add(new JPanel(), "growx"); + + + JButton settingsButton = new JButton(trans.get("printdlg.but.settings")); + settingsButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + PrintSettings settings = getPrintSettings(); + log.debug("settings=" + settings); + PrintSettingsDialog settingsDialog = new PrintSettingsDialog(PrintDialog.this, settings); + settingsDialog.setVisible(true); + setPrintSettings(settings); + } + }); + panel.add(settingsButton, "wrap para"); + + + previewButton = new JButton(trans.get("but.previewAndPrint")); + previewButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + onPreview(); + PrintDialog.this.setVisible(false); + } + }); + panel.add(previewButton, "split, right, gap para"); + + + saveAsPDF = new JButton(trans.get("printdlg.but.saveaspdf")); + saveAsPDF.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (onSavePDF()) { + PrintDialog.this.setVisible(false); + } + } + }); + panel.add(saveAsPDF, "right, gap para"); + + + cancel = new JButton(trans.get("button.cancel")); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + PrintDialog.this.setVisible(false); + } + }); + panel.add(cancel, "right, gap para"); + + + expandAll(currentTree, true); + if (currentTree != noStagedTree) { + expandAll(noStagedTree, true); + } + + + GUIUtil.setDisposableDialogOptions(this, previewButton); + + } + + + @Override + public void valueChanged(final TreeSelectionEvent e) { + final TreePath path = e.getNewLeadSelectionPath(); + if (path != null) { + previewButton.setEnabled(true); + saveAsPDF.setEnabled(true); + } else { + previewButton.setEnabled(false); + saveAsPDF.setEnabled(false); + } + } + + /** + * If expand is true, expands all nodes in the tree. Otherwise, collapses all nodes in the theTree. + * + * @param theTree the tree to expand/contract + * @param expand expand if true, contract if not + */ + public void expandAll(RocketPrintTree theTree, boolean expand) { + TreeNode root = (TreeNode) theTree.getModel().getRoot(); + // Traverse theTree from root + expandAll(theTree, new TreePath(root), expand); + } + + /** + * Recursively walk a tree, and if expand is true, expands all nodes in the tree. Otherwise, collapses all nodes in + * the theTree. + * + * @param theTree the tree to expand/contract + * @param parent the node to iterate/recurse over + * @param expand expand if true, contract if not + */ + private void expandAll(RocketPrintTree theTree, TreePath parent, boolean expand) { + theTree.addSelectionPath(parent); + // Traverse children + TreeNode node = (TreeNode) parent.getLastPathComponent(); + if (node.getChildCount() >= 0) { + for (Enumeration e = node.children(); e.hasMoreElements();) { + TreeNode n = (TreeNode) e.nextElement(); + TreePath path = parent.pathByAddingChild(n); + expandAll(theTree, path, expand); + } + } + // Expansion or collapse must be done bottom-up + if (expand) { + theTree.expandPath(parent); + } else { + theTree.collapsePath(parent); + } + } + + + + /** + * Generate a report using a temporary file. The file will be deleted upon JVM exit. + * + * @param paper the name of the paper size + * + * @return a file, populated with the "printed" output (the rocket info) + * + * @throws IOException thrown if the file could not be generated + */ + private File generateReport(PrintSettings settings) throws IOException { + final File f = File.createTempFile("openrocket-", ".pdf"); + f.deleteOnExit(); + return generateReport(f, settings); + } + + /** + * Generate a report to a specified file. + * + * @param f the file to which rocket data will be written + * @param paper the name of the paper size + * + * @return a file, populated with the "printed" output (the rocket info) + * + * @throws IOException thrown if the file could not be generated + */ + private File generateReport(File f, PrintSettings settings) throws IOException { + Iterator toBePrinted = currentTree.getToBePrinted(); + new PrintController().print(document, toBePrinted, new FileOutputStream(f), settings); + return f; + } + + + /** + * Handler for when the Preview button is clicked. + */ + private void onPreview() { + if (desktop != null) { + try { + PrintSettings settings = getPrintSettings(); + // TODO: HIGH: Remove UIManager, and pass settings to the actual printing methods + TemplateProperties.setColors(settings); + File f = generateReport(settings); + desktop.open(f); + } catch (IOException e) { + log.error("Could not open preview.", e); + JOptionPane.showMessageDialog(this, new String[] { + trans.get("error.preview.desc1"), + trans.get("error.preview.desc2") }, + trans.get("error.preview.title"), + JOptionPane.ERROR_MESSAGE); + } + } else { + JOptionPane.showMessageDialog(this, new String[] { + trans.get("error.preview.desc1"), + trans.get("error.preview.desc2") }, + trans.get("error.preview.title"), + JOptionPane.INFORMATION_MESSAGE); + } + } + + /** + * Handler for when the "Save as PDF" button is clicked. + * + * @return true if the PDF was saved + */ + private boolean onSavePDF() { + + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(FileHelper.PDF_FILTER); + + // Select initial directory + File dir = document.getFile(); + if (dir != null) { + dir = dir.getParentFile(); + } + if (dir == null) { + dir = prefs.getDefaultDirectory(); + } + chooser.setCurrentDirectory(dir); + + int returnVal = chooser.showSaveDialog(this); + File file = chooser.getSelectedFile(); + if (returnVal == JFileChooser.APPROVE_OPTION && file != null) { + + file = FileHelper.ensureExtension(file, "pdf"); + if (!FileHelper.confirmWrite(file, this)) { + return false; + } + + try { + + PrintSettings settings = getPrintSettings(); + // TODO: HIGH: Remove UIManager, and pass settings to the actual printing methods + TemplateProperties.setColors(settings); + generateReport(file, settings); + + } catch (IOException e) { + FileHelper.errorWriting(e, this); + return false; + } + return true; + } else { + return false; + } + } + + public PrintSettings getPrintSettings() { + PrintSettings settings = new PrintSettings(); + Color c; + + c = prefs.getColor("print.template.fillColor", (java.awt.Color) null); + if (c != null) { + settings.setTemplateFillColor(c); + } + + c = prefs.getColor("print.template.borderColor", (java.awt.Color) null); + if (c != null) { + settings.setTemplateBorderColor(c); + } + + settings.setPaperSize(prefs.getEnum("print.paper.size", settings.getPaperSize())); + settings.setPaperOrientation(prefs.getEnum("print.paper.orientation", settings.getPaperOrientation())); + + return settings; + } + + public void setPrintSettings(PrintSettings settings) { + prefs.putColor("print.template.fillColor", settings.getTemplateFillColor() ); + prefs.putColor("print.template.borderColor", settings.getTemplateBorderColor() ); + prefs.putEnum("print.paper.size", settings.getPaperSize()); + prefs.putEnum("print.paper.orientation", settings.getPaperOrientation()); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/PrintSettingsDialog.java b/core/src/net/sf/openrocket/gui/dialogs/PrintSettingsDialog.java new file mode 100644 index 00000000..53411709 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/PrintSettingsDialog.java @@ -0,0 +1,118 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Color; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.components.ColorChooserButton; +import net.sf.openrocket.gui.print.PaperOrientation; +import net.sf.openrocket.gui.print.PaperSize; +import net.sf.openrocket.gui.print.PrintSettings; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * This class is a dialog for displaying advanced settings for printing rocket related info. + */ +public class PrintSettingsDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + /** + * Construct a dialog for setting the advanced rocket print settings. + * + * @param parent the owning dialog + */ + public PrintSettingsDialog(Window parent, final PrintSettings settings) { + ////Print settings + super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); + + + JPanel panel = new JPanel(new MigLayout("fill")); + + ////Template fill color: + panel.add(new JLabel(trans.get("lbl.Templatefillcolor"))); + final ColorChooserButton fillColorButton = new ColorChooserButton(settings.getTemplateFillColor()); + fillColorButton.addColorPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + Color c = (Color) evt.getNewValue(); + log.info("Template fill color changed to " + c); + settings.setTemplateFillColor(c); + } + }); + panel.add(fillColorButton, "wrap para"); + + //// Template border color: + panel.add(new JLabel(trans.get("lbl.Templatebordercolor"))); + final ColorChooserButton borderColorButton = new ColorChooserButton(settings.getTemplateBorderColor()); + borderColorButton.addColorPropertyChangeListener(new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + Color c = (Color) evt.getNewValue(); + log.info("Template border color changed to " + c); + settings.setTemplateBorderColor(c); + } + }); + panel.add(borderColorButton, "wrap para*2"); + + + + JComboBox combo = new JComboBox(new EnumModel(settings, "PaperSize")); + ////Paper size: + panel.add(new JLabel(trans.get("lbl.Papersize"))); + panel.add(combo, "growx, wrap para"); + + + combo = new JComboBox(new EnumModel(settings, "PaperOrientation")); + //// Paper orientation: + panel.add(new JLabel(trans.get("lbl.Paperorientation"))); + panel.add(combo, "growx, wrap para*2"); + + + + + //// Reset + JButton button = new JButton(trans.get("but.Reset")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Resetting print setting values to defaults"); + PrintSettings defaults = new PrintSettings(); + settings.loadFrom(defaults); + fillColorButton.setSelectedColor(settings.getTemplateFillColor()); + borderColorButton.setSelectedColor(settings.getTemplateBorderColor()); + } + }); + panel.add(button, "spanx, split, right"); + + //// Close + JButton closeButton = new JButton(trans.get("but.Close")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + PrintSettingsDialog.this.setVisible(false); + } + }); + panel.add(closeButton, "right"); + + this.add(panel); + GUIUtil.setDisposableDialogOptions(this, closeButton); + } + + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java b/core/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java new file mode 100644 index 00000000..34f3d646 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java @@ -0,0 +1,606 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSpinner; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.BodyComponent; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.IllegalFinPointException; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RadiusRingComponent; +import net.sf.openrocket.rocketcomponent.RingComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.ShockCord; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.Reflection.Method; + +/** + * Dialog that allows scaling the rocket design. + * + * @author Sampo Niskanen + */ +public class ScaleDialog extends JDialog { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + /* + * Scaler implementations + * + * Each scaled value (except override cg/mass) is defined using a Scaler instance. + */ + private static final Map, List> SCALERS = + new HashMap, List>(); + static { + List list; + + // RocketComponent + addScaler(RocketComponent.class, "PositionValue"); + SCALERS.get(RocketComponent.class).add(new OverrideScaler()); + + // BodyComponent + addScaler(BodyComponent.class, "Length"); + + // SymmetricComponent + addScaler(SymmetricComponent.class, "Thickness", "isFilled"); + + // Transition + Nose cone + addScaler(Transition.class, "ForeRadius", "isForeRadiusAutomatic"); + addScaler(Transition.class, "AftRadius", "isAftRadiusAutomatic"); + addScaler(Transition.class, "ForeShoulderRadius"); + addScaler(Transition.class, "ForeShoulderThickness"); + addScaler(Transition.class, "ForeShoulderLength"); + addScaler(Transition.class, "AftShoulderRadius"); + addScaler(Transition.class, "AftShoulderThickness"); + addScaler(Transition.class, "AftShoulderLength"); + + // Body tube + addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic"); + addScaler(BodyTube.class, "MotorOverhang"); + + // Launch lug + addScaler(LaunchLug.class, "OuterRadius"); + addScaler(LaunchLug.class, "Thickness"); + addScaler(LaunchLug.class, "Length"); + + // FinSet + addScaler(FinSet.class, "Thickness"); + addScaler(FinSet.class, "TabHeight"); + addScaler(FinSet.class, "TabLength"); + addScaler(FinSet.class, "TabShift"); + + // TrapezoidFinSet + addScaler(TrapezoidFinSet.class, "Sweep"); + addScaler(TrapezoidFinSet.class, "RootChord"); + addScaler(TrapezoidFinSet.class, "TipChord"); + addScaler(TrapezoidFinSet.class, "Height"); + + // EllipticalFinSet + addScaler(EllipticalFinSet.class, "Length"); + addScaler(EllipticalFinSet.class, "Height"); + + // FreeformFinSet + list = new ArrayList(1); + list.add(new FreeformFinSetScaler()); + SCALERS.put(FreeformFinSet.class, list); + + // MassObject + addScaler(MassObject.class, "Length"); + addScaler(MassObject.class, "Radius"); + addScaler(MassObject.class, "RadialPosition"); + + // MassComponent + list = new ArrayList(1); + list.add(new MassComponentScaler()); + SCALERS.put(MassComponent.class, list); + + // Parachute + addScaler(Parachute.class, "Diameter"); + addScaler(Parachute.class, "LineLength"); + + // Streamer + addScaler(Streamer.class, "StripLength"); + addScaler(Streamer.class, "StripWidth"); + + // ShockCord + addScaler(ShockCord.class, "CordLength"); + + // RingComponent + addScaler(RingComponent.class, "Length"); + addScaler(RingComponent.class, "RadialPosition"); + + // ThicknessRingComponent + addScaler(ThicknessRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic"); + addScaler(ThicknessRingComponent.class, "Thickness"); + + // InnerTube + addScaler(InnerTube.class, "MotorOverhang"); + + // RadiusRingComponent + addScaler(RadiusRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic"); + addScaler(RadiusRingComponent.class, "InnerRadius", "isInnerRadiusAutomatic"); + } + + private static void addScaler(Class componentClass, String methodName) { + addScaler(componentClass, methodName, null); + } + + private static void addScaler(Class componentClass, String methodName, String autoMethodName) { + List list = SCALERS.get(componentClass); + if (list == null) { + list = new ArrayList(); + SCALERS.put(componentClass, list); + } + list.add(new GeneralScaler(componentClass, methodName, autoMethodName)); + } + + + + + + private static final double DEFAULT_INITIAL_SIZE = 0.1; // meters + private static final double SCALE_MIN = 0.01; + private static final double SCALE_MAX = 100.0; + + private static final String SCALE_ROCKET = trans.get("lbl.scaleRocket"); + private static final String SCALE_SUBSELECTION = trans.get("lbl.scaleSubselection"); + private static final String SCALE_SELECTION = trans.get("lbl.scaleSelection"); + + + + + private final DoubleModel multiplier = new DoubleModel(1.0, UnitGroup.UNITS_RELATIVE, SCALE_MIN, SCALE_MAX); + private final DoubleModel fromField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); + private final DoubleModel toField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); + + private final OpenRocketDocument document; + private final RocketComponent selection; + + private final JComboBox selectionOption; + private final JCheckBox scaleMassValues; + + private boolean changing = false; + + /** + * Sole constructor. + * + * @param document the document to modify. + * @param selection the currently selected component (or null if none selected). + * @param parent the parent window. + */ + public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) { + super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); + + this.document = document; + this.selection = selection; + + // Generate options for scaling + List options = new ArrayList(); + options.add(SCALE_ROCKET); + if (selection != null && selection.getChildCount() > 0) { + options.add(SCALE_SUBSELECTION); + } + if (selection != null) { + options.add(SCALE_SELECTION); + } + + + /* + * Select initial size for "from" field. + * + * If a component is selected, either its diameter (for SymmetricComponents) or length is selected. + * Otherwise the maximum body diameter is selected. As a fallback DEFAULT_INITIAL_SIZE is used. + */ + // + double initialSize = 0; + if (selection != null) { + if (selection instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent) selection; + initialSize = s.getForeRadius() * 2; + initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); + } else { + initialSize = selection.getLength(); + } + } else { + for (RocketComponent c : document.getRocket()) { + if (c instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent) c; + initialSize = s.getForeRadius() * 2; + initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); + } + } + } + if (initialSize < 0.001) { + Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); + initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE))); + } + + fromField.setValue(initialSize); + toField.setValue(initialSize); + + + // Add actions to the values + multiplier.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (!changing) { + changing = true; + updateToField(); + changing = false; + } + } + }); + fromField.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (!changing) { + changing = true; + updateToField(); + changing = false; + } + } + }); + toField.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (!changing) { + changing = true; + updateMultiplier(); + changing = false; + } + } + }); + + + + String tip; + JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); + this.add(panel); + + + // Scaling selection + tip = trans.get("lbl.scale.ttip"); + JLabel label = new JLabel(trans.get("lbl.scale")); + label.setToolTipText(tip); + panel.add(label, "span, split, gapright unrel"); + + selectionOption = new JComboBox(options.toArray()); + selectionOption.setEditable(false); + selectionOption.setToolTipText(tip); + panel.add(selectionOption, "growx, wrap para*2"); + + + // Scale multiplier + tip = trans.get("lbl.scaling.ttip"); + label = new JLabel(trans.get("lbl.scaling")); + label.setToolTipText(tip); + panel.add(label, "gapright unrel"); + + + JSpinner spin = new JSpinner(multiplier.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + panel.add(spin, "w :30lp:65lp"); + + UnitSelector unit = new UnitSelector(multiplier); + unit.setToolTipText(tip); + panel.add(unit, "w 30lp"); + BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0)); + slider.setToolTipText(tip); + panel.add(slider, "w 100lp, growx, wrap para"); + + + // Scale from ... to ... + tip = trans.get("lbl.scaleFromTo.ttip"); + label = new JLabel(trans.get("lbl.scaleFrom")); + label.setToolTipText(tip); + panel.add(label, "gapright unrel, right"); + + spin = new JSpinner(fromField.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + panel.add(spin, "span, split, w :30lp:65lp"); + + unit = new UnitSelector(fromField); + unit.setToolTipText(tip); + panel.add(unit, "w 30lp"); + + label = new JLabel(trans.get("lbl.scaleTo")); + label.setToolTipText(tip); + panel.add(label, "gap unrel"); + + spin = new JSpinner(toField.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + panel.add(spin, "w :30lp:65lp"); + + unit = new UnitSelector(toField); + unit.setToolTipText(tip); + panel.add(unit, "w 30lp, wrap para*2"); + + + // Scale override + scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass")); + scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip")); + scaleMassValues.setSelected(true); + boolean overridden = false; + for (RocketComponent c : document.getRocket()) { + if (c instanceof MassComponent || c.isMassOverridden()) { + overridden = true; + break; + } + } + scaleMassValues.setEnabled(overridden); + panel.add(scaleMassValues, "span, wrap para*3"); + + + // Buttons + + JButton scale = new JButton(trans.get("button.scale")); + scale.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + doScale(); + ScaleDialog.this.setVisible(false); + } + }); + panel.add(scale, "span, split, right, gap para"); + + JButton cancel = new JButton(trans.get("button.cancel")); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ScaleDialog.this.setVisible(false); + } + }); + panel.add(cancel, "right, gap para"); + + + + GUIUtil.setDisposableDialogOptions(this, scale); + } + + + + private void doScale() { + double mul = multiplier.getValue(); + if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) { + Application.getExceptionHandler().handleErrorCondition("Illegal multiplier value, mul=" + mul); + return; + } + + if (MathUtil.equals(mul, 1.0)) { + // Nothing to do + log.user("Scaling by value 1.0 - nothing to do"); + return; + } + + boolean scaleMass = scaleMassValues.isSelected(); + + Object item = selectionOption.getSelectedItem(); + log.user("Scaling design by factor " + mul + ", option=" + item); + if (SCALE_ROCKET.equals(item)) { + + // Scale the entire rocket design + try { + document.startUndo(trans.get("undo.scaleRocket")); + for (RocketComponent c : document.getRocket()) { + scale(c, mul, scaleMass); + } + } finally { + document.stopUndo(); + } + + } else if (SCALE_SUBSELECTION.equals(item)) { + + // Scale component and subcomponents + try { + document.startUndo(trans.get("undo.scaleComponents")); + for (RocketComponent c : selection) { + scale(c, mul, scaleMass); + } + } finally { + document.stopUndo(); + } + + } else if (SCALE_SELECTION.equals(item)) { + + // Scale only the selected component + try { + document.startUndo(trans.get("undo.scaleComponent")); + scale(selection, mul, scaleMass); + } finally { + document.stopUndo(); + } + + } else { + throw new BugException("Unknown item selected, item=" + item); + } + } + + + /** + * Perform scaling on a single component. + */ + private void scale(RocketComponent component, double mul, boolean scaleMass) { + + Class clazz = component.getClass(); + while (clazz != null) { + List list = SCALERS.get(clazz); + if (list != null) { + for (Scaler s : list) { + s.scale(component, mul, scaleMass); + } + } + + clazz = clazz.getSuperclass(); + } + } + + + private void updateToField() { + double mul = multiplier.getValue(); + double from = fromField.getValue(); + double to = from * mul; + toField.setValue(to); + } + + private void updateMultiplier() { + double from = fromField.getValue(); + double to = toField.getValue(); + double mul = to / from; + + if (!MathUtil.equals(from, 0)) { + mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX); + multiplier.setValue(mul); + } + updateToField(); + } + + + + /** + * Interface for scaling a specific component/value. + */ + private interface Scaler { + public void scale(RocketComponent c, double multiplier, boolean scaleMass); + } + + /** + * General scaler implementation that uses reflection to get/set a specific value. + */ + private static class GeneralScaler implements Scaler { + + private final Method getter; + private final Method setter; + private final Method autoMethod; + + public GeneralScaler(Class componentClass, String methodName, String autoMethodName) { + + getter = Reflection.findMethod(componentClass, "get" + methodName); + setter = Reflection.findMethod(componentClass, "set" + methodName, double.class); + if (autoMethodName != null) { + autoMethod = Reflection.findMethod(componentClass, autoMethodName); + } else { + autoMethod = null; + } + + } + + @Override + public void scale(RocketComponent c, double multiplier, boolean scaleMass) { + + // Do not scale if set to automatic + if (autoMethod != null) { + boolean auto = (Boolean) autoMethod.invoke(c); + if (auto) { + return; + } + } + + // Scale value + double value = (Double) getter.invoke(c); + value = value * multiplier; + setter.invoke(c, value); + } + + } + + + private static class OverrideScaler implements Scaler { + + @Override + public void scale(RocketComponent component, double multiplier, boolean scaleMass) { + + if (component.isCGOverridden()) { + double cgx = component.getOverrideCGX(); + cgx = cgx * multiplier; + component.setOverrideCGX(cgx); + } + + if (scaleMass && component.isMassOverridden()) { + double mass = component.getOverrideMass(); + mass = mass * MathUtil.pow3(multiplier); + component.setOverrideMass(mass); + } + } + + } + + private static class MassComponentScaler implements Scaler { + + @Override + public void scale(RocketComponent component, double multiplier, boolean scaleMass) { + if (scaleMass) { + MassComponent c = (MassComponent) component; + double mass = c.getComponentMass(); + mass = mass * MathUtil.pow3(multiplier); + c.setComponentMass(mass); + } + } + + } + + private static class FreeformFinSetScaler implements Scaler { + + @Override + public void scale(RocketComponent component, double multiplier, boolean scaleMass) { + FreeformFinSet finset = (FreeformFinSet) component; + Coordinate[] points = finset.getFinPoints(); + for (int i = 0; i < points.length; i++) { + points[i] = points[i].multiply(multiplier); + } + try { + finset.setPoints(points); + } catch (IllegalFinPointException e) { + throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e); + } + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java b/core/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java new file mode 100644 index 00000000..13fb93b0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java @@ -0,0 +1,181 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Dimension; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.SwingWorker; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; + + +/** + * A modal dialog that runs specific SwingWorkers and waits until they complete. + * A message and progress bar is provided and a cancel button. If the cancel button + * is pressed, the currently running worker is interrupted and the later workers are not + * executed. + * + * @author Sampo Niskanen + */ +public class SwingWorkerDialog extends JDialog implements PropertyChangeListener { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + /** Number of milliseconds to wait at a time between checking worker status */ + private static final int DELAY = 100; + + /** Minimum number of milliseconds to wait before estimating work length */ + private static final int ESTIMATION_DELAY = 190; + + /** Open the dialog if estimated remaining time is longer than this */ + private static final int REMAINING_TIME_FOR_DIALOG = 1000; + + /** Open the dialog if estimated total time is longed than this */ + private static final int TOTAL_TIME_FOR_DIALOG = 2000; + + + private final SwingWorker worker; + private final JProgressBar progressBar; + + private boolean cancelled = false; + + + private SwingWorkerDialog(Window parent, String title, String label, SwingWorker w) { + super(parent, title, ModalityType.APPLICATION_MODAL); + + this.worker = w; + w.addPropertyChangeListener(this); + + JPanel panel = new JPanel(new MigLayout("fill")); + + if (label != null) { + panel.add(new JLabel(label), "wrap para"); + } + + progressBar = new JProgressBar(); + panel.add(progressBar, "growx, wrap para"); + + //// Cancel button + JButton cancel = new JButton(trans.get("dlg.but.cancel")); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("User cancelled SwingWorker operation"); + cancel(); + } + }); + panel.add(cancel, "right"); + + this.add(panel); + this.setMinimumSize(new Dimension(250, 100)); + this.pack(); + this.setLocationRelativeTo(parent); + this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (worker.getState() == SwingWorker.StateValue.DONE) { + close(); + } + progressBar.setValue(worker.getProgress()); + } + + private void cancel() { + cancelled = true; + worker.cancel(true); + close(); + } + + private void close() { + worker.removePropertyChangeListener(this); + this.setVisible(false); + // For some reason setVisible(false) is not always enough... + this.dispose(); + } + + + /** + * Run a SwingWorker and if necessary show a dialog displaying the progress of + * the worker. The progress information is obtained from the SwingWorker's + * progress property. The dialog is shown only if the worker is estimated to + * take a notable amount of time. + *

+ * The dialog contains a cancel button. Clicking it will call + * worker.cancel(true) and close the dialog immediately. + * + * @param parent the parent window for the dialog, or null. + * @param title the title for the dialog. + * @param label an additional label for the dialog, or null. + * @param worker the SwingWorker to execute. + * @return true if the worker has completed normally, + * false if the user cancelled the operation + */ + public static boolean runWorker(Window parent, String title, String label, + SwingWorker worker) { + + log.info("Running SwingWorker " + worker); + + // Start timing the worker + final long startTime = System.currentTimeMillis(); + worker.execute(); + + // Monitor worker thread before opening the dialog + while (true) { + + try { + Thread.sleep(DELAY); + } catch (InterruptedException e) { + // Should never occur + log.error("EDT was interrupted", e); + } + + if (worker.isDone()) { + // Worker has completed within time limits + log.info("Worker completed before opening dialog"); + return true; + } + + // Check whether enough time has gone to get realistic estimate + long elapsed = System.currentTimeMillis() - startTime; + if (elapsed < ESTIMATION_DELAY) + continue; + + + // Calculate and check estimated remaining time + int progress = MathUtil.clamp(worker.getProgress(), 1, 100); // Avoid div-by-zero + long estimate = elapsed * 100 / progress; + long remaining = estimate - elapsed; + + log.debug("Estimated run time, estimate=" + estimate + " remaining=" + remaining); + + if (estimate >= TOTAL_TIME_FOR_DIALOG) + break; + + if (remaining >= REMAINING_TIME_FOR_DIALOG) + break; + } + + + // Dialog is required + + log.info("Opening dialog for SwingWorker " + worker); + SwingWorkerDialog dialog = new SwingWorkerDialog(parent, title, label, worker); + dialog.setVisible(true); + log.info("Worker done, cancelled=" + dialog.cancelled); + + return !dialog.cancelled; + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java b/core/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java new file mode 100644 index 00000000..3ec6d6a1 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java @@ -0,0 +1,97 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Collections; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.communication.UpdateInfo; +import net.sf.openrocket.gui.components.URLLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.ComparablePair; + +public class UpdateInfoDialog extends JDialog { + + private final JCheckBox remind; + private static final Translator trans = Application.getTranslator(); + + public UpdateInfoDialog(UpdateInfo info) { + //// OpenRocket update available + super((Window)null, "OpenRocket update available", ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill")); + + + panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket")), + "spany 100, top"); + + //// OpenRocket version + panel.add(new JLabel("OpenRocket version " + info.getLatestVersion() + + " is available!"), "wrap para"); + + List> updates = info.getUpdates(); + if (updates.size() > 0) { + //// Updates include: + panel.add(new JLabel("Updates include:"), "wrap rel"); + + Collections.sort(updates); + int count = 0; + int n = -1; + for (int i=updates.size()-1; i>=0; i--) { + // Add only specific number of top features + if (count >= 4 && n != updates.get(i).getU()) + break; + n = updates.get(i).getU(); + panel.add(new JLabel(" " + Chars.BULLET + " " + updates.get(i).getV()), + "wrap 0px"); + count++; + } + } + + //// Download the new version from: + panel.add(new JLabel("Download the new version from:"), + "gaptop para, alignx 50%, wrap unrel"); + panel.add(new URLLabel(AboutDialog.OPENROCKET_URL), "alignx 50%, wrap para"); + + //// Remind me later + remind = new JCheckBox("Remind me later"); + //// Show this update also the next time you start OpenRocket + remind.setToolTipText("Show this update also the next time you start OpenRocket"); + remind.setSelected(true); + panel.add(remind); + + //Close button + JButton button = new JButton(trans.get("dlg.but.close")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UpdateInfoDialog.this.dispose(); + } + }); + panel.add(button, "right, gapright para"); + + this.add(panel); + + this.pack(); + this.setLocationRelativeTo(null); + GUIUtil.setDisposableDialogOptions(this, button); + } + + + public boolean isReminderSelected() { + return remind.isSelected(); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/WarningDialog.java b/core/src/net/sf/openrocket/gui/dialogs/WarningDialog.java new file mode 100644 index 00000000..30ab240f --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/WarningDialog.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.gui.dialogs; + +import java.awt.Component; + +import javax.swing.JDialog; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JScrollPane; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; + +public class WarningDialog extends JDialog { + + public static void showWarnings(Component parent, Object message, String title, + WarningSet warnings) { + + Warning[] w = warnings.toArray(new Warning[0]); + JList list = new JList(w); + JScrollPane pane = new JScrollPane(list); + + JOptionPane.showMessageDialog(parent, new Object[] { message, pane }, + title, JOptionPane.WARNING_MESSAGE); + + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/CloseableDialog.java b/core/src/net/sf/openrocket/gui/dialogs/motor/CloseableDialog.java new file mode 100644 index 00000000..48e58a0f --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/CloseableDialog.java @@ -0,0 +1,12 @@ +package net.sf.openrocket.gui.dialogs.motor; + +public interface CloseableDialog { + + /** + * Close this dialog. + * + * @param ok whether "OK" should be considered to have been clicked. + */ + public void close(boolean ok); + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java b/core/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java new file mode 100644 index 00000000..eeb939c7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java @@ -0,0 +1,115 @@ +package net.sf.openrocket.gui.dialogs.motor; + + +import java.awt.Dialog; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog; +import net.sf.openrocket.gui.dialogs.motor.thrustcurve.ThrustCurveMotorSelectionPanel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; + +public class MotorChooserDialog extends JDialog implements CloseableDialog { + + private final ThrustCurveMotorSelectionPanel selectionPanel; + + private boolean okClicked = false; + private static final Translator trans = Application.getTranslator(); + + + public MotorChooserDialog(Motor current, double delay, double diameter, Window owner) { + super(owner, "Select a rocket motor", Dialog.ModalityType.APPLICATION_MODAL); + + // Check that the motor database has been loaded properly + MotorDatabaseLoadingDialog.check(null); + + + JPanel panel = new JPanel(new MigLayout("fill")); + + selectionPanel = new ThrustCurveMotorSelectionPanel((ThrustCurveMotor) current, delay, diameter); + + panel.add(selectionPanel, "grow, wrap para"); + + + // OK / Cancel buttons + JButton okButton = new JButton(trans.get("dlg.but.ok")); + okButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + close(true); + } + }); + panel.add(okButton, "tag ok, spanx, split"); + + //// Cancel button + JButton cancelButton = new JButton(trans.get("dlg.but.cancel")); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + close(false); + } + }); + panel.add(cancelButton, "tag cancel"); + + this.add(panel); + + this.setModal(true); + this.pack(); + this.setLocationByPlatform(true); + GUIUtil.setDisposableDialogOptions(this, okButton); + + JComponent focus = selectionPanel.getDefaultFocus(); + if (focus != null) { + focus.grabFocus(); + } + + // Set the closeable dialog after all initialization + selectionPanel.setCloseableDialog(this); + } + + + /** + * Return the motor selected by this chooser dialog, or null if the selection has been aborted. + * + * @return the selected motor, or null if no motor has been selected or the selection was canceled. + */ + public Motor getSelectedMotor() { + if (!okClicked) + return null; + return selectionPanel.getSelectedMotor(); + } + + /** + * Return the selected ejection charge delay. + * + * @return the selected ejection charge delay. + */ + public double getSelectedDelay() { + return selectionPanel.getSelectedDelay(); + } + + + + @Override + public void close(boolean ok) { + okClicked = ok; + this.setVisible(false); + + Motor selected = getSelectedMotor(); + if (okClicked && selected != null) { + selectionPanel.selectedMotor(selected); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/MotorSelector.java b/core/src/net/sf/openrocket/gui/dialogs/motor/MotorSelector.java new file mode 100644 index 00000000..23bd9984 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/MotorSelector.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.gui.dialogs.motor; + +import javax.swing.JComponent; + +import net.sf.openrocket.motor.Motor; + +public interface MotorSelector { + + /** + * Return the currently selected motor. + * + * @return the currently selected motor, or null if no motor is selected. + */ + public Motor getSelectedMotor(); + + /** + * Return the currently selected ejection charge delay. + * + * @return the currently selected ejection charge delay. + */ + public double getSelectedDelay(); + + /** + * Return the component that should have the default focus in this motor selector panel. + * + * @return the component that should have default focus, or null for none. + */ + public JComponent getDefaultFocus(); + + /** + * Notify that the provided motor has been selected. This can be used to store preference + * data for later usage. + * + * @param m the motor that was selected. + */ + public void selectedMotor(Motor m); + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorHolder.java b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorHolder.java new file mode 100644 index 00000000..bb811333 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorHolder.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import net.sf.openrocket.motor.ThrustCurveMotor; + +class MotorHolder { + + private final ThrustCurveMotor motor; + private final int index; + + public MotorHolder(ThrustCurveMotor motor, int index) { + this.motor = motor; + this.index = index; + } + + public ThrustCurveMotor getMotor() { + return motor; + } + + public int getIndex() { + return index; + } + + @Override + public String toString() { + return motor.getDesignation(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MotorHolder)) + return false; + MotorHolder other = (MotorHolder) obj; + return this.motor.equals(other.motor); + } + + @Override + public int hashCode() { + return motor.hashCode(); + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorColumns.java b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorColumns.java new file mode 100644 index 00000000..d4cefca2 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorColumns.java @@ -0,0 +1,146 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.text.Collator; +import java.util.Comparator; + +import net.sf.openrocket.database.ThrustCurveMotorSet; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.DesignationComparator; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.unit.ValueComparator; + + +/** + * Enum defining the table columns. + */ + +enum ThrustCurveMotorColumns { + //// Manufacturer + MANUFACTURER("TCurveMotorCol.MANUFACTURER", 100) { + @Override + public String getValue(ThrustCurveMotorSet m) { + return m.getManufacturer().getDisplayName(); + } + + @Override + public Comparator getComparator() { + return Collator.getInstance(); + } + }, + //// Designation + DESIGNATION("TCurveMotorCol.DESIGNATION") { + @Override + public String getValue(ThrustCurveMotorSet m) { + return m.getDesignation(); + } + + @Override + public Comparator getComparator() { + return new DesignationComparator(); + } + }, + //// Type + TYPE("TCurveMotorCol.TYPE") { + @Override + public String getValue(ThrustCurveMotorSet m) { + return m.getType().getName(); + } + + @Override + public Comparator getComparator() { + return Collator.getInstance(); + } + }, + //// Diameter + DIAMETER("TCurveMotorCol.DIAMETER") { + @Override + public Object getValue(ThrustCurveMotorSet m) { + return new Value(m.getDiameter(), UnitGroup.UNITS_MOTOR_DIMENSIONS); + } + + @Override + public Comparator getComparator() { + return ValueComparator.INSTANCE; + } + }, + //// Length + LENGTH("TCurveMotorCol.LENGTH") { + @Override + public Object getValue(ThrustCurveMotorSet m) { + return new Value(m.getLength(), UnitGroup.UNITS_MOTOR_DIMENSIONS); + } + + @Override + public Comparator getComparator() { + return ValueComparator.INSTANCE; + } + }; + + + private final String title; + private final int width; + private static final Translator trans = Application.getTranslator(); + + ThrustCurveMotorColumns(String title) { + this(title, 50); + } + + ThrustCurveMotorColumns(String title, int width) { + this.title = title; + this.width = width; + } + + + public abstract Object getValue(ThrustCurveMotorSet m); + + public abstract Comparator getComparator(); + + public String getTitle() { + return trans.get(title); + } + + public int getWidth() { + return width; + } + + public String getToolTipText(ThrustCurveMotor m) { + String tip = ""; + tip += "" + m.toString() + ""; + tip += " (" + m.getMotorType().getDescription() + ")


"; + + String desc = m.getDescription().trim(); + if (desc.length() > 0) { + tip += "" + desc.replace("\n", "
") + "


"; + } + + tip += ("Diameter: " + + UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getDiameter()) + + "
"); + tip += ("Length: " + + UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getLength()) + + "
"); + tip += ("Maximum thrust: " + + UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getMaxThrustEstimate()) + + "
"); + tip += ("Average thrust: " + + UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getAverageThrustEstimate()) + + "
"); + tip += ("Burn time: " + + UnitGroup.UNITS_SHORT_TIME.getDefaultUnit() + .toStringUnit(m.getBurnTimeEstimate()) + "
"); + tip += ("Total impulse: " + + UnitGroup.UNITS_IMPULSE.getDefaultUnit() + .toStringUnit(m.getTotalImpulseEstimate()) + "
"); + tip += ("Launch mass: " + + UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(m.getLaunchCG().weight) + + "
"); + tip += ("Empty mass: " + + UnitGroup.UNITS_MASS.getDefaultUnit() + .toStringUnit(m.getEmptyCG().weight)); + return tip; + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorComparator.java b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorComparator.java new file mode 100644 index 00000000..0096bf17 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorComparator.java @@ -0,0 +1,30 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.util.Comparator; + +import net.sf.openrocket.motor.ThrustCurveMotor; + +/** + * Compares two ThrustCurveMotor objects for quality. + * + * @author Sampo Niskanen + */ +public class ThrustCurveMotorComparator implements Comparator { + + + @Override + public int compare(ThrustCurveMotor o1, ThrustCurveMotor o2) { + return calculateGoodness(o2) - calculateGoodness(o1); + } + + + private int calculateGoodness(ThrustCurveMotor motor) { + /* + * 10 chars of comments correspond to one thrust point, max ten points. + */ + int commentLength = Math.min(motor.getDescription().length(), 100); + return motor.getTimePoints().length * 10 + commentLength; + } + + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorDatabaseModel.java b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorDatabaseModel.java new file mode 100644 index 00000000..2dc92bd3 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorDatabaseModel.java @@ -0,0 +1,51 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.util.List; + +import javax.swing.table.AbstractTableModel; + +import net.sf.openrocket.database.ThrustCurveMotorSet; + +class ThrustCurveMotorDatabaseModel extends AbstractTableModel { + private final List database; + + public ThrustCurveMotorDatabaseModel(List database) { + this.database = database; + } + + @Override + public int getColumnCount() { + return ThrustCurveMotorColumns.values().length; + } + + @Override + public int getRowCount() { + return database.size(); + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + ThrustCurveMotorColumns column = getColumn(columnIndex); + return column.getValue(database.get(rowIndex)); + } + + @Override + public String getColumnName(int columnIndex) { + return getColumn(columnIndex).getTitle(); + } + + + public ThrustCurveMotorSet getMotorSet(int rowIndex) { + return database.get(rowIndex); + } + + + public int getIndex(ThrustCurveMotorSet m) { + return database.indexOf(m); + } + + private ThrustCurveMotorColumns getColumn(int index) { + return ThrustCurveMotorColumns.values()[index]; + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorPlotDialog.java b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorPlotDialog.java new file mode 100644 index 00000000..c1ecb7cf --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorPlotDialog.java @@ -0,0 +1,134 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Rectangle; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.StandardXYItemRenderer; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +public class ThrustCurveMotorPlotDialog extends JDialog { + private static final Translator trans = Application.getTranslator(); + + public ThrustCurveMotorPlotDialog(List motors, int selected, Window parent) { + super(parent, "Motor thrust curves", ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill")); + + // Thrust curve plot + JFreeChart chart = ChartFactory.createXYLineChart( + "Motor thrust curves", // title + "Time / " + UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().getUnit(), // xAxisLabel + "Thrust / " + UnitGroup.UNITS_FORCE.getDefaultUnit().getUnit(), // yAxisLabel + null, // dataset + PlotOrientation.VERTICAL, + true, // legend + true, // tooltips + false // urls + ); + + + // Add the data and formatting to the plot + XYPlot plot = chart.getXYPlot(); + + chart.setBackgroundPaint(panel.getBackground()); + plot.setBackgroundPaint(Color.WHITE); + plot.setDomainGridlinePaint(Color.LIGHT_GRAY); + plot.setRangeGridlinePaint(Color.LIGHT_GRAY); + + ChartPanel chartPanel = new ChartPanel(chart, + false, // properties + true, // save + false, // print + true, // zoom + true); // tooltips + chartPanel.setMouseWheelEnabled(true); + chartPanel.setEnforceFileExtensions(true); + chartPanel.setInitialDelay(500); + + StandardXYItemRenderer renderer = new StandardXYItemRenderer(); + renderer.setBaseShapesVisible(true); + renderer.setBaseShapesFilled(true); + plot.setRenderer(renderer); + + + // Create the plot data set + XYSeriesCollection dataset = new XYSeriesCollection(); + + // Selected thrust curve + int n = 0; + if (selected >= 0) { + dataset.addSeries(generateSeries(motors.get(selected))); + renderer.setSeriesStroke(n, new BasicStroke(1.5f)); + renderer.setSeriesPaint(n, ThrustCurveMotorSelectionPanel.getColor(selected)); + } + n++; + + // Other thrust curves + for (int i = 0; i < motors.size(); i++) { + if (i == selected) + continue; + + ThrustCurveMotor m = motors.get(i); + dataset.addSeries(generateSeries(m)); + renderer.setSeriesStroke(n, new BasicStroke(1.5f)); + renderer.setSeriesPaint(n, ThrustCurveMotorSelectionPanel.getColor(i)); + renderer.setSeriesShape(n, new Rectangle()); + n++; + } + + plot.setDataset(dataset); + + panel.add(chartPanel, "width 600:600:, height 400:400:, grow, wrap para"); + + + // Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + ThrustCurveMotorPlotDialog.this.setVisible(false); + } + }); + panel.add(close, "right, tag close"); + + + this.add(panel); + + this.pack(); + GUIUtil.setDisposableDialogOptions(this, null); + } + + + private XYSeries generateSeries(ThrustCurveMotor motor) { + XYSeries series = new XYSeries(motor.getManufacturer() + " " + motor.getDesignation()); + double[] time = motor.getTimePoints(); + double[] thrust = motor.getThrustPoints(); + + for (int j = 0; j < time.length; j++) { + series.add(time[j], thrust[j]); + } + return series; + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java new file mode 100644 index 00000000..e4c22229 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java @@ -0,0 +1,991 @@ +package net.sf.openrocket.gui.dialogs.motor.thrustcurve; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Font; +import java.awt.Paint; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.prefs.Preferences; + +import javax.swing.BorderFactory; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JLayeredPane; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSeparator; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.SwingUtilities; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.TableModel; +import javax.swing.table.TableRowSorter; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.database.ThrustCurveMotorSet; +import net.sf.openrocket.database.ThrustCurveMotorSetDatabase; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.dialogs.motor.CloseableDialog; +import net.sf.openrocket.gui.dialogs.motor.MotorSelector; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.utils.MotorCorrelation; + +import org.jfree.chart.ChartColor; +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelector { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private static final double MOTOR_SIMILARITY_THRESHOLD = 0.95; + + private static final int SHOW_ALL = 0; + private static final int SHOW_SMALLER = 1; + private static final int SHOW_EXACT = 2; + private static final String[] SHOW_DESCRIPTIONS = { + //// Show all motors + trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc1"), + //// Show motors with diameter less than that of the motor mount + trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc2"), + //// Show motors with diameter equal to that of the motor mount + trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc3") + }; + private static final int SHOW_MAX = 2; + + private static final int ZOOM_ICON_POSITION_NEGATIVE_X = 50; + private static final int ZOOM_ICON_POSITION_POSITIVE_Y = 12; + + private static final Paint[] CURVE_COLORS = ChartColor.createDefaultPaintArray(); + + private static final Color NO_COMMENT_COLOR = Color.GRAY; + private static final Color WITH_COMMENT_COLOR = Color.BLACK; + + private static final ThrustCurveMotorComparator MOTOR_COMPARATOR = new ThrustCurveMotorComparator(); + + + + private final List database; + + private final double diameter; + private CloseableDialog dialog = null; + + + private final ThrustCurveMotorDatabaseModel model; + private final JTable table; + private final TableRowSorter sorter; + + private final JCheckBox hideSimilarBox; + + private final JTextField searchField; + private String[] searchTerms = new String[0]; + + + private final JLabel curveSelectionLabel; + private final JComboBox curveSelectionBox; + private final DefaultComboBoxModel curveSelectionModel; + + private final JLabel totalImpulseLabel; + private final JLabel avgThrustLabel; + private final JLabel maxThrustLabel; + private final JLabel burnTimeLabel; + private final JLabel launchMassLabel; + private final JLabel emptyMassLabel; + private final JLabel dataPointsLabel; + private final JLabel digestLabel; + + private final JTextArea comment; + private final Font noCommentFont; + private final Font withCommentFont; + + private final JFreeChart chart; + private final ChartPanel chartPanel; + private final JLabel zoomIcon; + + private final JComboBox delayBox; + + private ThrustCurveMotor selectedMotor; + private ThrustCurveMotorSet selectedMotorSet; + private double selectedDelay; + + + /** + * Sole constructor. + * + * @param current the currently selected ThrustCurveMotor, or null for none. + * @param delay the currently selected ejection charge delay. + * @param diameter the diameter of the motor mount. + */ + public ThrustCurveMotorSelectionPanel(ThrustCurveMotor current, double delay, double diameter) { + super(new MigLayout("fill", "[grow][]")); + + this.diameter = diameter; + + + // Construct the database (adding the current motor if not in the db already) + List db; + // TODO - ugly blind cast. + db = ((ThrustCurveMotorSetDatabase) Application.getMotorSetDatabase()).getMotorSets(); + + // If current motor is not found in db, add a new ThrustCurveMotorSet containing it + if (current != null) { + selectedMotor = current; + for (ThrustCurveMotorSet motorSet : db) { + if (motorSet.getMotors().contains(current)) { + selectedMotorSet = motorSet; + break; + } + } + if (selectedMotorSet == null) { + db = new ArrayList(db); + ThrustCurveMotorSet extra = new ThrustCurveMotorSet(); + extra.addMotor(current); + selectedMotorSet = extra; + db.add(extra); + Collections.sort(db); + } + } + database = db; + + + + //// GUI + + JPanel panel; + JLabel label; + + panel = new JPanel(new MigLayout("fill")); + this.add(panel, "grow"); + + + + // Selection label + //// Select rocket motor: + label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Selrocketmotor"), Style.BOLD); + panel.add(label, "spanx, wrap para"); + + // Diameter selection + JComboBox filterComboBox = new JComboBox(SHOW_DESCRIPTIONS); + filterComboBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JComboBox cb = (JComboBox) e.getSource(); + int sel = cb.getSelectedIndex(); + if ((sel < 0) || (sel > SHOW_MAX)) + sel = SHOW_ALL; + switch (sel) { + case SHOW_ALL: + sorter.setRowFilter(new MotorRowFilterAll()); + break; + + case SHOW_SMALLER: + sorter.setRowFilter(new MotorRowFilterSmaller()); + break; + + case SHOW_EXACT: + sorter.setRowFilter(new MotorRowFilterExact()); + break; + + default: + throw new BugException("Invalid selection mode sel=" + sel); + } + Application.getPreferences().putChoice("MotorDiameterMatch", sel); + scrollSelectionVisible(); + } + }); + panel.add(filterComboBox, "spanx, growx, wrap rel"); + + //// Hide very similar thrust curves + hideSimilarBox = new JCheckBox(trans.get("TCMotorSelPan.checkbox.hideSimilar")); + GUIUtil.changeFontSize(hideSimilarBox, -1); + hideSimilarBox.setSelected(Application.getPreferences().getBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, true)); + hideSimilarBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Application.getPreferences().putBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, hideSimilarBox.isSelected()); + updateData(); + } + }); + panel.add(hideSimilarBox, "gapleft para, spanx, growx, wrap para"); + + + // Motor selection table + model = new ThrustCurveMotorDatabaseModel(database); + table = new JTable(model); + + + // Set comparators and widths + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + sorter = new TableRowSorter(model); + for (int i = 0; i < ThrustCurveMotorColumns.values().length; i++) { + ThrustCurveMotorColumns column = ThrustCurveMotorColumns.values()[i]; + sorter.setComparator(i, column.getComparator()); + table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth()); + } + table.setRowSorter(sorter); + + // Set selection and double-click listeners + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + int row = table.getSelectedRow(); + if (row >= 0) { + row = table.convertRowIndexToModel(row); + ThrustCurveMotorSet motorSet = model.getMotorSet(row); + log.user("Selected table row " + row + ": " + motorSet); + if (motorSet != selectedMotorSet) { + select(selectMotor(motorSet)); + } + } else { + log.user("Selected table row " + row + ", nothing selected"); + } + } + }); + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { + if (dialog != null) { + dialog.close(true); + } + } + } + }); + + + JScrollPane scrollpane = new JScrollPane(); + scrollpane.setViewportView(table); + panel.add(scrollpane, "grow, width :500:, height :300:, spanx, wrap para"); + + + + + // Motor mount diameter label + //// Motor mount diameter: + label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Motormountdia")+ " " + + UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter)); + panel.add(label, "gapright 30lp, spanx, split"); + + + + // Search field + //// Search: + label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Search")); + panel.add(label, ""); + + searchField = new JTextField(); + searchField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + update(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + update(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + update(); + } + + private void update() { + String text = searchField.getText().trim(); + String[] split = text.split("\\s+"); + ArrayList list = new ArrayList(); + for (String s : split) { + s = s.trim().toLowerCase(); + if (s.length() > 0) { + list.add(s); + } + } + searchTerms = list.toArray(new String[0]); + sorter.sort(); + scrollSelectionVisible(); + } + }); + panel.add(searchField, "growx, wrap"); + + + + // Vertical split + this.add(panel, "grow"); + this.add(new JSeparator(JSeparator.VERTICAL), "growy, gap para para"); + panel = new JPanel(new MigLayout("fill")); + + + + // Thrust curve selection + //// Select thrust curve: + curveSelectionLabel = new JLabel(trans.get("TCMotorSelPan.lbl.Selectthrustcurve")); + panel.add(curveSelectionLabel); + + curveSelectionModel = new DefaultComboBoxModel(); + curveSelectionBox = new JComboBox(curveSelectionModel); + curveSelectionBox.setRenderer(new CurveSelectionRenderer(curveSelectionBox.getRenderer())); + curveSelectionBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Object value = curveSelectionBox.getSelectedItem(); + if (value != null) { + select(((MotorHolder) value).getMotor()); + } + } + }); + panel.add(curveSelectionBox, "growx, wrap para"); + + + + + + // Ejection charge delay: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay"))); + + delayBox = new JComboBox(); + delayBox.setEditable(true); + delayBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JComboBox cb = (JComboBox) e.getSource(); + String sel = (String) cb.getSelectedItem(); + //// None + if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.equalsIgnoreCase.None"))) { + selectedDelay = Motor.PLUGGED; + } else { + try { + selectedDelay = Double.parseDouble(sel); + } catch (NumberFormatException ignore) { + } + } + setDelays(false); + } + }); + panel.add(delayBox, "growx, wrap rel"); + //// (Number of seconds or \"None\") + panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.NumberofsecondsorNone"), -3), "skip, wrap para"); + setDelays(false); + + + panel.add(new JSeparator(), "spanx, growx, wrap para"); + + + + // Thrust curve info + //// Total impulse: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Totalimpulse"))); + totalImpulseLabel = new JLabel(); + panel.add(totalImpulseLabel, "wrap"); + + //// Avg. thrust: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Avgthrust"))); + avgThrustLabel = new JLabel(); + panel.add(avgThrustLabel, "wrap"); + + //// Max. thrust: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Maxthrust"))); + maxThrustLabel = new JLabel(); + panel.add(maxThrustLabel, "wrap"); + + //// Burn time: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Burntime"))); + burnTimeLabel = new JLabel(); + panel.add(burnTimeLabel, "wrap"); + + //// Launch mass: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Launchmass"))); + launchMassLabel = new JLabel(); + panel.add(launchMassLabel, "wrap"); + + //// Empty mass: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Emptymass"))); + emptyMassLabel = new JLabel(); + panel.add(emptyMassLabel, "wrap"); + + //// Data points: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Datapoints"))); + dataPointsLabel = new JLabel(); + panel.add(dataPointsLabel, "wrap para"); + + if (System.getProperty("openrocket.debug.motordigest") != null) { + //// Digest: + panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Digest"))); + digestLabel = new JLabel(); + panel.add(digestLabel, "w :300:, wrap para"); + } else { + digestLabel = null; + } + + + comment = new JTextArea(5, 5); + GUIUtil.changeFontSize(comment, -2); + withCommentFont = comment.getFont(); + noCommentFont = withCommentFont.deriveFont(Font.ITALIC); + comment.setLineWrap(true); + comment.setWrapStyleWord(true); + comment.setEditable(false); + scrollpane = new JScrollPane(comment); + panel.add(scrollpane, "spanx, growx, wrap para"); + + + + + // Thrust curve plot + chart = ChartFactory.createXYLineChart( + null, // title + null, // xAxisLabel + null, // yAxisLabel + null, // dataset + PlotOrientation.VERTICAL, + false, // legend + false, // tooltips + false // urls + ); + + + // Add the data and formatting to the plot + XYPlot plot = chart.getXYPlot(); + + changeLabelFont(plot.getRangeAxis(), -2); + changeLabelFont(plot.getDomainAxis(), -2); + + //// Thrust curve: + chart.setTitle(new TextTitle(trans.get("TCMotorSelPan.title.Thrustcurve"), this.getFont())); + chart.setBackgroundPaint(this.getBackground()); + plot.setBackgroundPaint(Color.WHITE); + plot.setDomainGridlinePaint(Color.LIGHT_GRAY); + plot.setRangeGridlinePaint(Color.LIGHT_GRAY); + + chartPanel = new ChartPanel(chart, + false, // properties + false, // save + false, // print + false, // zoom + false); // tooltips + chartPanel.setMouseZoomable(false); + chartPanel.setPopupMenu(null); + chartPanel.setMouseWheelEnabled(false); + chartPanel.setRangeZoomable(false); + chartPanel.setDomainZoomable(false); + + chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + chartPanel.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (selectedMotor == null || selectedMotorSet == null) + return; + if (e.getButton() == MouseEvent.BUTTON1) { + // Open plot dialog + List motors = getFilteredCurves(); + ThrustCurveMotorPlotDialog plotDialog = new ThrustCurveMotorPlotDialog(motors, + motors.indexOf(selectedMotor), + SwingUtilities.getWindowAncestor(ThrustCurveMotorSelectionPanel.this)); + plotDialog.setVisible(true); + } + } + }); + + JLayeredPane layer = new CustomLayeredPane(); + + layer.setBorder(BorderFactory.createLineBorder(Color.BLUE)); + + layer.add(chartPanel, (Integer) 0); + + zoomIcon = new JLabel(Icons.ZOOM_IN); + zoomIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + layer.add(zoomIcon, (Integer) 1); + + + panel.add(layer, "width 300:300:, height 180:180:, grow, spanx"); + + + + this.add(panel, "grow"); + + + + // Sets the filter: + int showMode = Application.getPreferences().getChoice(net.sf.openrocket.startup.Preferences.MOTOR_DIAMETER_FILTER, SHOW_MAX, SHOW_EXACT); + filterComboBox.setSelectedIndex(showMode); + + + // Update the panel data + updateData(); + selectedDelay = delay; + setDelays(false); + + } + + + @Override + public Motor getSelectedMotor() { + return selectedMotor; + } + + + @Override + public double getSelectedDelay() { + return selectedDelay; + } + + + @Override + public JComponent getDefaultFocus() { + return searchField; + } + + @Override + public void selectedMotor(Motor motorSelection) { + if (!(motorSelection instanceof ThrustCurveMotor)) { + log.error("Received argument that was not ThrustCurveMotor: " + motorSelection); + return; + } + + ThrustCurveMotor motor = (ThrustCurveMotor) motorSelection; + ThrustCurveMotorSet set = findMotorSet(motor); + if (set == null) { + log.error("Could not find set for motor:" + motorSelection); + return; + } + + // Store selected motor in preferences node, set all others to false + Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); + for (ThrustCurveMotor m : set.getMotors()) { + String digest = MotorDigest.digestMotor(m); + prefs.putBoolean(digest, m == motor); + } + } + + public void setCloseableDialog(CloseableDialog dialog) { + this.dialog = dialog; + } + + + + private void changeLabelFont(ValueAxis axis, float size) { + Font font = axis.getTickLabelFont(); + font = font.deriveFont(font.getSize2D() + size); + axis.setTickLabelFont(font); + } + + + /** + * Called when a different motor is selected from within the panel. + */ + private void select(ThrustCurveMotor motor) { + if (selectedMotor == motor) + return; + + ThrustCurveMotorSet set = findMotorSet(motor); + if (set == null) { + throw new BugException("Could not find motor from database, motor=" + motor); + } + + boolean updateDelays = (selectedMotorSet != set); + + selectedMotor = motor; + selectedMotorSet = set; + updateData(); + if (updateDelays) { + setDelays(true); + } + } + + + private void updateData() { + + if (selectedMotorSet == null) { + // No motor selected + curveSelectionModel.removeAllElements(); + curveSelectionBox.setEnabled(false); + curveSelectionLabel.setEnabled(false); + totalImpulseLabel.setText(""); + avgThrustLabel.setText(""); + maxThrustLabel.setText(""); + burnTimeLabel.setText(""); + launchMassLabel.setText(""); + emptyMassLabel.setText(""); + dataPointsLabel.setText(""); + if (digestLabel != null) { + digestLabel.setText(""); + } + setComment(""); + chart.getXYPlot().setDataset(new XYSeriesCollection()); + return; + } + + + // Check which thrust curves to display + List motors = getFilteredCurves(); + final int index = motors.indexOf(selectedMotor); + + + // Update the thrust curve selection box + curveSelectionModel.removeAllElements(); + for (int i = 0; i < motors.size(); i++) { + curveSelectionModel.addElement(new MotorHolder(motors.get(i), i)); + } + curveSelectionBox.setSelectedIndex(index); + + if (motors.size() > 1) { + curveSelectionBox.setEnabled(true); + curveSelectionLabel.setEnabled(true); + } else { + curveSelectionBox.setEnabled(false); + curveSelectionLabel.setEnabled(false); + } + + + // Update thrust curve data + totalImpulseLabel.setText(UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit( + selectedMotor.getTotalImpulseEstimate())); + avgThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( + selectedMotor.getAverageThrustEstimate())); + maxThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( + selectedMotor.getMaxThrustEstimate())); + burnTimeLabel.setText(UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit( + selectedMotor.getBurnTimeEstimate())); + launchMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( + selectedMotor.getLaunchCG().weight)); + emptyMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( + selectedMotor.getEmptyCG().weight)); + dataPointsLabel.setText("" + (selectedMotor.getTimePoints().length - 1)); + if (digestLabel != null) { + digestLabel.setText(MotorDigest.digestMotor(selectedMotor)); + } + + setComment(selectedMotor.getDescription()); + + + // Update the plot + XYPlot plot = chart.getXYPlot(); + + XYSeriesCollection dataset = new XYSeriesCollection(); + for (int i = 0; i < motors.size(); i++) { + ThrustCurveMotor m = motors.get(i); + + //// Thrust + XYSeries series = new XYSeries(trans.get("TCMotorSelPan.title.Thrust")); + double[] time = m.getTimePoints(); + double[] thrust = m.getThrustPoints(); + + for (int j = 0; j < time.length; j++) { + series.add(time[j], thrust[j]); + } + + dataset.addSeries(series); + + boolean selected = (i == index); + plot.getRenderer().setSeriesStroke(i, new BasicStroke(selected ? 3 : 1)); + plot.getRenderer().setSeriesPaint(i, getColor(i)); + } + + plot.setDataset(dataset); + } + + + private List getFilteredCurves() { + List motors = selectedMotorSet.getMotors(); + if (hideSimilarBox.isSelected()) { + List filtered = new ArrayList(motors.size()); + for (int i = 0; i < motors.size(); i++) { + ThrustCurveMotor m = motors.get(i); + if (m.equals(selectedMotor)) { + filtered.add(m); + continue; + } + + double similarity = MotorCorrelation.similarity(selectedMotor, m); + log.debug("Motor similarity: " + similarity); + if (similarity < MOTOR_SIMILARITY_THRESHOLD) { + filtered.add(m); + } + } + motors = filtered; + } + + Collections.sort(motors, MOTOR_COMPARATOR); + + return motors; + } + + + private void setComment(String s) { + s = s.trim(); + if (s.length() == 0) { + //// No description available. + comment.setText("No description available."); + comment.setFont(noCommentFont); + comment.setForeground(NO_COMMENT_COLOR); + } else { + comment.setText(s); + comment.setFont(withCommentFont); + comment.setForeground(WITH_COMMENT_COLOR); + } + comment.setCaretPosition(0); + } + + private void scrollSelectionVisible() { + if (selectedMotorSet != null) { + int index = table.convertRowIndexToView(model.getIndex(selectedMotorSet)); + System.out.println("index=" + index); + table.getSelectionModel().setSelectionInterval(index, index); + Rectangle rect = table.getCellRect(index, 0, true); + rect = new Rectangle(rect.x, rect.y - 100, rect.width, rect.height + 200); + table.scrollRectToVisible(rect); + } + } + + + public static Color getColor(int index) { + return (Color) CURVE_COLORS[index % CURVE_COLORS.length]; + } + + + /** + * Find the ThrustCurveMotorSet that contains a motor. + * + * @param motor the motor to look for. + * @return the ThrustCurveMotorSet, or null if not found. + */ + private ThrustCurveMotorSet findMotorSet(ThrustCurveMotor motor) { + for (ThrustCurveMotorSet set : database) { + if (set.getMotors().contains(motor)) { + return set; + } + } + + return null; + } + + + + /** + * Select the default motor from this ThrustCurveMotorSet. This uses primarily motors + * that the user has previously used, and secondarily a heuristic method of selecting which + * thrust curve seems to be better or more reliable. + * + * @param set the motor set + * @return the default motor in this set + */ + private ThrustCurveMotor selectMotor(ThrustCurveMotorSet set) { + if (set.getMotorCount() == 0) { + throw new BugException("Attempting to select motor from empty ThrustCurveMotorSet: " + set); + } + if (set.getMotorCount() == 1) { + return set.getMotors().get(0); + } + + + // Find which motor has been used the most recently + List list = set.getMotors(); + Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); + for (ThrustCurveMotor m : list) { + String digest = MotorDigest.digestMotor(m); + if (prefs.getBoolean(digest, false)) { + return m; + } + } + + // No motor has been used + Collections.sort(list, MOTOR_COMPARATOR); + return list.get(0); + } + + + /** + * Set the values in the delay combo box. If reset is true + * then sets the selected value as the value closest to selectedDelay, otherwise + * leaves selection alone. + */ + private void setDelays(boolean reset) { + if (selectedMotor == null) { + + //// None + delayBox.setModel(new DefaultComboBoxModel(new String[] { trans.get("TCMotorSelPan.delayBox.None") })); + delayBox.setSelectedIndex(0); + + } else { + + List delays = selectedMotorSet.getDelays(); + String[] delayStrings = new String[delays.size()]; + double currentDelay = selectedDelay; // Store current setting locally + + for (int i = 0; i < delays.size(); i++) { + //// None + delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), trans.get("TCMotorSelPan.delayBox.None")); + } + delayBox.setModel(new DefaultComboBoxModel(delayStrings)); + + if (reset) { + + // Find and set the closest value + double closest = Double.NaN; + for (int i = 0; i < delays.size(); i++) { + // if-condition to always become true for NaN + if (!(Math.abs(delays.get(i) - currentDelay) > Math.abs(closest - currentDelay))) { + closest = delays.get(i); + } + } + if (!Double.isNaN(closest)) { + selectedDelay = closest; + //// None + delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(closest, trans.get("TCMotorSelPan.delayBox.None"))); + } else { + delayBox.setSelectedItem("None"); + } + + } else { + + selectedDelay = currentDelay; + //// None + delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, trans.get("TCMotorSelPan.delayBox.None"))); + + } + + } + } + + + + + ////////////////////// + + + private class CurveSelectionRenderer implements ListCellRenderer { + + private final ListCellRenderer renderer; + + public CurveSelectionRenderer(ListCellRenderer renderer) { + this.renderer = renderer; + } + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + + Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof MotorHolder) { + MotorHolder m = (MotorHolder) value; + c.setForeground(getColor(m.getIndex())); + } + + return c; + } + + } + + + //////// Row filters + + /** + * Abstract adapter class. + */ + private abstract class MotorRowFilter extends RowFilter { + @Override + public boolean include(RowFilter.Entry entry) { + int index = entry.getIdentifier(); + ThrustCurveMotorSet m = model.getMotorSet(index); + return filterByDiameter(m) && filterByString(m); + } + + public abstract boolean filterByDiameter(ThrustCurveMotorSet m); + + + public boolean filterByString(ThrustCurveMotorSet m) { + main: for (String s : searchTerms) { + for (ThrustCurveMotorColumns col : ThrustCurveMotorColumns.values()) { + String str = col.getValue(m).toString().toLowerCase(); + if (str.indexOf(s) >= 0) + continue main; + } + return false; + } + return true; + } + } + + /** + * Show all motors. + */ + private class MotorRowFilterAll extends MotorRowFilter { + @Override + public boolean filterByDiameter(ThrustCurveMotorSet m) { + return true; + } + } + + /** + * Show motors smaller than the mount. + */ + private class MotorRowFilterSmaller extends MotorRowFilter { + @Override + public boolean filterByDiameter(ThrustCurveMotorSet m) { + return (m.getDiameter() <= diameter + 0.0004); + } + } + + /** + * Show motors that fit the mount. + */ + private class MotorRowFilterExact extends MotorRowFilter { + @Override + public boolean filterByDiameter(ThrustCurveMotorSet m) { + return ((m.getDiameter() <= diameter + 0.0004) && (m.getDiameter() >= diameter - 0.0015)); + } + } + + + /** + * Custom layered pane that sets the bounds of the components on every layout. + */ + public class CustomLayeredPane extends JLayeredPane { + @Override + public void doLayout() { + synchronized (getTreeLock()) { + int w = getWidth(); + int h = getHeight(); + chartPanel.setBounds(0, 0, w, h); + zoomIcon.setBounds(w - ZOOM_ICON_POSITION_NEGATIVE_X, ZOOM_ICON_POSITION_POSITIVE_Y, 50, 50); + } + } + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java b/core/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java new file mode 100644 index 00000000..6162e965 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java @@ -0,0 +1,67 @@ +package net.sf.openrocket.gui.dialogs.optimization; + +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.unit.Value; + +/** + * Value object for function evaluation information. + * + * @author Sampo Niskanen + */ +public class FunctionEvaluationData { + + private final Point point; + private final Value[] state; + private final Value domainReference; + private final Value parameterValue; + private final double goalValue; + + + public FunctionEvaluationData(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue) { + this.point = point; + this.state = state.clone(); + this.domainReference = domainReference; + this.parameterValue = parameterValue; + this.goalValue = goalValue; + } + + + /** + * Return the function evaluation point (in 0...1 range). + */ + public Point getPoint() { + return point; + } + + + /** + * Return the function evaluation state in SI units + units. + */ + public Value[] getState() { + return state; + } + + + /** + * Return the domain description. + */ + public Value getDomainReference() { + return domainReference; + } + + + /** + * Return the optimization parameter value (or NaN is outside of domain). + */ + public Value getParameterValue() { + return parameterValue; + } + + + /** + * Return the function goal value. + */ + public double getGoalValue() { + return goalValue; + } +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java b/core/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java new file mode 100644 index 00000000..033da286 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java @@ -0,0 +1,1585 @@ +package net.sf.openrocket.gui.dialogs.optimization; + +import java.awt.Component; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.DefaultComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTable; +import javax.swing.JToggleButton; +import javax.swing.ListSelectionModel; +import javax.swing.Timer; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.TableColumnModel; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.CsvOptionPanel; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.DoubleCellEditor; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.components.UnitCellEditor; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.scalefigure.RocketFigure; +import net.sf.openrocket.gui.scalefigure.ScaleScrollPane; +import net.sf.openrocket.gui.util.FileHelper; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; +import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.optimization.rocketoptimization.domains.IdentitySimulationDomain; +import net.sf.openrocket.optimization.rocketoptimization.domains.StabilityDomain; +import net.sf.openrocket.optimization.rocketoptimization.goals.MaximizationGoal; +import net.sf.openrocket.optimization.rocketoptimization.goals.MinimizationGoal; +import net.sf.openrocket.optimization.rocketoptimization.goals.ValueSeekGoal; +import net.sf.openrocket.optimization.services.OptimizationServiceHelper; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.CaliberUnit; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.Named; +import net.sf.openrocket.util.TextUtil; + +import com.itextpdf.text.Font; + + +/** + * General rocket optimization dialog. + * + * @author Sampo Niskanen + */ +public class GeneralOptimizationDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private static final Collator collator = Collator.getInstance(); + + + private static final String GOAL_MAXIMIZE = trans.get("goal.maximize"); + private static final String GOAL_MINIMIZE = trans.get("goal.minimize"); + private static final String GOAL_SEEK = trans.get("goal.seek"); + + private static final String START_TEXT = trans.get("btn.start"); + private static final String STOP_TEXT = trans.get("btn.stop"); + + + + private final List optimizationParameters = new ArrayList(); + private final Map> simulationModifiers = + new HashMap>(); + + + private final OpenRocketDocument baseDocument; + private OpenRocketDocument documentCopy; + + + private final JButton addButton; + private final JButton removeButton; + private final JButton removeAllButton; + + private final ParameterSelectionTableModel selectedModifierTableModel; + private final JTable selectedModifierTable; + private final DescriptionArea selectedModifierDescription; + private final SimulationModifierTree availableModifierTree; + + private final JComboBox simulationSelectionCombo; + private final JComboBox optimizationParameterCombo; + + private final JComboBox optimizationGoalCombo; + private final JSpinner optimizationGoalSpinner; + private final UnitSelector optimizationGoalUnitSelector; + private final DoubleModel optimizationSeekValue; + + private DoubleModel minimumStability; + private DoubleModel maximumStability; + private final JCheckBox minimumStabilitySelected; + private final JSpinner minimumStabilitySpinner; + private final UnitSelector minimumStabilityUnitSelector; + private final JCheckBox maximumStabilitySelected; + private final JSpinner maximumStabilitySpinner; + private final UnitSelector maximumStabilityUnitSelector; + + private final JLabel bestValueLabel; + private final JLabel stepCountLabel; + private final JLabel evaluationCountLabel; + private final JLabel stepSizeLabel; + + private final RocketFigure figure; + private final JToggleButton startButton; + private final JButton plotButton; + private final JButton saveButton; + + private final JButton applyButton; + private final JButton resetButton; + private final JButton closeButton; + + private final List selectedModifiers = new ArrayList(); + + /** List of components to disable while optimization is running */ + private final List disableComponents = new ArrayList(); + + /** Whether optimization is currently running or not */ + private boolean running = false; + /** The optimization worker that is running */ + private OptimizationWorker worker = null; + + + private double bestValue = Double.NaN; + private Unit bestValueUnit = Unit.NOUNIT2; + private int stepCount = 0; + private int evaluationCount = 0; + private double stepSize = 0; + + private final Map evaluationHistory = new LinkedHashMap(); + private final List optimizationPath = new LinkedList(); + + + private boolean updating = false; + + + /** + * Sole constructor. + * + * @param document the document + * @param parent the parent window + */ + public GeneralOptimizationDialog(OpenRocketDocument document, Window parent) { + super(parent, trans.get("title")); + + this.baseDocument = document; + this.documentCopy = document.copy(); + + loadOptimizationParameters(); + loadSimulationModifiers(); + + JPanel sub; + JLabel label; + JScrollPane scroll; + String tip; + + JPanel panel = new JPanel(new MigLayout("fill")); + + + ChangeListener clearHistoryChangeListener = new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + clearHistory(); + } + }; + ActionListener clearHistoryActionListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + clearHistory(); + } + }; + + + + //// Selected modifiers table + + selectedModifierTableModel = new ParameterSelectionTableModel(); + selectedModifierTable = new JTable(selectedModifierTableModel); + selectedModifierTable.setDefaultRenderer(Double.class, new DoubleCellRenderer()); + selectedModifierTable.setRowSelectionAllowed(true); + selectedModifierTable.setColumnSelectionAllowed(false); + selectedModifierTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + // Make sure spinner editor fits into the cell height + selectedModifierTable.setRowHeight(new JSpinner().getPreferredSize().height - 4); + + selectedModifierTable.setDefaultEditor(Double.class, new DoubleCellEditor()); + selectedModifierTable.setDefaultEditor(Unit.class, new UnitCellEditor() { + @Override + protected UnitGroup getUnitGroup(Unit value, int row, int column) { + return selectedModifiers.get(row).getUnitGroup(); + } + }); + + disableComponents.add(selectedModifierTable); + + selectedModifierTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + updateComponents(); + } + }); + + // Set column widths + TableColumnModel columnModel = selectedModifierTable.getColumnModel(); + columnModel.getColumn(0).setPreferredWidth(150); + columnModel.getColumn(1).setPreferredWidth(40); + columnModel.getColumn(2).setPreferredWidth(40); + columnModel.getColumn(3).setPreferredWidth(40); + + scroll = new JScrollPane(selectedModifierTable); + + label = new StyledLabel(trans.get("lbl.paramsToOptimize"), Style.BOLD); + disableComponents.add(label); + panel.add(label, "split 3, flowy"); + panel.add(scroll, "wmin 300lp, height 200lp, grow"); + selectedModifierDescription = new DescriptionArea(2, -3); + disableComponents.add(selectedModifierDescription); + panel.add(selectedModifierDescription, "growx"); + + + + //// Add/remove buttons + sub = new JPanel(new MigLayout("fill")); + + addButton = new JButton(Chars.LEFT_ARROW + " " + trans.get("btn.add") + " "); + addButton.setToolTipText(trans.get("btn.add.ttip")); + addButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SimulationModifier mod = getSelectedAvailableModifier(); + if (mod != null) { + addModifier(mod); + clearHistory(); + } else { + log.error("Attempting to add simulation modifier when none is selected"); + } + } + }); + disableComponents.add(addButton); + sub.add(addButton, "wrap para, sg button"); + + removeButton = new JButton(" " + trans.get("btn.remove") + " " + Chars.RIGHT_ARROW); + removeButton.setToolTipText(trans.get("btn.remove.ttip")); + removeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SimulationModifier mod = getSelectedModifier(); + if (mod == null) { + log.error("Attempting to remove simulation modifier when none is selected"); + return; + } + removeModifier(mod); + clearHistory(); + } + }); + disableComponents.add(removeButton); + sub.add(removeButton, "wrap para*2, sg button"); + + removeAllButton = new JButton(trans.get("btn.removeAll")); + removeAllButton.setToolTipText(trans.get("btn.removeAll.ttip")); + removeAllButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Removing all selected modifiers"); + selectedModifiers.clear(); + selectedModifierTableModel.fireTableDataChanged(); + availableModifierTree.repaint(); + clearHistory(); + } + }); + disableComponents.add(removeAllButton); + sub.add(removeAllButton, "wrap para, sg button"); + + panel.add(sub); + + + + //// Available modifier tree + availableModifierTree = new SimulationModifierTree(documentCopy.getRocket(), simulationModifiers, selectedModifiers); + availableModifierTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { + @Override + public void valueChanged(TreeSelectionEvent e) { + updateComponents(); + } + }); + + // Handle double-click + availableModifierTree.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + if (e.getClickCount() == 2) { + SimulationModifier mod = getSelectedAvailableModifier(); + if (mod != null) { + addModifier(mod); + clearHistory(); + } else { + log.user("Double-clicked non-available option"); + } + } + } + }); + + disableComponents.add(availableModifierTree); + scroll = new JScrollPane(availableModifierTree); + label = new StyledLabel(trans.get("lbl.availableParams"), Style.BOLD); + disableComponents.add(label); + panel.add(label, "split 2, flowy"); + panel.add(scroll, "width 300lp, height 200lp, grow, wrap para*2"); + + + + + //// Optimization options sub-panel + + sub = new JPanel(new MigLayout("fill")); + TitledBorder border = BorderFactory.createTitledBorder(trans.get("lbl.optimizationOpts")); + GUIUtil.changeFontStyle(border, Font.BOLD); + sub.setBorder(border); + disableComponents.add(sub); + + + //// Simulation to optimize + + label = new JLabel(trans.get("lbl.optimizeSim")); + tip = trans.get("lbl.optimizeSim.ttip"); + label.setToolTipText(tip); + disableComponents.add(label); + sub.add(label, ""); + + simulationSelectionCombo = new JComboBox(); + simulationSelectionCombo.setToolTipText(tip); + populateSimulations(); + simulationSelectionCombo.addActionListener(clearHistoryActionListener); + disableComponents.add(simulationSelectionCombo); + sub.add(simulationSelectionCombo, "growx, wrap unrel"); + + + + //// Value to optimize + label = new JLabel(trans.get("lbl.optimizeValue")); + tip = trans.get("lbl.optimizeValue.ttip"); + label.setToolTipText(tip); + disableComponents.add(label); + sub.add(label, ""); + + optimizationParameterCombo = new JComboBox(); + optimizationParameterCombo.setToolTipText(tip); + populateParameters(); + optimizationParameterCombo.addActionListener(clearHistoryActionListener); + disableComponents.add(optimizationParameterCombo); + sub.add(optimizationParameterCombo, "growx, wrap unrel"); + + + + //// Optimization goal + label = new JLabel(trans.get("lbl.optimizeGoal")); + tip = trans.get("lbl.optimizeGoal"); + label.setToolTipText(tip); + disableComponents.add(label); + sub.add(label, ""); + + optimizationGoalCombo = new JComboBox(new String[] { GOAL_MAXIMIZE, GOAL_MINIMIZE, GOAL_SEEK }); + optimizationGoalCombo.setToolTipText(tip); + optimizationGoalCombo.setEditable(false); + optimizationGoalCombo.addActionListener(clearHistoryActionListener); + disableComponents.add(optimizationGoalCombo); + sub.add(optimizationGoalCombo, "growx"); + + + //// Optimization custom value + optimizationSeekValue = new DoubleModel(0, UnitGroup.UNITS_NONE); + optimizationSeekValue.addChangeListener(clearHistoryChangeListener); + + optimizationGoalSpinner = new JSpinner(optimizationSeekValue.getSpinnerModel()); + tip = trans.get("lbl.optimizeGoalValue.ttip"); + optimizationGoalSpinner.setToolTipText(tip); + optimizationGoalSpinner.setEditor(new SpinnerEditor(optimizationGoalSpinner)); + disableComponents.add(optimizationGoalSpinner); + sub.add(optimizationGoalSpinner, "width 30lp"); + + optimizationGoalUnitSelector = new UnitSelector(optimizationSeekValue); + optimizationGoalUnitSelector.setToolTipText(tip); + disableComponents.add(optimizationGoalUnitSelector); + sub.add(optimizationGoalUnitSelector, "width 20lp, wrap unrel"); + + + panel.add(sub, "grow"); + + + + //// Required stability sub-panel + + sub = new JPanel(new MigLayout("fill")); + border = BorderFactory.createTitledBorder(trans.get("lbl.requireStability")); + GUIUtil.changeFontStyle(border, Font.BOLD); + sub.setBorder(border); + disableComponents.add(sub); + + + + double ref = CaliberUnit.calculateCaliber(baseDocument.getRocket()); + minimumStability = new DoubleModel(ref, UnitGroup.stabilityUnits(ref)); + maximumStability = new DoubleModel(5 * ref, UnitGroup.stabilityUnits(ref)); + minimumStability.addChangeListener(clearHistoryChangeListener); + maximumStability.addChangeListener(clearHistoryChangeListener); + + + //// Minimum stability + tip = trans.get("lbl.requireMinStability.ttip"); + minimumStabilitySelected = new JCheckBox(trans.get("lbl.requireMinStability")); + minimumStabilitySelected.setSelected(true); + minimumStabilitySelected.setToolTipText(tip); + minimumStabilitySelected.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateComponents(); + } + }); + disableComponents.add(minimumStabilitySelected); + sub.add(minimumStabilitySelected); + + minimumStabilitySpinner = new JSpinner(minimumStability.getSpinnerModel()); + minimumStabilitySpinner.setToolTipText(tip); + minimumStabilitySpinner.setEditor(new SpinnerEditor(minimumStabilitySpinner)); + disableComponents.add(minimumStabilitySpinner); + sub.add(minimumStabilitySpinner, "growx"); + + minimumStabilityUnitSelector = new UnitSelector(minimumStability); + minimumStabilityUnitSelector.setToolTipText(tip); + disableComponents.add(minimumStabilityUnitSelector); + sub.add(minimumStabilityUnitSelector, "growx, wrap unrel"); + + + //// Maximum stability + tip = trans.get("lbl.requireMaxStability.ttip"); + maximumStabilitySelected = new JCheckBox(trans.get("lbl.requireMaxStability")); + maximumStabilitySelected.setToolTipText(tip); + maximumStabilitySelected.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + updateComponents(); + } + }); + disableComponents.add(maximumStabilitySelected); + sub.add(maximumStabilitySelected); + + maximumStabilitySpinner = new JSpinner(maximumStability.getSpinnerModel()); + maximumStabilitySpinner.setToolTipText(tip); + maximumStabilitySpinner.setEditor(new SpinnerEditor(maximumStabilitySpinner)); + disableComponents.add(maximumStabilitySpinner); + sub.add(maximumStabilitySpinner, "growx"); + + maximumStabilityUnitSelector = new UnitSelector(maximumStability); + maximumStabilityUnitSelector.setToolTipText(tip); + disableComponents.add(maximumStabilityUnitSelector); + sub.add(maximumStabilityUnitSelector, "growx, wrap para"); + + + + // DescriptionArea desc = new DescriptionArea("Stability requirements are verified during each time step of the simulation.", + // 2, -2, false); + // desc.setViewportBorder(null); + // disableComponents.add(desc); + // sub.add(desc, "span, growx"); + + + panel.add(sub, "span 2, grow, wrap para*2"); + + + + + //// Rocket figure + figure = new RocketFigure(getSelectedSimulation().getConfiguration()); + figure.setBorderPixels(1, 1); + ScaleScrollPane figureScrollPane = new ScaleScrollPane(figure); + figureScrollPane.setFitting(true); + panel.add(figureScrollPane, "span, split, height 200lp, grow"); + + + sub = new JPanel(new MigLayout("fill")); + + + label = new JLabel(trans.get("status.bestValue")); + tip = trans.get("status.bestValue.ttip"); + label.setToolTipText(tip); + sub.add(label, "gapright unrel"); + + bestValueLabel = new JLabel(); + bestValueLabel.setToolTipText(tip); + sub.add(bestValueLabel, "wmin 60lp, wrap rel"); + + + label = new JLabel(trans.get("status.stepCount")); + tip = trans.get("status.stepCount.ttip"); + label.setToolTipText(tip); + sub.add(label, "gapright unrel"); + + stepCountLabel = new JLabel(); + stepCountLabel.setToolTipText(tip); + sub.add(stepCountLabel, "wrap rel"); + + + label = new JLabel(trans.get("status.evalCount")); + tip = trans.get("status.evalCount.ttip"); + label.setToolTipText(tip); + sub.add(label, "gapright unrel"); + + evaluationCountLabel = new JLabel(); + evaluationCountLabel.setToolTipText(tip); + sub.add(evaluationCountLabel, "wrap rel"); + + + label = new JLabel(trans.get("status.stepSize")); + tip = trans.get("status.stepSize.ttip"); + label.setToolTipText(tip); + sub.add(label, "gapright unrel"); + + stepSizeLabel = new JLabel(); + stepSizeLabel.setToolTipText(tip); + sub.add(stepSizeLabel, "wrap para"); + + + //// Start/Stop button + + startButton = new JToggleButton(START_TEXT); + startButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (updating) { + log.debug("Updating, ignoring event"); + return; + } + if (running) { + log.user("Stopping optimization"); + stopOptimization(); + } else { + log.user("Starting optimization"); + startOptimization(); + } + } + }); + sub.add(startButton, "span, growx, wrap para*2"); + + + plotButton = new JButton(trans.get("btn.plotPath")); + plotButton.setToolTipText(trans.get("btn.plotPath.ttip")); + plotButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Plotting optimization path, dimensionality=" + selectedModifiers.size()); + OptimizationPlotDialog dialog = new OptimizationPlotDialog( + Collections.unmodifiableList(optimizationPath), + Collections.unmodifiableMap(evaluationHistory), + Collections.unmodifiableList(selectedModifiers), + getSelectedParameter(), + UnitGroup.stabilityUnits(getSelectedSimulation().getRocket()), + GeneralOptimizationDialog.this); + dialog.setVisible(true); + } + }); + disableComponents.add(plotButton); + sub.add(plotButton, "span, growx, wrap"); + + + saveButton = new JButton(trans.get("btn.save")); + saveButton.setToolTipText(trans.get("btn.save.ttip")); + saveButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("User selected save path"); + savePath(); + } + }); + disableComponents.add(saveButton); + sub.add(saveButton, "span, growx"); + + + + panel.add(sub, "wrap para*2"); + + + + + //// Bottom buttons + + applyButton = new JButton(trans.get("btn.apply")); + applyButton.setToolTipText(trans.get("btn.apply.ttip")); + applyButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Applying optimization changes"); + applyDesign(); + } + }); + disableComponents.add(applyButton); + panel.add(applyButton, "span, split, gapright para, right"); + + resetButton = new JButton(trans.get("btn.reset")); + resetButton.setToolTipText(trans.get("btn.reset.ttip")); + resetButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Resetting optimization design"); + resetDesign(); + } + }); + disableComponents.add(resetButton); + panel.add(resetButton, "gapright para, right"); + + closeButton = new JButton(trans.get("btn.close")); + closeButton.setToolTipText(trans.get("btn.close.ttip")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Closing optimization dialog"); + stopOptimization(); + GeneralOptimizationDialog.this.dispose(); + } + }); + panel.add(closeButton, "right"); + + + this.add(panel); + clearHistory(); + updateComponents(); + GUIUtil.setDisposableDialogOptions(this, null); + } + + + private void startOptimization() { + if (running) { + log.info("Optimization already running"); + return; + } + + + if (selectedModifiers.isEmpty()) { + JOptionPane.showMessageDialog(this, trans.get("error.selectParams.text"), + trans.get("error.selectParams.title"), JOptionPane.ERROR_MESSAGE); + updating = true; + startButton.setSelected(false); + startButton.setText(START_TEXT); + updating = false; + return; + } + + + running = true; + + // Update the button status + updating = true; + startButton.setSelected(true); + startButton.setText(STOP_TEXT); + updating = false; + + + // Create a copy of the simulation (we're going to modify the original in the current thread) + Simulation simulation = getSelectedSimulation(); + Rocket rocketCopy = simulation.getRocket().copyWithOriginalID(); + simulation = simulation.duplicateSimulation(rocketCopy); + + OptimizableParameter parameter = getSelectedParameter(); + + OptimizationGoal goal; + String value = (String) optimizationGoalCombo.getSelectedItem(); + if (GOAL_MAXIMIZE.equals(value)) { + goal = new MaximizationGoal(); + } else if (GOAL_MINIMIZE.equals(value)) { + goal = new MinimizationGoal(); + } else if (GOAL_SEEK.equals(value)) { + goal = new ValueSeekGoal(optimizationSeekValue.getValue()); + } else { + throw new BugException("optimizationGoalCombo had invalid value: " + value); + } + + SimulationDomain domain; + if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) { + double min, max; + boolean minAbsolute, maxAbsolute; + + /* + * Make minAbsolute/maxAbsolute consistent with each other to produce reasonable + * result in plot tool tips. Yes, this is a bit ugly. + */ + + // Min stability + Unit unit = minimumStability.getCurrentUnit(); + if (unit instanceof CaliberUnit) { + min = unit.toUnit(minimumStability.getValue()); + minAbsolute = false; + } else { + min = minimumStability.getValue(); + minAbsolute = true; + } + + // Max stability + unit = maximumStability.getCurrentUnit(); + if (unit instanceof CaliberUnit) { + max = unit.toUnit(maximumStability.getValue()); + maxAbsolute = false; + } else { + max = maximumStability.getValue(); + maxAbsolute = true; + } + + + if (!minimumStabilitySelected.isSelected()) { + min = Double.NaN; + minAbsolute = maxAbsolute; + } + if (!maximumStabilitySelected.isSelected()) { + max = Double.NaN; + maxAbsolute = minAbsolute; + } + + domain = new StabilityDomain(min, minAbsolute, max, maxAbsolute); + } else { + domain = new IdentitySimulationDomain(); + } + + SimulationModifier[] modifiers = selectedModifiers.toArray(new SimulationModifier[0]); + + // Create and start the background worker + worker = new OptimizationWorker(simulation, parameter, goal, domain, modifiers) { + @Override + protected void done(OptimizationException exception) { + log.info("Optimization finished, exception=" + exception, exception); + + if (exception != null) { + JOptionPane.showMessageDialog(GeneralOptimizationDialog.this, + new Object[] { + trans.get("error.optimizationFailure.text"), + exception.getLocalizedMessage() + }, trans.get("error.optimizationFailure.title"), JOptionPane.ERROR_MESSAGE); + } + + worker = null; + stopOptimization(); + + // Disable the start/stop button for a short while after ending the simulation + // to prevent accidentally starting a new optimization when trying to stop it + startButton.setEnabled(false); + Timer timer = new Timer(750, new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + startButton.setEnabled(true); + } + }); + timer.setRepeats(false); + timer.start(); + updateComponents(); + } + + @Override + protected void functionEvaluated(List data) { + for (FunctionEvaluationData d : data) { + evaluationHistory.put(d.getPoint(), d); + evaluationCount++; + } + updateCounters(); + } + + @Override + protected void optimizationStepTaken(List data) { + + // Add starting point to the path + if (optimizationPath.isEmpty()) { + optimizationPath.add(data.get(0).getOldPoint()); + } + + // Add other points to the path + for (OptimizationStepData d : data) { + optimizationPath.add(d.getNewPoint()); + } + + // Get function value from the latest point + OptimizationStepData latest = data.get(data.size() - 1); + Point newPoint = latest.getNewPoint(); + + FunctionEvaluationData pointValue = evaluationHistory.get(newPoint); + if (pointValue != null && pointValue.getParameterValue() != null) { + bestValue = pointValue.getParameterValue().getValue(); + } else { + bestValue = Double.NaN; + } + + // Update the simulation + Simulation sim = getSelectedSimulation(); + for (int i = 0; i < newPoint.dim(); i++) { + try { + selectedModifiers.get(i).modify(sim, newPoint.get(i)); + } catch (OptimizationException e) { + throw new BugException("Simulation modifier failed to modify the base simulation " + + "modifier=" + selectedModifiers.get(i), e); + } + } + figure.updateFigure(); + + // Update other counter data + stepCount += data.size(); + stepSize = latest.getStepSize(); + updateCounters(); + } + }; + worker.start(); + + + clearHistory(); + + updateComponents(); + } + + private void stopOptimization() { + if (!running) { + log.info("Optimization not running"); + return; + } + + if (worker != null && worker.isAlive()) { + log.info("Worker still running, interrupting it and setting to null"); + worker.interrupt(); + worker = null; + return; + } + + running = false; + + // Update the button status + updating = true; + startButton.setSelected(false); + startButton.setText(START_TEXT); + updating = false; + + + updateComponents(); + } + + + + + /** + * Reset the current optimization history and values. This does not reset the design. + */ + private void clearHistory() { + evaluationHistory.clear(); + optimizationPath.clear(); + bestValue = Double.NaN; + bestValueUnit = getSelectedParameter().getUnitGroup().getDefaultUnit(); + stepCount = 0; + evaluationCount = 0; + stepSize = 0.5; + updateCounters(); + updateComponents(); + } + + + private void applyDesign() { + // TODO: MEDIUM: Apply also potential changes to simulations + Rocket src = getSelectedSimulation().getRocket().copyWithOriginalID(); + Rocket dest = baseDocument.getRocket(); + try { + baseDocument.startUndo(trans.get("undoText")); + dest.freeze(); + + // Remove all children + while (dest.getChildCount() > 0) { + dest.removeChild(0); + } + + // Move all children to the destination rocket + while (src.getChildCount() > 0) { + RocketComponent c = src.getChild(0); + src.removeChild(0); + dest.addChild(c); + } + + } finally { + dest.thaw(); + baseDocument.stopUndo(); + } + } + + + private void resetDesign() { + clearHistory(); + + documentCopy = baseDocument.copy(); + + loadOptimizationParameters(); + loadSimulationModifiers(); + + // Replace selected modifiers with corresponding new modifiers + List newSelected = new ArrayList(); + for (SimulationModifier original : selectedModifiers) { + List newModifiers = simulationModifiers.get(original.getRelatedObject()); + if (newModifiers != null) { + int index = newModifiers.indexOf(original); + if (index >= 0) { + SimulationModifier updated = newModifiers.get(index); + updated.setMinValue(original.getMinValue()); + updated.setMaxValue(original.getMaxValue()); + newSelected.add(updated); + } + } + } + selectedModifiers.clear(); + selectedModifiers.addAll(newSelected); + selectedModifierTableModel.fireTableDataChanged(); + + // Update the available modifier tree + availableModifierTree.populateTree(documentCopy.getRocket(), simulationModifiers); + availableModifierTree.expandComponents(); + + + // Update selectable simulations + populateSimulations(); + + // Update selectable parameters + populateParameters(); + + } + + + private void populateSimulations() { + String current = null; + Object selection = simulationSelectionCombo.getSelectedItem(); + if (selection != null) { + current = selection.toString(); + } + + + List> simulations = new ArrayList>(); + Rocket rocket = documentCopy.getRocket(); + + for (Simulation s : documentCopy.getSimulations()) { + String id = s.getConfiguration().getMotorConfigurationID(); + String name = createSimulationName(s.getName(), rocket.getMotorConfigurationNameOrDescription(id)); + simulations.add(new Named(s, name)); + } + + for (String id : rocket.getMotorConfigurationIDs()) { + if (id == null) { + continue; + } + Simulation sim = new Simulation(rocket); + sim.getConfiguration().setMotorConfigurationID(id); + String name = createSimulationName(trans.get("basicSimulationName"), rocket.getMotorConfigurationNameOrDescription(id)); + simulations.add(new Named(sim, name)); + } + + + Simulation sim = new Simulation(rocket); + sim.getConfiguration().setMotorConfigurationID(null); + String name = createSimulationName(trans.get("noSimulationName"), rocket.getMotorConfigurationNameOrDescription(null)); + simulations.add(new Named(sim, name)); + + + simulationSelectionCombo.setModel(new DefaultComboBoxModel(simulations.toArray())); + + if (current != null) { + for (int i = 0; i < simulations.size(); i++) { + if (simulations.get(i).toString().equals(current)) { + simulationSelectionCombo.setSelectedIndex(i); + break; + } + } + } + } + + + private void populateParameters() { + String current = null; + Object selection = optimizationParameterCombo.getSelectedItem(); + if (selection != null) { + current = selection.toString(); + } else { + // Default to apogee altitude event if it is not the first one in the list + current = trans.get("MaximumAltitudeParameter.name"); + } + + List> parameters = new ArrayList>(); + for (OptimizableParameter p : optimizationParameters) { + parameters.add(new Named(p, p.getName())); + } + + optimizationParameterCombo.setModel(new DefaultComboBoxModel(parameters.toArray())); + + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i).toString().equals(current)) { + optimizationParameterCombo.setSelectedIndex(i); + break; + } + } + } + + private void updateCounters() { + bestValueLabel.setText(bestValueUnit.toStringUnit(bestValue)); + stepCountLabel.setText("" + stepCount); + evaluationCountLabel.setText("" + evaluationCount); + stepSizeLabel.setText(UnitGroup.UNITS_RELATIVE.toStringUnit(stepSize)); + } + + + private void loadOptimizationParameters() { + optimizationParameters.clear(); + optimizationParameters.addAll(OptimizationServiceHelper.getOptimizableParameters(documentCopy)); + + if (optimizationParameters.isEmpty()) { + throw new BugException("No rocket optimization parameters found, distribution built wrong."); + } + + Collections.sort(optimizationParameters, new Comparator() { + @Override + public int compare(OptimizableParameter o1, OptimizableParameter o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + } + + + private void loadSimulationModifiers() { + simulationModifiers.clear(); + + for (SimulationModifier m : OptimizationServiceHelper.getSimulationModifiers(documentCopy)) { + Object key = m.getRelatedObject(); + List list = simulationModifiers.get(key); + if (list == null) { + list = new ArrayList(); + simulationModifiers.put(key, list); + } + list.add(m); + } + + for (Object key : simulationModifiers.keySet()) { + List list = simulationModifiers.get(key); + Collections.sort(list, new Comparator() { + @Override + public int compare(SimulationModifier o1, SimulationModifier o2) { + return o1.getName().compareTo(o2.getName()); + } + }); + } + + } + + + + private void addModifier(SimulationModifier mod) { + if (!selectedModifiers.contains(mod)) { + log.user(1, "Adding simulation modifier " + mod); + selectedModifiers.add(mod); + Collections.sort(selectedModifiers, new SimulationModifierComparator()); + selectedModifierTableModel.fireTableDataChanged(); + availableModifierTree.repaint(); + } else { + log.user(1, "Attempting to add an already existing simulation modifier " + mod); + } + } + + + private void removeModifier(SimulationModifier mod) { + log.user(1, "Removing simulation modifier " + mod); + selectedModifiers.remove(mod); + selectedModifierTableModel.fireTableDataChanged(); + availableModifierTree.repaint(); + } + + + + /** + * Update the enabled status of all components in the dialog. + */ + private void updateComponents() { + boolean state; + + if (updating) { + log.debug("Ignoring updateComponents"); + return; + } + + log.debug("Running updateComponents()"); + + updating = true; + + + // First enable all components if optimization not running + if (!running) { + log.debug("Initially enabling all components"); + for (JComponent c : disableComponents) { + c.setEnabled(true); + } + } + + + // "Add" button + SimulationModifier mod = getSelectedAvailableModifier(); + state = (mod != null && !selectedModifiers.contains(mod)); + log.debug("addButton enabled: " + state); + addButton.setEnabled(state); + + // "Remove" button + state = (selectedModifierTable.getSelectedRow() >= 0); + log.debug("removeButton enabled: " + state); + removeButton.setEnabled(state); + + // "Remove all" button + state = (!selectedModifiers.isEmpty()); + log.debug("removeAllButton enabled: " + state); + removeAllButton.setEnabled(state); + + + // Optimization goal + String selected = (String) optimizationGoalCombo.getSelectedItem(); + state = GOAL_SEEK.equals(selected); + log.debug("optimizationGoalSpinner & UnitSelector enabled: " + state); + optimizationGoalSpinner.setVisible(state); + optimizationGoalUnitSelector.setVisible(state); + + + // Minimum/maximum stability options + state = minimumStabilitySelected.isSelected(); + log.debug("minimumStabilitySpinner & UnitSelector enabled: " + state); + minimumStabilitySpinner.setEnabled(state); + minimumStabilityUnitSelector.setEnabled(state); + + state = maximumStabilitySelected.isSelected(); + log.debug("maximumStabilitySpimmer & UnitSelector enabled: " + state); + maximumStabilitySpinner.setEnabled(state); + maximumStabilityUnitSelector.setEnabled(state); + + + // Plot button (enabled if path exists and dimensionality is 1 or 2) + state = (!optimizationPath.isEmpty() && (selectedModifiers.size() == 1 || selectedModifiers.size() == 2)); + log.debug("plotButton enabled: " + state + " optimizationPath.isEmpty=" + optimizationPath.isEmpty() + + " selectedModifiers.size=" + selectedModifiers.size()); + plotButton.setEnabled(state); + + // Save button (enabled if path exists) + state = (!evaluationHistory.isEmpty()); + log.debug("saveButton enabled: " + state); + saveButton.setEnabled(state); + + + // Last disable all components if optimization is running + if (running) { + log.debug("Disabling all components because optimization is running"); + for (JComponent c : disableComponents) { + c.setEnabled(false); + } + } + + + // Update description text + mod = getSelectedModifier(); + if (mod != null) { + selectedModifierDescription.setText(mod.getDescription()); + } else { + selectedModifierDescription.setText(""); + } + + + // Update the figure + figure.setConfiguration(getSelectedSimulation().getConfiguration()); + + updating = false; + } + + + private void savePath() { + + if (evaluationHistory.isEmpty()) { + throw new BugException("evaluation history is empty"); + } + + CsvOptionPanel csvOptions = new CsvOptionPanel(GeneralOptimizationDialog.class, + trans.get("export.header"), trans.get("export.header.ttip")); + + + JFileChooser chooser = new JFileChooser(); + chooser.setFileFilter(FileHelper.CSV_FILE_FILTER); + chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + chooser.setAccessory(csvOptions); + + if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) + return; + + File file = chooser.getSelectedFile(); + if (file == null) + return; + + file = FileHelper.ensureExtension(file, "csv"); + if (!FileHelper.confirmWrite(file, this)) { + return; + } + + String fieldSeparator = csvOptions.getFieldSeparator(); + String commentCharacter = csvOptions.getCommentCharacter(); + boolean includeHeader = csvOptions.getSelectionOption(0); + csvOptions.storePreferences(); + + log.info("Saving optimization path to " + file + ", fieldSeparator=" + fieldSeparator + + ", commentCharacter=" + commentCharacter + ", includeHeader=" + includeHeader); + + try { + Writer writer = new BufferedWriter(new FileWriter(file)); + + // Write header + if (includeHeader) { + FunctionEvaluationData data = evaluationHistory.values().iterator().next(); + + writer.write(commentCharacter); + for (SimulationModifier mod : selectedModifiers) { + writer.write(mod.getRelatedObject().toString() + ": " + mod.getName() + " / " + + mod.getUnitGroup().getDefaultUnit().getUnit()); + writer.write(fieldSeparator); + } + if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) { + writer.write(trans.get("export.stability") + " / " + data.getDomainReference().getUnit().getUnit()); + writer.write(fieldSeparator); + } + writer.write(getSelectedParameter().getName() + " / " + + getSelectedParameter().getUnitGroup().getDefaultUnit().getUnit()); + + writer.write("\n"); + } + + + for (FunctionEvaluationData data : evaluationHistory.values()) { + Value[] state = data.getState(); + + for (int i = 0; i < state.length; i++) { + writer.write(TextUtil.doubleToString(state[i].getUnitValue())); + writer.write(fieldSeparator); + } + + if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) { + writer.write(TextUtil.doubleToString(data.getDomainReference().getUnitValue())); + writer.write(fieldSeparator); + } + + if (data.getParameterValue() != null) { + writer.write(TextUtil.doubleToString(data.getParameterValue().getUnitValue())); + } else { + writer.write("N/A"); + } + writer.write("\n"); + } + + writer.close(); + log.info("File successfully saved"); + + } catch (IOException e) { + FileHelper.errorWriting(e, this); + } + + } + + /** + * Return the currently selected available simulation modifier from the modifier tree, + * or null if none selected. + */ + private SimulationModifier getSelectedAvailableModifier() { + TreePath treepath = availableModifierTree.getSelectionPath(); + if (treepath != null) { + Object o = ((DefaultMutableTreeNode) treepath.getLastPathComponent()).getUserObject(); + if (o instanceof SimulationModifier) { + return (SimulationModifier) o; + } + } + return null; + } + + /** + * Return the currently selected simulation. + * @return the selected simulation. + */ + @SuppressWarnings("unchecked") + private Simulation getSelectedSimulation() { + return ((Named) simulationSelectionCombo.getSelectedItem()).get(); + } + + + /** + * Return the currently selected simulation modifier from the table, + * or null if none selected. + * @return the selected modifier or null. + */ + private SimulationModifier getSelectedModifier() { + int row = selectedModifierTable.getSelectedRow(); + if (row < 0) { + return null; + } + row = selectedModifierTable.convertRowIndexToModel(row); + return selectedModifiers.get(row); + } + + + /** + * Return the currently selected optimization parameter. + * @return the selected optimization parameter. + */ + @SuppressWarnings("unchecked") + private OptimizableParameter getSelectedParameter() { + return ((Named) optimizationParameterCombo.getSelectedItem()).get(); + } + + + private Unit getModifierUnit(int index) { + return selectedModifiers.get(index).getUnitGroup().getDefaultUnit(); + } + + private String createSimulationName(String simulationName, String motorConfiguration) { + String name; + boolean hasParenthesis = motorConfiguration.matches("^[\\[\\(].*[\\]\\)]$"); + name = simulationName + " "; + if (!hasParenthesis) { + name += "("; + } + name += motorConfiguration; + if (!hasParenthesis) { + name += ")"; + } + return name; + } + + /** + * The table model for the parameter selection. + * + * [Body tube: Length] [min] [max] [unit] + */ + private class ParameterSelectionTableModel extends AbstractTableModel { + + private static final int PARAMETER = 0; + private static final int CURRENT = 1; + private static final int MIN = 2; + private static final int MAX = 3; + private static final int COUNT = 4; + + @Override + public int getColumnCount() { + return COUNT; + } + + @Override + public int getRowCount() { + return selectedModifiers.size(); + } + + @Override + public String getColumnName(int column) { + switch (column) { + case PARAMETER: + return trans.get("table.col.parameter"); + case CURRENT: + return trans.get("table.col.current"); + case MIN: + return trans.get("table.col.min"); + case MAX: + return trans.get("table.col.max"); + default: + throw new IndexOutOfBoundsException("column=" + column); + } + + } + + @Override + public Class getColumnClass(int column) { + switch (column) { + case PARAMETER: + return String.class; + case CURRENT: + return Double.class; + case MIN: + return Double.class; + case MAX: + return Double.class; + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + @Override + public Object getValueAt(int row, int column) { + + SimulationModifier modifier = selectedModifiers.get(row); + + switch (column) { + case PARAMETER: + return modifier.getRelatedObject().toString() + ": " + modifier.getName(); + case CURRENT: + try { + return getModifierUnit(row).toUnit(modifier.getCurrentSIValue(getSelectedSimulation())); + } catch (OptimizationException e) { + throw new BugException("Could not read current SI value from modifier " + modifier, e); + } + case MIN: + return getModifierUnit(row).toUnit(modifier.getMinValue()); + case MAX: + return getModifierUnit(row).toUnit(modifier.getMaxValue()); + default: + throw new IndexOutOfBoundsException("column=" + column); + } + + } + + @Override + public void setValueAt(Object value, int row, int column) { + + if (row >= selectedModifiers.size()) { + throw new BugException("setValueAt with invalid row: value=" + value + " row=" + row + " column=" + column + + " selectedModifiers.size=" + selectedModifiers.size() + " selectedModifiers=" + selectedModifiers + + " selectedModifierTable.getRowCount=" + selectedModifierTable.getRowCount()); + } + + switch (column) { + case PARAMETER: + break; + + case MIN: + double min = (Double) value; + min = getModifierUnit(row).fromUnit(min); + selectedModifiers.get(row).setMinValue(min); + break; + + case CURRENT: + break; + + case MAX: + double max = (Double) value; + max = getModifierUnit(row).fromUnit(max); + selectedModifiers.get(row).setMaxValue(max); + break; + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + this.fireTableRowsUpdated(row, row); + + } + + @Override + public boolean isCellEditable(int row, int column) { + switch (column) { + case PARAMETER: + return false; + case CURRENT: + return false; + case MIN: + return true; + case MAX: + return true; + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + } + + + private class DoubleCellRenderer extends DefaultTableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, + boolean hasFocus, int row, int column) { + + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + double val = (Double) value; + Unit unit = getModifierUnit(row); + + val = unit.fromUnit(val); + this.setText(unit.toStringUnit(val)); + + return this; + } + } + + + private static class SimulationModifierComparator implements Comparator { + + @Override + public int compare(SimulationModifier mod1, SimulationModifier mod2) { + Object rel1 = mod1.getRelatedObject(); + Object rel2 = mod2.getRelatedObject(); + + /* + * Primarily order by related object: + * + * - RocketComponents first + * - Two RocketComponents are ordered based on their position in the rocket + */ + if (!rel1.equals(rel2)) { + + if (rel1 instanceof RocketComponent) { + if (rel2 instanceof RocketComponent) { + + RocketComponent root = ((RocketComponent) rel1).getRoot(); + for (RocketComponent c : root) { + if (c.equals(rel1)) { + return -1; + } + if (c.equals(rel2)) { + return 1; + } + } + + throw new BugException("Error sorting modifiers, mod1=" + mod1 + " rel1=" + rel1 + + " mod2=" + mod2 + " rel2=" + rel2); + + } else { + return -1; + } + } else { + if (rel2 instanceof RocketComponent) { + return 1; + } + } + + } + + // Secondarily sort by name + return collator.compare(mod1.getName(), mod2.getName()); + } + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java b/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java new file mode 100644 index 00000000..1df3214e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java @@ -0,0 +1,435 @@ +package net.sf.openrocket.gui.dialogs.optimization; + +import java.awt.Color; +import java.awt.Paint; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.LinearInterpolator; +import net.sf.openrocket.util.MathUtil; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.annotations.XYBoxAnnotation; +import org.jfree.chart.annotations.XYLineAnnotation; +import org.jfree.chart.annotations.XYPointerAnnotation; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.labels.CustomXYToolTipGenerator; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.PaintScale; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.renderer.xy.XYShapeRenderer; +import org.jfree.chart.title.PaintScaleLegend; +import org.jfree.data.xy.DefaultXYZDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import org.jfree.ui.RectangleEdge; +import org.jfree.ui.TextAnchor; + +/** + * A class that plots the path of an optimization. + * + * @author Sampo Niskanen + */ +public class OptimizationPlotDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + private static final LinearInterpolator RED = new LinearInterpolator( + new double[] { 0.0, 1.0 }, new double[] { 0.0, 1.0 } + ); + private static final LinearInterpolator GREEN = new LinearInterpolator( + new double[] { 0.0, 1.0 }, new double[] { 0.0, 0.0 } + ); + private static final LinearInterpolator BLUE = new LinearInterpolator( + new double[] { 0.0, 1.0 }, new double[] { 1.0, 0.0 } + ); + + private static final Color OUT_OF_DOMAIN_COLOR = Color.BLACK; + + private static final Color PATH_COLOR = new Color(220, 0, 0); + + + public OptimizationPlotDialog(List path, Map evaluations, + List modifiers, OptimizableParameter parameter, UnitGroup stabilityUnit, Window parent) { + super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); + + + JPanel panel = new JPanel(new MigLayout("fill")); + + ChartPanel chart; + if (modifiers.size() == 1) { + chart = create1DPlot(path, evaluations, modifiers, parameter, stabilityUnit); + } else if (modifiers.size() == 2) { + chart = create2DPlot(path, evaluations, modifiers, parameter, stabilityUnit); + } else { + throw new IllegalArgumentException("Invalid dimensionality, dim=" + modifiers.size()); + } + chart.setBorder(BorderFactory.createLineBorder(Color.BLACK)); + panel.add(chart, "span, grow, wrap para"); + + + JLabel label = new StyledLabel(trans.get("lbl.zoomInstructions"), -2); + panel.add(label, ""); + + + JButton close = new JButton(trans.get("button.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + OptimizationPlotDialog.this.setVisible(false); + } + }); + panel.add(close, "right"); + + + this.add(panel); + + GUIUtil.setDisposableDialogOptions(this, close); + GUIUtil.rememberWindowSize(this); + } + + + + /** + * Create a 1D plot of the optimization path. + */ + private ChartPanel create1DPlot(List path, Map evaluations, + List modifiers, OptimizableParameter parameter, UnitGroup stabilityUnit) { + + SimulationModifier modX = modifiers.get(0); + Unit xUnit = modX.getUnitGroup().getDefaultUnit(); + Unit yUnit = parameter.getUnitGroup().getDefaultUnit(); + + // Create the optimization path (with autosort) + XYSeries series = new XYSeries(trans.get("plot1d.series"), true, true); + List tooltips = new ArrayList(); + for (Point p : evaluations.keySet()) { + FunctionEvaluationData data = evaluations.get(p); + if (data != null) { + if (data.getParameterValue() != null) { + Value[] state = data.getState(); + series.add(xUnit.toUnit(state[0].getValue()), yUnit.toUnit(data.getParameterValue().getValue())); + tooltips.add(getTooltip(data, parameter)); + } + } else { + log.error("Could not find evaluation data for point " + p); + } + } + + + String xLabel = modX.getRelatedObject().toString() + ": " + modX.getName() + " / " + xUnit.getUnit(); + String yLabel = parameter.getName() + " / " + yUnit.getUnit(); + + JFreeChart chart = ChartFactory.createXYLineChart( + trans.get("plot1d.title"), + xLabel, + yLabel, + null, + PlotOrientation.VERTICAL, + false, // Legend + true, // Tooltips + false); // Urls + + + // Set the scale of the plot to the limits + double x1 = xUnit.toUnit(modX.getMinValue()); + double x2 = xUnit.toUnit(modX.getMaxValue()); + + if (x1 < x2 - 0.0001) { + log.debug("Setting 1D plot domain axis x1=" + x1 + " x2=" + x2); + chart.getXYPlot().getDomainAxis().setRange(x1, x2); + } else { + log.warn("1D plot domain singular x1=" + x1 + " x2=" + x2 + ", not setting"); + } + + // Add lines to show optimization limits + XYLineAnnotation line = new XYLineAnnotation(x1, -1e19, x1, 1e19); + chart.getXYPlot().addAnnotation(line); + line = new XYLineAnnotation(x2, -1e19, x2, 1e19); + chart.getXYPlot().addAnnotation(line); + + // Mark the optimum point + Point optimum = path.get(path.size() - 1); + FunctionEvaluationData data = evaluations.get(optimum); + if (data != null) { + if (data.getParameterValue() != null) { + Value[] state = data.getState(); + double x = xUnit.toUnit(state[0].getValue()); + double y = yUnit.toUnit(data.getParameterValue().getValue()); + + XYPointerAnnotation text = new XYPointerAnnotation(trans.get("plot.label.optimum"), + x, y, Math.PI / 2); + text.setTextAnchor(TextAnchor.TOP_LEFT); + chart.getXYPlot().addAnnotation(text); + } + } else { + log.error("Could not find evaluation data for point " + optimum); + } + + + XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, true); + lineRenderer.setBaseShapesVisible(true); + lineRenderer.setSeriesShapesFilled(0, false); + //lineRenderer.setSeriesShape(0, shapeRenderer.getBaseShape()); + lineRenderer.setSeriesOutlinePaint(0, PATH_COLOR); + lineRenderer.setSeriesPaint(0, PATH_COLOR); + lineRenderer.setUseOutlinePaint(true); + CustomXYToolTipGenerator tooltipGenerator = new CustomXYToolTipGenerator(); + tooltipGenerator.addToolTipSeries(tooltips); + lineRenderer.setBaseToolTipGenerator(tooltipGenerator); + + XYPlot plot = chart.getXYPlot(); + + plot.setDataset(0, new XYSeriesCollection(series)); + plot.setRenderer(lineRenderer); + + + + return new ChartPanel(chart); + } + + /** + * Create a 2D plot of the optimization path. + */ + private ChartPanel create2DPlot(List path, Map evaluations, + List modifiers, OptimizableParameter parameter, UnitGroup stabilityUnit) { + + Unit parameterUnit = parameter.getUnitGroup().getDefaultUnit(); + + SimulationModifier modX = modifiers.get(0); + SimulationModifier modY = modifiers.get(1); + + Unit xUnit = modX.getUnitGroup().getDefaultUnit(); + Unit yUnit = modY.getUnitGroup().getDefaultUnit(); + + // Create the optimization path dataset + XYSeries pathSeries = new XYSeries(trans.get("plot2d.path"), false, true); + List pathTooltips = new ArrayList(); + for (Point p : path) { + FunctionEvaluationData data = evaluations.get(p); + if (data != null) { + Value[] state = data.getState(); + pathSeries.add(xUnit.toUnit(state[0].getValue()), yUnit.toUnit(state[1].getValue())); + pathTooltips.add(getTooltip(data, parameter)); + } else { + log.error("Could not find evaluation data for point " + p); + } + } + + + // Create evaluations dataset + double min = Double.POSITIVE_INFINITY; + double max = Double.NEGATIVE_INFINITY; + double[][] evals = new double[3][evaluations.size()]; + List evalTooltips = new ArrayList(); + + Iterator iterator = evaluations.values().iterator(); + for (int i = 0; i < evaluations.size(); i++) { + FunctionEvaluationData data = iterator.next(); + Value param = data.getParameterValue(); + double value; + if (param != null) { + value = parameterUnit.toUnit(data.getParameterValue().getValue()); + } else { + value = Double.NaN; + } + + Value[] state = data.getState(); + evals[0][i] = xUnit.toUnit(state[0].getValue()); + evals[1][i] = yUnit.toUnit(state[1].getValue()); + evals[2][i] = value; + + if (value < min) { + min = value; + } + if (value > max) { + max = value; + } + + evalTooltips.add(getTooltip(data, parameter)); + } + DefaultXYZDataset evalDataset = new DefaultXYZDataset(); + evalDataset.addSeries(trans.get("plot2d.evals"), evals); + + + + String xLabel = modX.getRelatedObject().toString() + ": " + modX.getName() + " / " + xUnit.getUnit(); + String yLabel = modY.getRelatedObject().toString() + ": " + modY.getName() + " / " + yUnit.getUnit(); + + JFreeChart chart = ChartFactory.createXYLineChart( + trans.get("plot2d.title"), + xLabel, + yLabel, + null, + //evalDataset, + PlotOrientation.VERTICAL, + true, // Legend + true, // Tooltips + false); // Urls + + + // Set the scale of the plot to the limits + double x1 = xUnit.toUnit(modX.getMinValue()); + double x2 = xUnit.toUnit(modX.getMaxValue()); + double y1 = yUnit.toUnit(modY.getMinValue()); + double y2 = yUnit.toUnit(modY.getMaxValue()); + + if (x1 < x2 - 0.0001) { + log.debug("Setting 2D plot domain axis to x1=" + x1 + " x2=" + x2); + chart.getXYPlot().getDomainAxis().setRange(x1, x2); + } else { + log.warn("2D plot has singular domain axis: x1=" + x1 + " x2=" + x2); + } + + if (y1 < y2 - 0.0001) { + log.debug("Setting 2D plot range axis to y1=" + y1 + " y2=" + y2); + chart.getXYPlot().getRangeAxis().setRange(y1, y2); + } else { + log.warn("2D plot has singular range axis: y1=" + y1 + " y2=" + y2); + } + + XYBoxAnnotation box = new XYBoxAnnotation(x1, y1, x2, y2); + chart.getXYPlot().addAnnotation(box); + + int n = pathSeries.getItemCount(); + XYPointerAnnotation text = new XYPointerAnnotation(trans.get("plot.label.optimum"), + (Double) pathSeries.getX(n - 1), (Double) pathSeries.getY(n - 1), -Math.PI / 5); + text.setTextAnchor(TextAnchor.BASELINE_LEFT); + chart.getXYPlot().addAnnotation(text); + + + if (min < max - 0.0001) { + log.debug("Setting gradient scale range to min=" + min + " max=" + max); + } else { + log.warn("2D plot has singular gradient scale, resetting to (0,1): min=" + min + " max=" + max); + min = 0; + max = 1; + } + + PaintScale paintScale = new GradientScale(min, max); + + XYShapeRenderer shapeRenderer = new XYShapeRenderer(); + shapeRenderer.setPaintScale(paintScale); + shapeRenderer.setUseFillPaint(true); + CustomXYToolTipGenerator tooltipGenerator = new CustomXYToolTipGenerator(); + tooltipGenerator.addToolTipSeries(evalTooltips); + shapeRenderer.setBaseToolTipGenerator(tooltipGenerator); + + + shapeRenderer.getLegendItem(0, 0); + + + XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, true); + lineRenderer.setBaseShapesVisible(true); + lineRenderer.setSeriesShapesFilled(0, false); + lineRenderer.setSeriesShape(0, shapeRenderer.getBaseShape()); + lineRenderer.setSeriesOutlinePaint(0, PATH_COLOR); + lineRenderer.setSeriesPaint(0, PATH_COLOR); + lineRenderer.setUseOutlinePaint(true); + tooltipGenerator = new CustomXYToolTipGenerator(); + tooltipGenerator.addToolTipSeries(pathTooltips); + lineRenderer.setBaseToolTipGenerator(tooltipGenerator); + + + XYPlot plot = chart.getXYPlot(); + + plot.setDataset(0, new XYSeriesCollection(pathSeries)); + plot.setRenderer(lineRenderer); + + plot.setDataset(1, evalDataset); + plot.setRenderer(1, shapeRenderer); + + + // Add value scale + NumberAxis numberAxis = new NumberAxis(parameter.getName() + " / " + parameterUnit.getUnit()); + PaintScaleLegend scale = new PaintScaleLegend(paintScale, numberAxis); + scale.setPosition(RectangleEdge.RIGHT); + scale.setMargin(4.0D, 4.0D, 40.0D, 4.0D); + scale.setAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); + chart.addSubtitle(scale); + + + return new ChartPanel(chart); + } + + + + private String getTooltip(FunctionEvaluationData data, OptimizableParameter parameter) { + String ttip = ""; + if (data.getParameterValue() != null) { + ttip += parameter.getName() + ": " + + parameter.getUnitGroup().getDefaultUnit().toStringUnit(data.getParameterValue().getValue()); + ttip += "
"; + } + if (data.getDomainReference() != null) { + ttip += trans.get("plot.ttip.stability") + " " + data.getDomainReference(); + } + return ttip; + } + + private class GradientScale implements PaintScale { + + private final double min; + private final double max; + + public GradientScale(double min, double max) { + this.min = min; + this.max = max; + } + + @Override + public Paint getPaint(double value) { + if (Double.isNaN(value)) { + return OUT_OF_DOMAIN_COLOR; + } + + value = MathUtil.map(value, min, max, 0.0, 1.0); + value = MathUtil.clamp(value, 0.0, 1.0); + + float r = (float) RED.getValue(value); + float g = (float) GREEN.getValue(value); + float b = (float) BLUE.getValue(value); + + return new Color(r, g, b); + } + + @Override + public double getLowerBound() { + return min; + } + + @Override + public double getUpperBound() { + return max; + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java b/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java new file mode 100644 index 00000000..ec1ece5a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java @@ -0,0 +1,52 @@ +package net.sf.openrocket.gui.dialogs.optimization; + +import net.sf.openrocket.optimization.general.Point; + +/** + * Value object for optimization step data. + * + * @author Sampo Niskanen + */ +public class OptimizationStepData { + + private final Point oldPoint; + private final double oldValue; + private final Point newPoint; + private final double newValue; + private final double stepSize; + + + public OptimizationStepData(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { + this.oldPoint = oldPoint; + this.oldValue = oldValue; + this.newPoint = newPoint; + this.newValue = newValue; + this.stepSize = stepSize; + } + + + public Point getOldPoint() { + return oldPoint; + } + + + public double getOldValue() { + return oldValue; + } + + + public Point getNewPoint() { + return newPoint; + } + + + public double getNewValue() { + return newValue; + } + + + public double getStepSize() { + return stepSize; + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java b/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java new file mode 100644 index 00000000..eda1f9ae --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java @@ -0,0 +1,234 @@ +package net.sf.openrocket.gui.dialogs.optimization; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.LinkedBlockingQueue; + +import javax.swing.SwingUtilities; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.FunctionOptimizer; +import net.sf.openrocket.optimization.general.OptimizationController; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.ParallelExecutorCache; +import net.sf.openrocket.optimization.general.ParallelFunctionCache; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer; +import net.sf.openrocket.optimization.general.onedim.GoldenSectionSearchOptimizer; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; +import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationFunction; +import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationListener; +import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.BugException; + +/** + * A background worker that runs the optimization in the background. It supports providing + * evaluation and step counter information via listeners that are executed on the EDT. + * + * @author Sampo Niskanen + */ +public abstract class OptimizationWorker extends Thread implements OptimizationController, RocketOptimizationListener { + + /* + * Note: This is implemented as a separate Thread object instead of a SwingWorker because + * the SwingWorker cannot be interrupted in any way except by canceling the task, which + * makes it impossible to wait for its exiting (SwingWorker.get() throws a CancellationException + * if cancel() has been called). + * + * SwingWorker also seems to miss some chunks that have been provided to process() when the + * thread ends. + * + * Nothing of this is documented, of course... + */ + + private static final LogHelper log = Application.getLogger(); + + /** Notify listeners every this many milliseconds */ + private static final long PURGE_TIMEOUT = 500; + /** End optimization when step size is below this threshold */ + private static final double STEP_SIZE_LIMIT = 0.005; + + private final FunctionOptimizer optimizer; + private final RocketOptimizationFunction function; + + private final Simulation simulation; + private final SimulationModifier[] modifiers; + + private final ParallelFunctionCache cache; + + + private final LinkedBlockingQueue evaluationQueue = + new LinkedBlockingQueue(); + private final LinkedBlockingQueue stepQueue = + new LinkedBlockingQueue(); + private volatile long lastPurge = 0; + + private OptimizationException optimizationException = null; + + + /** + * Sole constructor + * @param simulation the simulation + * @param parameter the optimization parameter + * @param goal the optimization goal + * @param domain the optimization domain + * @param modifiers the simulation modifiers + */ + public OptimizationWorker(Simulation simulation, OptimizableParameter parameter, + OptimizationGoal goal, SimulationDomain domain, SimulationModifier... modifiers) { + + this.simulation = simulation; + this.modifiers = modifiers.clone(); + + function = new RocketOptimizationFunction(simulation, parameter, goal, domain, modifiers); + function.addRocketOptimizationListener(this); + + cache = new ParallelExecutorCache(1); + cache.setFunction(function); + + if (modifiers.length == 1) { + optimizer = new GoldenSectionSearchOptimizer(cache); + } else { + optimizer = new MultidirectionalSearchOptimizer(cache); + } + } + + + @Override + public void run() { + try { + + double[] current = new double[modifiers.length]; + for (int i = 0; i < modifiers.length; i++) { + current[i] = modifiers[i].getCurrentScaledValue(simulation); + } + Point initial = new Point(current); + + optimizer.optimize(initial, this); + + } catch (OptimizationException e) { + this.optimizationException = e; + } finally { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + lastPurge = System.currentTimeMillis() + 24L * 3600L * 1000L; + processQueue(); + done(optimizationException); + } + }); + } + } + + /** + * This method is called after the optimization has ended, either normally, when interrupted + * or by throwing an exception. This method is called on the EDT, like the done() method of SwingWorker. + *

+ * All data chunks to the listeners will be guaranteed to have been processed before calling done(). + * + * @param exception a possible optimization exception that occurred, or null for normal exit. + */ + protected abstract void done(OptimizationException exception); + + + /** + * This method is called for each function evaluation that has taken place. + * This method is called on the EDT. + * + * @param data the data accumulated since the last call + */ + protected abstract void functionEvaluated(List data); + + /** + * This method is called after each step taken by the optimization algorithm. + * This method is called on the EDT. + * + * @param data the data accumulated since the last call + */ + protected abstract void optimizationStepTaken(List data); + + + /** + * Publishes data to the listeners. The queue is purged every PURGE_TIMEOUT milliseconds. + * + * @param data the data to publish to the listeners + */ + private synchronized void publish(FunctionEvaluationData evaluation, OptimizationStepData step) { + + if (evaluation != null) { + evaluationQueue.add(evaluation); + } + if (step != null) { + stepQueue.add(step); + } + + // Add a method to the EDT to process the queue data + long now = System.currentTimeMillis(); + if (lastPurge + PURGE_TIMEOUT <= now) { + lastPurge = now; + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + processQueue(); + } + }); + } + + } + + + /** + * Process the queue and call the listeners. This method must always be called from the EDT. + */ + private void processQueue() { + + if (!SwingUtilities.isEventDispatchThread()) { + throw new BugException("processQueue called from non-EDT"); + } + + + List evaluations = new ArrayList(); + evaluationQueue.drainTo(evaluations); + if (!evaluations.isEmpty()) { + functionEvaluated(evaluations); + } + + + List steps = new ArrayList(); + stepQueue.drainTo(steps); + if (!steps.isEmpty()) { + optimizationStepTaken(steps); + } + } + + + + + /* + * NOTE: The stepTaken and evaluated methods may be called from other + * threads than the EDT or the SwingWorker thread! + */ + + @Override + public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { + publish(null, new OptimizationStepData(oldPoint, oldValue, newPoint, newValue, stepSize)); + + if (stepSize < STEP_SIZE_LIMIT) { + log.info("stepSize=" + stepSize + " is below limit, ending optimization"); + return false; + } else { + return true; + } + } + + @Override + public void evaluated(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue) { + publish(new FunctionEvaluationData(point, state, domainReference, parameterValue, goalValue), null); + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java b/core/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java new file mode 100644 index 00000000..1e7e47f0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java @@ -0,0 +1,193 @@ +package net.sf.openrocket.gui.dialogs.optimization; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Font; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; + +import javax.swing.JTree; +import javax.swing.ToolTipManager; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; + +import net.sf.openrocket.gui.components.BasicTree; +import net.sf.openrocket.gui.main.ComponentIcons; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.TextUtil; + +/** + * A tree that displays the simulation modifiers in a tree structure. + *

+ * All nodes in the model are instances of DefaultMutableTreeNode. The user objects + * within are either of type RocketComponent, String or SimulationModifier. + * + * @author Sampo Niskanen + */ +public class SimulationModifierTree extends BasicTree { + + private final List selectedModifiers; + + /** + * Sole constructor. + * + * @param rocket the rocket. + * @param simulationModifiers the simulation modifiers, ordered and mapped by components + * @param selectedModifiers a list of the currently selected modifiers (may be modified). + */ + public SimulationModifierTree(Rocket rocket, Map> simulationModifiers, + List selectedModifiers) { + this.selectedModifiers = selectedModifiers; + + populateTree(rocket, simulationModifiers); + this.setCellRenderer(new ComponentModifierTreeRenderer()); + + // Enable tooltips for this component + ToolTipManager.sharedInstance().registerComponent(this); + + expandComponents(); + } + + + /** + * Populate the simulation modifier tree from the provided information. This can be used to update + * the tree. + */ + public void populateTree(Rocket rocket, Map> simulationModifiers) { + + DefaultMutableTreeNode baseNode = new DefaultMutableTreeNode(rocket); + populateTree(baseNode, rocket, simulationModifiers); + + this.setModel(new DefaultTreeModel(baseNode)); + } + + + private static void populateTree(DefaultMutableTreeNode node, RocketComponent component, + Map> simulationModifiers) { + + // Add modifiers (if any) + List modifiers = simulationModifiers.get(component); + if (modifiers != null) { + DefaultMutableTreeNode modifierNode; + + if (component.getChildCount() > 0) { + modifierNode = new DefaultMutableTreeNode("Optimization parameters"); + node.add(modifierNode); + } else { + modifierNode = node; + } + + for (SimulationModifier m : modifiers) { + modifierNode.add(new DefaultMutableTreeNode(m)); + } + } + + // Add child components + for (RocketComponent c : component.getChildren()) { + DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(c); + node.add(newNode); + populateTree(newNode, c, simulationModifiers); + } + + } + + + /** + * Expand the rocket components, but not the modifiers. + */ + @SuppressWarnings("rawtypes") + public void expandComponents() { + DefaultMutableTreeNode baseNode = (DefaultMutableTreeNode) this.getModel().getRoot(); + + Enumeration enumeration = baseNode.breadthFirstEnumeration(); + + while (enumeration.hasMoreElements()) { + DefaultMutableTreeNode node = (DefaultMutableTreeNode) enumeration.nextElement(); + Object object = node.getUserObject(); + if (object instanceof RocketComponent) { + this.makeVisible(new TreePath(node.getPath())); + } + } + } + + + + + public class ComponentModifierTreeRenderer extends DefaultTreeCellRenderer { + private Font componentFont; + private Font stringFont; + private Font modifierFont; + + @Override + public Component getTreeCellRendererComponent( + JTree tree, + Object value, + boolean sel, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus2) { + + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus2); + + if (componentFont == null) { + makeFonts(); + } + + + // Customize based on line type + + Object object = ((DefaultMutableTreeNode) value).getUserObject(); + + // Set icon (for rocket components, null for others) + setIcon(ComponentIcons.getSmallIcon(object.getClass())); + + + // Set text color/style + if (object instanceof RocketComponent) { + setForeground(Color.GRAY); + setFont(componentFont); + + // Set tooltip + RocketComponent c = (RocketComponent) object; + String comment = c.getComment().trim(); + if (comment.length() > 0) { + comment = TextUtil.htmlEncode(comment); + comment = "" + comment.replace("\n", "
"); + this.setToolTipText(comment); + } else { + this.setToolTipText(null); + } + } else if (object instanceof String) { + setForeground(Color.GRAY); + setFont(stringFont); + } else if (object instanceof SimulationModifier) { + + if (selectedModifiers.contains(object)) { + setForeground(Color.GRAY); + } else { + setForeground(Color.BLACK); + } + setFont(modifierFont); + setText(((SimulationModifier) object).getName()); + setToolTipText(((SimulationModifier) object).getDescription()); + } + + return this; + } + + private void makeFonts() { + Font font = getFont(); + componentFont = font.deriveFont(Font.ITALIC); + stringFont = font; + modifierFont = font.deriveFont(Font.BOLD); + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java b/core/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java new file mode 100644 index 00000000..a106ba32 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java @@ -0,0 +1,392 @@ +package net.sf.openrocket.gui.dialogs.preferences; + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Iterator; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.DefaultTableCellRenderer; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.database.Database; +import net.sf.openrocket.database.Databases; +import net.sf.openrocket.gui.adaptors.Column; +import net.sf.openrocket.gui.adaptors.ColumnTableModel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.dialogs.CustomMaterialDialog; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.unit.Value; + +public class MaterialEditPanel extends JPanel { + + private final JTable table; + + private final JButton addButton; + private final JButton editButton; + private final JButton deleteButton; + private final JButton revertButton; + private static final Translator trans = Application.getTranslator(); + + + public MaterialEditPanel() { + super(new MigLayout("fill")); + + + // TODO: LOW: Create sorter that keeps material types always in order + final ColumnTableModel model = new ColumnTableModel( + //// Material + new Column(trans.get("matedtpan.col.Material")) { + @Override + public Object getValueAt(int row) { + return getMaterial(row).getName(); + } + }, + //// Type + new Column(trans.get("matedtpan.col.Type")) { + @Override + public Object getValueAt(int row) { + return getMaterial(row).getType().toString(); + } + + @Override + public int getDefaultWidth() { + return 15; + } + }, + //// Density + new Column(trans.get("matedtpan.col.Density")) { + @Override + public Object getValueAt(int row) { + Material m = getMaterial(row); + double d = m.getDensity(); + switch (m.getType()) { + case LINE: + return UnitGroup.UNITS_DENSITY_LINE.toValue(d); + + case SURFACE: + return UnitGroup.UNITS_DENSITY_SURFACE.toValue(d); + + case BULK: + return UnitGroup.UNITS_DENSITY_BULK.toValue(d); + + default: + throw new IllegalStateException("Material type " + m.getType()); + } + } + + @Override + public int getDefaultWidth() { + return 15; + } + + @Override + public Class getColumnClass() { + return Value.class; + } + } + ) { + @Override + public int getRowCount() { + return Databases.BULK_MATERIAL.size() + Databases.SURFACE_MATERIAL.size() + + Databases.LINE_MATERIAL.size(); + } + }; + + table = new JTable(model); + model.setColumnWidths(table.getColumnModel()); + table.setAutoCreateRowSorter(true); + table.setDefaultRenderer(Object.class, new MaterialCellRenderer()); + this.add(new JScrollPane(table), "w 200px, h 100px, grow 100"); + + + //// New button + addButton = new JButton(trans.get("matedtpan.but.new")); + //// Add a new material + addButton.setToolTipText(trans.get("matedtpan.col.but.ttip.New")); + addButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + CustomMaterialDialog dialog = new CustomMaterialDialog( + SwingUtilities.getWindowAncestor(MaterialEditPanel.this), + //// Add a custom material + null, false, trans.get("matedtpan.title.Addcustmaterial")); + dialog.setVisible(true); + if (dialog.getOkClicked()) { + Material mat = dialog.getMaterial(); + getDatabase(mat).add(mat); + model.fireTableDataChanged(); + setButtonStates(); + } + } + }); + this.add(addButton, "gap rel rel para para, w 70lp, split 5, flowy, growx 1, top"); + + //// Edit button + editButton = new JButton(trans.get("matedtpan.but.edit")); + //// Edit an existing material + editButton.setToolTipText(trans.get("matedtpan.but.ttip.edit")); + editButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int sel = table.getSelectedRow(); + if (sel < 0) + return; + sel = table.convertRowIndexToModel(sel); + Material m = getMaterial(sel); + + CustomMaterialDialog dialog; + if (m.isUserDefined()) { + dialog = new CustomMaterialDialog( + SwingUtilities.getWindowAncestor(MaterialEditPanel.this), + //// Edit material + m, false, trans.get("matedtpan.title.Editmaterial")); + } else { + dialog = new CustomMaterialDialog( + SwingUtilities.getWindowAncestor(MaterialEditPanel.this), + //// Add a custom material + m, false, trans.get("matedtpan.title.Addcustmaterial"), + //// The built-in materials cannot be modified. + trans.get("matedtpan.title2.Editmaterial")); + } + + dialog.setVisible(true); + + if (dialog.getOkClicked()) { + if (m.isUserDefined()) { + getDatabase(m).remove(m); + } + Material mat = dialog.getMaterial(); + getDatabase(mat).add(mat); + model.fireTableDataChanged(); + setButtonStates(); + } + } + }); + this.add(editButton, "gap rel rel para para, growx 1, top"); + + //// Delete button + deleteButton = new JButton(trans.get("matedtpan.but.delete")); + //// Delete a user-defined material + deleteButton.setToolTipText(trans.get("matedtpan.but.ttip.delete")); + deleteButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int sel = table.getSelectedRow(); + if (sel < 0) + return; + sel = table.convertRowIndexToModel(sel); + Material m = getMaterial(sel); + if (!m.isUserDefined()) + return; + getDatabase(m).remove(m); + model.fireTableDataChanged(); + setButtonStates(); + } + }); + this.add(deleteButton, "gap rel rel para para, growx 1, top"); + + + this.add(new JPanel(), "grow 1"); + + //// Revert all button + revertButton = new JButton(trans.get("matedtpan.but.revertall")); + //// Delete all user-defined materials + revertButton.setToolTipText(trans.get("matedtpan.but.ttip.revertall")); + revertButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int sel = JOptionPane.showConfirmDialog(MaterialEditPanel.this, + //// Delete all user-defined materials? + trans.get("matedtpan.title.Deletealluser-defined"), + //// Revert all? + trans.get("matedtpan.title.Revertall"), + JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); + if (sel == JOptionPane.YES_OPTION) { + Iterator iterator; + + iterator = Databases.LINE_MATERIAL.iterator(); + while (iterator.hasNext()) { + if (iterator.next().isUserDefined()) + iterator.remove(); + } + + iterator = Databases.SURFACE_MATERIAL.iterator(); + while (iterator.hasNext()) { + if (iterator.next().isUserDefined()) + iterator.remove(); + } + + iterator = Databases.BULK_MATERIAL.iterator(); + while (iterator.hasNext()) { + if (iterator.next().isUserDefined()) + iterator.remove(); + } + model.fireTableDataChanged(); + setButtonStates(); + } + } + }); + this.add(revertButton, "gap rel rel para para, growx 1, bottom, wrap unrel"); + + setButtonStates(); + table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + setButtonStates(); + } + }); + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + editButton.doClick(); + } + } + }); + + //// Editing materials will not affect existing + //// rocket designs. + this.add(new StyledLabel(trans.get("matedtpan.lbl.edtmaterials"), -2, Style.ITALIC), "span"); + + + } + + + private Database getDatabase(Material m) { + switch (m.getType()) { + case BULK: + return Databases.BULK_MATERIAL; + + case SURFACE: + return Databases.SURFACE_MATERIAL; + + case LINE: + return Databases.LINE_MATERIAL; + + default: + throw new IllegalArgumentException("Material type invalid, m=" + m); + } + } + + + private void setButtonStates() { + int sel = table.getSelectedRow(); + + // Add button always enabled + addButton.setEnabled(true); + + // Edit button enabled if a material is selected + editButton.setEnabled(sel >= 0); + + // Delete button enabled if a user-defined material is selected + if (sel >= 0) { + int modelRow = table.convertRowIndexToModel(sel); + deleteButton.setEnabled(getMaterial(modelRow).isUserDefined()); + } else { + deleteButton.setEnabled(false); + } + + // Revert button enabled if any user-defined material exists + boolean found = false; + + for (Material m : Databases.BULK_MATERIAL) { + if (m.isUserDefined()) { + found = true; + break; + } + } + if (!found) { + for (Material m : Databases.SURFACE_MATERIAL) { + if (m.isUserDefined()) { + found = true; + break; + } + } + } + if (!found) { + for (Material m : Databases.LINE_MATERIAL) { + if (m.isUserDefined()) { + found = true; + break; + } + } + } + revertButton.setEnabled(found); + + } + + private Material getMaterial(int origRow) { + int row = origRow; + int n; + + n = Databases.BULK_MATERIAL.size(); + if (row < n) { + return Databases.BULK_MATERIAL.get(row); + } + row -= n; + + n = Databases.SURFACE_MATERIAL.size(); + if (row < n) { + return Databases.SURFACE_MATERIAL.get(row); + } + row -= n; + + n = Databases.LINE_MATERIAL.size(); + if (row < n) { + return Databases.LINE_MATERIAL.get(row); + } + throw new IndexOutOfBoundsException("row=" + origRow + " while material count" + + " bulk:" + Databases.BULK_MATERIAL.size() + + " surface:" + Databases.SURFACE_MATERIAL.size() + + " line:" + Databases.LINE_MATERIAL.size()); + } + + + private class MaterialCellRenderer extends DefaultTableCellRenderer { + + /* (non-Javadoc) + * @see javax.swing.table.DefaultTableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int) + */ + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + Component c = super.getTableCellRendererComponent(table, value, isSelected, + hasFocus, row, column); + if (c instanceof JLabel) { + JLabel label = (JLabel) c; + Material m = getMaterial(row); + + if (isSelected) { + if (m.isUserDefined()) + label.setForeground(table.getSelectionForeground()); + else + label.setForeground(Color.GRAY); + } else { + if (m.isUserDefined()) + label.setForeground(table.getForeground()); + else + label.setForeground(Color.GRAY); + } + } + return c; + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java b/core/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java new file mode 100644 index 00000000..986c4a40 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java @@ -0,0 +1,693 @@ +package net.sf.openrocket.gui.dialogs.preferences; + +import java.awt.Dialog; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import javax.swing.AbstractListModel; +import javax.swing.ComboBoxModel; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.Timer; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.communication.UpdateInfo; +import net.sf.openrocket.communication.UpdateInfoRetriever; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.dialogs.UpdateInfoDialog; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.gui.util.SimpleFileFilter; +import net.sf.openrocket.l10n.L10N; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.Named; +import net.sf.openrocket.util.Utils; + + +public class PreferencesDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + + private final List unitSelectors = new ArrayList(); + + private File defaultDirectory = null; + private static final Translator trans = Application.getTranslator(); + + private PreferencesDialog(Window parent) { + //// Preferences + super(parent, trans.get("pref.dlg.title.Preferences"), Dialog.ModalityType.APPLICATION_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill, gap unrel", "[grow]", "[grow][]")); + + JTabbedPane tabbedPane = new JTabbedPane(); + panel.add(tabbedPane, "grow, wrap"); + + //// Units and Default units + tabbedPane.addTab(trans.get("pref.dlg.tab.Units"), null, unitsPane(), + trans.get("pref.dlg.tab.Defaultunits")); + //// Materials and Custom materials + tabbedPane.addTab(trans.get("pref.dlg.tab.Materials"), null, new MaterialEditPanel(), + trans.get("pref.dlg.tab.Custommaterials")); + //// Options and Miscellaneous options + tabbedPane.addTab(trans.get("pref.dlg.tab.Options"), null, optionsPane(), + trans.get("pref.dlg.tab.Miscellaneousoptions")); + + //// Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent arg0) { + PreferencesDialog.this.setVisible(false); + PreferencesDialog.this.dispose(); + } + }); + panel.add(close, "span, right, tag close"); + + this.setContentPane(panel); + pack(); + this.setLocationRelativeTo(null); + + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + ((SwingPreferences) Application.getPreferences()).storeDefaultUnits(); + } + }); + + GUIUtil.setDisposableDialogOptions(this, close); + } + + + private JPanel optionsPane() { + JPanel panel = new JPanel(new MigLayout("fillx, ins 30lp n n n")); + + + //// Language selector + Locale userLocale = null; + { + String locale = Application.getPreferences().getString("locale", null); + userLocale = L10N.toLocale(locale); + } + List> locales = new ArrayList>(); + for (Locale l : SwingPreferences.getSupportedLocales()) { + locales.add(new Named(l, l.getDisplayLanguage())); + } + Collections.sort(locales); + locales.add(0, new Named(null, trans.get("languages.default"))); + + final JComboBox languageCombo = new JComboBox(locales.toArray()); + for (int i = 0; i < locales.size(); i++) { + if (Utils.equals(userLocale, locales.get(i).get())) { + languageCombo.setSelectedIndex(i); + } + } + languageCombo.addActionListener(new ActionListener() { + @Override + @SuppressWarnings("unchecked") + public void actionPerformed(ActionEvent e) { + Named selection = (Named) languageCombo.getSelectedItem(); + Locale l = selection.get(); + Application.getPreferences().putString(Preferences.USER_LOCAL, l == null ? null : l.toString()); + } + }); + panel.add(new JLabel(trans.get("lbl.language")), "gapright para"); + panel.add(languageCombo, "wrap rel, growx, sg combos"); + + panel.add(new StyledLabel(trans.get("PreferencesDialog.lbl.languageEffect"), -3, Style.ITALIC), "span, wrap para*2"); + + + //// Position to insert new body components: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Positiontoinsert")), "gapright para"); + panel.add(new JComboBox(new PrefChoiseSelector(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, + //// Always ask + //// Insert in middle + //// Add to end + trans.get("pref.dlg.PrefChoiseSelector1"), + trans.get("pref.dlg.PrefChoiseSelector2"), + trans.get("pref.dlg.PrefChoiseSelector3"))), "wrap para, growx, sg combos"); + + //// Confirm deletion of simulations: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Confirmdeletion"))); + panel.add(new JComboBox(new PrefBooleanSelector(Preferences.CONFIRM_DELETE_SIMULATION, + //// Delete + //// Confirm + trans.get("pref.dlg.PrefBooleanSelector1"), + trans.get("pref.dlg.PrefBooleanSelector2"), true)), "wrap 40lp, growx, sg combos"); + + //// User-defined thrust curves: + panel.add(new JLabel(trans.get("pref.dlg.lbl.User-definedthrust")), "spanx, wrap"); + final JTextField field = new JTextField(); + List files = ((SwingPreferences) Application.getPreferences()).getUserThrustCurveFiles(); + String str = ""; + for (File file : files) { + if (str.length() > 0) { + str += ";"; + } + str += file.getAbsolutePath(); + } + field.setText(str); + field.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void removeUpdate(DocumentEvent e) { + changed(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + changed(); + } + + @Override + public void changedUpdate(DocumentEvent e) { + changed(); + } + + private void changed() { + String text = field.getText(); + List list = new ArrayList(); + for (String s : text.split(";")) { + s = s.trim(); + if (s.length() > 0) { + list.add(new File(s)); + } + } + ((SwingPreferences) Application.getPreferences()).setUserThrustCurveFiles(list); + } + }); + panel.add(field, "w 100px, gapright unrel, spanx, growx, split"); + + //// Add button + JButton button = new JButton(trans.get("pref.dlg.but.add")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + JFileChooser chooser = new JFileChooser(); + SimpleFileFilter filter = + new SimpleFileFilter( + //// All thrust curve files (*.eng; *.rse; *.zip; directories) + trans.get("pref.dlg.Allthrustcurvefiles"), + true, "eng", "rse", "zip"); + chooser.addChoosableFileFilter(filter); + //// RASP motor files (*.eng) + chooser.addChoosableFileFilter(new SimpleFileFilter(trans.get("pref.dlg.RASPfiles"), + true, "eng")); + //// RockSim engine files (*.rse) + chooser.addChoosableFileFilter(new SimpleFileFilter(trans.get("pref.dlg.RockSimfiles"), + true, "rse")); + //// ZIP archives (*.zip) + chooser.addChoosableFileFilter(new SimpleFileFilter(trans.get("pref.dlg.ZIParchives"), + true, "zip")); + chooser.setFileFilter(filter); + chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); + if (defaultDirectory != null) { + chooser.setCurrentDirectory(defaultDirectory); + } + + //// Add + int returnVal = chooser.showDialog(PreferencesDialog.this, trans.get("pref.dlg.Add")); + if (returnVal == JFileChooser.APPROVE_OPTION) { + log.user("Adding user thrust curve: " + chooser.getSelectedFile()); + defaultDirectory = chooser.getCurrentDirectory(); + String text = field.getText().trim(); + if (text.length() > 0) { + text += ";"; + } + text += chooser.getSelectedFile().getAbsolutePath(); + field.setText(text); + } + } + }); + panel.add(button, "gapright unrel"); + + //// Reset button + button = new JButton(trans.get("pref.dlg.but.reset")); + + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + // First one sets to the default, but does not un-set the pref + field.setText(((SwingPreferences)Application.getPreferences()).getDefaultUserThrustCurveFile().getAbsolutePath()); + ((SwingPreferences) Application.getPreferences()).setUserThrustCurveFiles(null); + } + }); + panel.add(button, "wrap"); + + //// Add directories, RASP motor files (*.eng), RockSim engine files (*.rse) or ZIP archives separated by a semicolon (;) to load external thrust curves. Changes will take effect the next time you start OpenRocket. + DescriptionArea desc = new DescriptionArea(trans.get("pref.dlg.DescriptionArea.Adddirectories"), 3, -3, false); + desc.setBackground(getBackground()); + panel.add(desc, "spanx, growx, wrap 40lp"); + + + + //// Check for software updates at startup + final JCheckBox softwareUpdateBox = + new JCheckBox(trans.get("pref.dlg.checkbox.Checkupdates")); + softwareUpdateBox.setSelected( Application.getPreferences().getCheckUpdates()); + softwareUpdateBox.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Application.getPreferences().setCheckUpdates(softwareUpdateBox.isSelected()); + } + }); + panel.add(softwareUpdateBox); + + //// Check now button + button = new JButton(trans.get("pref.dlg.but.checknow")); + //// Check for software updates now + button.setToolTipText(trans.get("pref.dlg.ttip.Checkupdatesnow")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + checkForUpdates(); + } + }); + panel.add(button, "right, wrap"); + + + return panel; + } + + private JPanel unitsPane() { + JPanel panel = new JPanel(new MigLayout("", "[][]40lp[][]")); + JComboBox combo; + + //// Select your preferred units: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Selectprefunits")), "span, wrap paragraph"); + + + //// Rocket dimensions: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Rocketdimensions"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH)); + panel.add(combo, "sizegroup boxes"); + + //// Line density: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Linedensity"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_LINE)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Motor dimensions: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Motordimensions"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MOTOR_DIMENSIONS)); + panel.add(combo, "sizegroup boxes"); + + //// Surface density: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Surfacedensity"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_SURFACE)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Distance: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Distance"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE)); + panel.add(combo, "sizegroup boxes"); + + //// Bulk density:: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Bulkdensity"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_BULK)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Velocity: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Velocity"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_VELOCITY)); + panel.add(combo, "sizegroup boxes"); + + //// Surface roughness: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Surfaceroughness"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROUGHNESS)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Acceleration: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Acceleration"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ACCELERATION)); + panel.add(combo, "sizegroup boxes"); + + //// Area: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Area"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_AREA)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Mass: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Mass"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MASS)); + panel.add(combo, "sizegroup boxes"); + + //// Angle: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Angle"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ANGLE)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Force: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Force"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE)); + panel.add(combo, "sizegroup boxes"); + + //// Roll rate: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Rollrate"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROLL)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Total impulse: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Totalimpulse"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_IMPULSE)); + panel.add(combo, "sizegroup boxes"); + + //// Temperature: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Temperature"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_TEMPERATURE)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Moment of inertia: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Momentofinertia"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_INERTIA)); + panel.add(combo, "sizegroup boxes"); + + //// Pressure: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Pressure"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_PRESSURE)); + panel.add(combo, "sizegroup boxes, wrap"); + + + //// Stability: + panel.add(new JLabel(trans.get("pref.dlg.lbl.Stability"))); + combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_STABILITY)); + panel.add(combo, "sizegroup boxes, wrap para"); + + + + //// Default metric button + JButton button = new JButton(trans.get("pref.dlg.but.defaultmetric")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UnitGroup.setDefaultMetricUnits(); + for (DefaultUnitSelector s : unitSelectors) + s.fireChange(); + } + }); + panel.add(button, "spanx, split 2, grow"); + + //// Default imperial button + button = new JButton(trans.get("pref.dlg.but.defaultimperial")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UnitGroup.setDefaultImperialUnits(); + for (DefaultUnitSelector s : unitSelectors) + s.fireChange(); + } + }); + panel.add(button, "grow, wrap para"); + + //// The effects will take place the next time you open a window. + panel.add(new StyledLabel( + trans.get("pref.dlg.lbl.effect1"), -2, Style.ITALIC), + "spanx, wrap"); + + + return panel; + } + + + + + + private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel { + + private final UnitGroup group; + + public DefaultUnitSelector(UnitGroup group) { + this.group = group; + unitSelectors.add(this); + } + + @Override + public Object getSelectedItem() { + return group.getDefaultUnit(); + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + // Clear selection - huh? + return; + } + if (!(item instanceof Unit)) { + throw new IllegalArgumentException("Illegal argument " + item); + } + group.setDefaultUnit(group.getUnitIndex((Unit) item)); + } + + @Override + public Object getElementAt(int index) { + return group.getUnit(index); + } + + @Override + public int getSize() { + return group.getUnitCount(); + } + + + public void fireChange() { + this.fireContentsChanged(this, 0, this.getSize()); + } + } + + + + private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel { + private final String preference; + private final String[] descriptions; + + public PrefChoiseSelector(String preference, String... descriptions) { + this.preference = preference; + this.descriptions = descriptions; + } + + @Override + public Object getSelectedItem() { + return descriptions[Application.getPreferences().getChoice(preference, descriptions.length, 0)]; + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + // Clear selection - huh? + return; + } + if (!(item instanceof String)) { + throw new IllegalArgumentException("Illegal argument " + item); + } + int index; + for (index = 0; index < descriptions.length; index++) { + if (((String) item).equalsIgnoreCase(descriptions[index])) + break; + } + if (index >= descriptions.length) { + throw new IllegalArgumentException("Illegal argument " + item); + } + + Application.getPreferences().putChoice(preference, index); + } + + @Override + public Object getElementAt(int index) { + return descriptions[index]; + } + + @Override + public int getSize() { + return descriptions.length; + } + } + + + private class PrefBooleanSelector extends AbstractListModel implements ComboBoxModel { + private final String preference; + private final String trueDesc, falseDesc; + private final boolean def; + + public PrefBooleanSelector(String preference, String falseDescription, + String trueDescription, boolean defaultState) { + this.preference = preference; + this.trueDesc = trueDescription; + this.falseDesc = falseDescription; + this.def = defaultState; + } + + @Override + public Object getSelectedItem() { + if (Application.getPreferences().getBoolean(preference, def)) { + return trueDesc; + } else { + return falseDesc; + } + } + + @Override + public void setSelectedItem(Object item) { + if (item == null) { + // Clear selection - huh? + return; + } + if (!(item instanceof String)) { + throw new IllegalArgumentException("Illegal argument " + item); + } + + if (trueDesc.equals(item)) { + Application.getPreferences().putBoolean(preference, true); + } else if (falseDesc.equals(item)) { + Application.getPreferences().putBoolean(preference, false); + } else { + throw new IllegalArgumentException("Illegal argument " + item); + } + } + + @Override + public Object getElementAt(int index) { + switch (index) { + case 0: + return def ? trueDesc : falseDesc; + + case 1: + return def ? falseDesc : trueDesc; + + default: + throw new IndexOutOfBoundsException("Boolean asked for index=" + index); + } + } + + @Override + public int getSize() { + return 2; + } + } + + + private void checkForUpdates() { + final UpdateInfoRetriever retriever = new UpdateInfoRetriever(); + retriever.start(); + + + // Progress dialog + final JDialog dialog = new JDialog(this, ModalityType.APPLICATION_MODAL); + JPanel panel = new JPanel(new MigLayout()); + + //// Checking for updates... + panel.add(new JLabel(trans.get("pref.dlg.lbl.Checkingupdates")), "wrap"); + + JProgressBar bar = new JProgressBar(); + bar.setIndeterminate(true); + panel.add(bar, "growx, wrap para"); + + //// Cancel button + JButton cancel = new JButton(trans.get("dlg.but.cancel")); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dialog.dispose(); + } + }); + panel.add(cancel, "right"); + dialog.add(panel); + + GUIUtil.setDisposableDialogOptions(dialog, cancel); + + + // Timer to monitor progress + final Timer timer = new Timer(100, null); + final long startTime = System.currentTimeMillis(); + + ActionListener listener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!retriever.isRunning() || startTime + 10000 < System.currentTimeMillis()) { + timer.stop(); + dialog.dispose(); + } + } + }; + timer.addActionListener(listener); + timer.start(); + + + // Wait for action + dialog.setVisible(true); + + + // Check result + UpdateInfo info = retriever.getUpdateInfo(); + if (info == null) { + JOptionPane.showMessageDialog(this, + //// An error occurred while communicating with the server. + trans.get("pref.dlg.lbl.msg1"), + //// Unable to retrieve update information + trans.get("pref.dlg.lbl.msg2"), JOptionPane.WARNING_MESSAGE, null); + } else if (info.getLatestVersion() == null || + info.getLatestVersion().equals("") || + BuildProperties.getVersion().equalsIgnoreCase(info.getLatestVersion())) { + JOptionPane.showMessageDialog(this, + //// You are running the latest version of OpenRocket. + trans.get("pref.dlg.lbl.msg3"), + //// No updates available + trans.get("pref.dlg.lbl.msg4"), JOptionPane.INFORMATION_MESSAGE, null); + } else { + UpdateInfoDialog infoDialog = new UpdateInfoDialog(info); + infoDialog.setVisible(true); + if (infoDialog.isReminderSelected()) { + Application.getPreferences().putString(SwingPreferences.LAST_UPDATE, ""); + } else { + Application.getPreferences().putString(SwingPreferences.LAST_UPDATE, info.getLatestVersion()); + } + } + + } + + + //////// Singleton implementation //////// + + private static PreferencesDialog dialog = null; + + public static void showPreferences(Window parent) { + if (dialog != null) { + dialog.dispose(); + } + dialog = new PreferencesDialog(parent); + dialog.setVisible(true); + } + + +} diff --git a/core/src/net/sf/openrocket/gui/figureelements/CGCaret.java b/core/src/net/sf/openrocket/gui/figureelements/CGCaret.java new file mode 100644 index 00000000..14a8a677 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/figureelements/CGCaret.java @@ -0,0 +1,62 @@ +package net.sf.openrocket.gui.figureelements; + +import java.awt.Color; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Rectangle2D; + +/** + * A mark indicating the position of the center of gravity. It is a blue circle with every + * second quarter filled with blue. + * + * @author Sampo Niskanen + */ + +public class CGCaret extends Caret { + private static final float RADIUS = 7; + + private static Area caret = null; + + /** + * Create a new CGCaret at the specified coordinates. + */ + public CGCaret(double x, double y) { + super(x,y); + } + + /** + * Returns the Area corresponding to the caret. The Area object is created only once, + * after which the object is cloned for new copies. + */ + @Override + protected Area getCaret() { + if (caret != null) { + return (Area)caret.clone(); + } + + Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS); + caret = new Area(e); + + Area a; + a = new Area(new Rectangle2D.Float(-RADIUS,-RADIUS,RADIUS,RADIUS)); + caret.subtract(a); + a = new Area(new Rectangle2D.Float(0,0,RADIUS,RADIUS)); + caret.subtract(a); + + a = new Area(new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS)); + a.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f, + 2*0.9f*RADIUS,2*0.9f*RADIUS))); + caret.add(a); + + return (Area) caret.clone(); + } + + /** + * Return the color of the caret (blue). + */ + @Override + protected Color getColor() { + return Color.BLUE; + } + +} diff --git a/core/src/net/sf/openrocket/gui/figureelements/CPCaret.java b/core/src/net/sf/openrocket/gui/figureelements/CPCaret.java new file mode 100644 index 00000000..09e9cceb --- /dev/null +++ b/core/src/net/sf/openrocket/gui/figureelements/CPCaret.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.gui.figureelements; + +import java.awt.Color; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; + +/** + * A mark indicating the position of the center of pressure. It is a red filled circle + * inside a slightly larger red circle. + * + * @author Sampo Niskanen + */ + +public class CPCaret extends Caret { + private static final float RADIUS = 7; + + private static Area caret = null; + + /** + * Create a new CPCaret at the specified coordinates. + */ + public CPCaret(double x, double y) { + super(x,y); + } + + /** + * Returns the Area object of the caret. The Area object is created only once, + * after which new copies are cloned from it. + */ + @Override + protected Area getCaret() { + if (caret != null) { + return (Area)caret.clone(); + } + + Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS); + caret = new Area(e); + + caret.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f, + 2*0.9f*RADIUS,2*0.9f*RADIUS))); + + caret.add(new Area(new Ellipse2D.Float(-RADIUS*0.75f,-RADIUS*0.75f, + 2*0.75f*RADIUS,2*0.75f*RADIUS))); + + return (Area) caret.clone(); + } + + + /** + * Return the color of the caret (red). + */ + @Override + protected Color getColor() { + return Color.RED; + } +} diff --git a/core/src/net/sf/openrocket/gui/figureelements/Caret.java b/core/src/net/sf/openrocket/gui/figureelements/Caret.java new file mode 100644 index 00000000..b82731ff --- /dev/null +++ b/core/src/net/sf/openrocket/gui/figureelements/Caret.java @@ -0,0 +1,55 @@ +package net.sf.openrocket.gui.figureelements; + + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.awt.geom.Area; + +public abstract class Caret implements FigureElement { + private double x,y; + + /** + * Creates a new caret at the specified coordinates. + */ + public Caret(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * Sets the position of the caret to the new coordinates. + */ + public void setPosition(double x, double y) { + this.x = x; + this.y = y; + } + + /** + * Paints the caret to the Graphics2D element. + */ + public void paint(Graphics2D g2, double scale) { + Area caret = getCaret(); + AffineTransform t = new AffineTransform(1.0/scale, 0, 0, 1.0/scale, x, y); + caret.transform(t); + + g2.setColor(getColor()); + g2.fill(caret); + } + + + public void paint(Graphics2D g2, double scale, Rectangle visible) { + throw new UnsupportedOperationException("paint() with rectangle unsupported."); + } + + /** + * Return the Area object corresponding to the mark. + */ + protected abstract Area getCaret(); + + /** + * Return the color to be used when drawing the mark. + */ + protected abstract Color getColor(); +} diff --git a/core/src/net/sf/openrocket/gui/figureelements/FigureElement.java b/core/src/net/sf/openrocket/gui/figureelements/FigureElement.java new file mode 100644 index 00000000..953d1918 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/figureelements/FigureElement.java @@ -0,0 +1,12 @@ +package net.sf.openrocket.gui.figureelements; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +public interface FigureElement { + + public void paint(Graphics2D g2, double scale); + + public void paint(Graphics2D g2, double scale, Rectangle visible); + +} diff --git a/core/src/net/sf/openrocket/gui/figureelements/RocketInfo.java b/core/src/net/sf/openrocket/gui/figureelements/RocketInfo.java new file mode 100644 index 00000000..39fb32a5 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/figureelements/RocketInfo.java @@ -0,0 +1,439 @@ +package net.sf.openrocket.gui.figureelements; + +import static net.sf.openrocket.util.Chars.ALPHA; +import static net.sf.openrocket.util.Chars.THETA; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.font.GlyphVector; +import java.awt.geom.Rectangle2D; + +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.MathUtil; + + +/** + * A FigureElement that draws text at different positions in the figure + * with general data about the rocket. + * + * @author Sampo Niskanen + */ +public class RocketInfo implements FigureElement { + + private static final Translator trans = Application.getTranslator(); + // Margin around the figure edges, pixels + private static final int MARGIN = 8; + + // Font to use + private static final Font FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 11); + private static final Font SMALLFONT = new Font(Font.SANS_SERIF, Font.PLAIN, 9); + + + private final Caret cpCaret = new CPCaret(0,0); + private final Caret cgCaret = new CGCaret(0,0); + + private final Configuration configuration; + private final UnitGroup stabilityUnits; + + private double cg = 0, cp = 0; + private double length = 0, diameter = 0; + private double mass = 0; + private double aoa = Double.NaN, theta = Double.NaN, mach = Application.getPreferences().getDefaultMach(); + + private WarningSet warnings = null; + + private boolean calculatingData = false; + private FlightData flightData = null; + + private Graphics2D g2 = null; + private float line = 0; + private float x1, x2, y1, y2; + + + + + + public RocketInfo(Configuration configuration) { + this.configuration = configuration; + this.stabilityUnits = UnitGroup.stabilityUnits(configuration); + } + + + @Override + public void paint(Graphics2D g2, double scale) { + throw new UnsupportedOperationException("paint() must be called with coordinates"); + } + + @Override + public void paint(Graphics2D g2, double scale, Rectangle visible) { + this.g2 = g2; + this.line = FONT.getLineMetrics("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + g2.getFontRenderContext()).getHeight(); + + x1 = visible.x + MARGIN; + x2 = visible.x + visible.width - MARGIN; + y1 = visible.y + line ; + y2 = visible.y + visible.height - MARGIN; + + drawMainInfo(); + drawStabilityInfo(); + drawWarnings(); + drawFlightInformation(); + } + + + public void setCG(double cg) { + this.cg = cg; + } + + public void setCP(double cp) { + this.cp = cp; + } + + public void setLength(double length) { + this.length = length; + } + + public void setDiameter(double diameter) { + this.diameter = diameter; + } + + public void setMass(double mass) { + this.mass = mass; + } + + public void setWarnings(WarningSet warnings) { + this.warnings = warnings.clone(); + } + + public void setAOA(double aoa) { + this.aoa = aoa; + } + + public void setTheta(double theta) { + this.theta = theta; + } + + public void setMach(double mach) { + this.mach = mach; + } + + + public void setFlightData(FlightData data) { + this.flightData = data; + } + + public void setCalculatingData(boolean calc) { + this.calculatingData = calc; + } + + + + + private void drawMainInfo() { + GlyphVector name = createText(configuration.getRocket().getName()); + GlyphVector lengthLine = createText( + //// Length + trans.get("RocketInfo.lengthLine.Length") +" " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(length) + + //// , max. diameter + trans.get("RocketInfo.lengthLine.maxdiameter") +" " + + UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(diameter)); + + String massText; + if (configuration.hasMotors()) + //// Mass with motors + massText = trans.get("RocketInfo.massText1") +" "; + else + //// Mass with no motors + massText = trans.get("RocketInfo.massText2") +" "; + + massText += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(mass); + + GlyphVector massLine = createText(massText); + + + g2.setColor(Color.BLACK); + + g2.drawGlyphVector(name, x1, y1); + g2.drawGlyphVector(lengthLine, x1, y1+line); + g2.drawGlyphVector(massLine, x1, y1+2*line); + + } + + + private void drawStabilityInfo() { + String at; + //// at M= + at = trans.get("RocketInfo.at")+UnitGroup.UNITS_COEFFICIENT.getDefaultUnit().toStringUnit(mach); + if (!Double.isNaN(aoa)) { + at += " "+ALPHA+"=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(aoa); + } + if (!Double.isNaN(theta)) { + at += " "+THETA+"=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(theta); + } + + GlyphVector cgValue = createText( + getCg()); + GlyphVector cpValue = createText( + getCp()); + GlyphVector stabValue = createText( + getStability()); + //// CG: + GlyphVector cgText = createText(trans.get("RocketInfo.cgText") +" "); + //// CP: + GlyphVector cpText = createText(trans.get("RocketInfo.cpText") +" "); + //// Stability: + GlyphVector stabText = createText(trans.get("RocketInfo.stabText") + " "); + GlyphVector atText = createSmallText(at); + + Rectangle2D cgRect = cgValue.getVisualBounds(); + Rectangle2D cpRect = cpValue.getVisualBounds(); + Rectangle2D cgTextRect = cgText.getVisualBounds(); + Rectangle2D cpTextRect = cpText.getVisualBounds(); + Rectangle2D stabRect = stabValue.getVisualBounds(); + Rectangle2D stabTextRect = stabText.getVisualBounds(); + Rectangle2D atTextRect = atText.getVisualBounds(); + + double unitWidth = MathUtil.max(cpRect.getWidth(), cgRect.getWidth(), + stabRect.getWidth()); + double textWidth = Math.max(cpTextRect.getWidth(), cgTextRect.getWidth()); + + + g2.setColor(Color.BLACK); + + g2.drawGlyphVector(stabValue, (float)(x2-stabRect.getWidth()), y1); + g2.drawGlyphVector(cgValue, (float)(x2-cgRect.getWidth()), y1+line); + g2.drawGlyphVector(cpValue, (float)(x2-cpRect.getWidth()), y1+2*line); + + g2.drawGlyphVector(stabText, (float)(x2-unitWidth-stabTextRect.getWidth()), y1); + g2.drawGlyphVector(cgText, (float)(x2-unitWidth-cgTextRect.getWidth()), y1+line); + g2.drawGlyphVector(cpText, (float)(x2-unitWidth-cpTextRect.getWidth()), y1+2*line); + + cgCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+line-0.3*line); + cgCaret.paint(g2, 1.7); + + cpCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+2*line-0.3*line); + cpCaret.paint(g2, 1.7); + + float atPos; + if (unitWidth + textWidth + 10 > atTextRect.getWidth()) { + atPos = (float)(x2-(unitWidth+textWidth+10+atTextRect.getWidth())/2); + } else { + atPos = (float)(x2 - atTextRect.getWidth()); + } + + g2.setColor(Color.GRAY); + g2.drawGlyphVector(atText, atPos, y1 + 3*line); + + } + + /** + * Get the mass, in default mass units. + * + * @return the mass + */ + public double getMass() { + return mass; + } + + /** + * Get the mass in specified mass units. + * + * @param u UnitGroup.MASS + * + * @return the mass + */ + public String getMass(Unit u) { + return u.toStringUnit(mass); + } + + /** + * Get the stability, in calibers. + * + * @return the current stability margin + */ + public String getStability () { + return stabilityUnits.getDefaultUnit().toStringUnit(cp-cg); + } + + /** + * Get the center of pressure in default length units. + * + * @return the distance from the tip to the center of pressure, in default length units + */ + public String getCp () { + return getCp(UnitGroup.UNITS_LENGTH.getDefaultUnit()); + } + + /** + * Get the center of pressure in default length units. + * + * @param u UnitGroup.LENGTH + * + * @return the distance from the tip to the center of pressure, in default length units + */ + public String getCp (Unit u) { + return u.toStringUnit(cp); + } + + /** + * Get the center of gravity in default length units. + * + * @return the distance from the tip to the center of gravity, in default length units + */ + public String getCg () { + return getCg(UnitGroup.UNITS_LENGTH.getDefaultUnit()); + } + + /** + * Get the center of gravity in specified length units. + * + * @param u UnitGroup.LENGTH + * @return the distance from the tip to the center of gravity, in specified units + */ + public String getCg (Unit u) { + return u.toStringUnit(cg); + } + + /** + * Get the flight data for the current motor configuration. + * + * @return flight data, or null + */ + public FlightData getFlightData () { + return flightData; + } + + private void drawWarnings() { + if (warnings == null || warnings.isEmpty()) + return; + + GlyphVector[] texts = new GlyphVector[warnings.size()+1]; + double max = 0; + + //// Warning: + texts[0] = createText(trans.get("RocketInfo.Warning")); + int i=1; + for (Warning w: warnings) { + texts[i] = createText(w.toString()); + i++; + } + + for (GlyphVector v: texts) { + Rectangle2D rect = v.getVisualBounds(); + if (rect.getWidth() > max) + max = rect.getWidth(); + } + + + float y = y2 - line * warnings.size(); + g2.setColor(new Color(255,0,0,130)); + + for (GlyphVector v: texts) { + Rectangle2D rect = v.getVisualBounds(); + g2.drawGlyphVector(v, (float)(x2 - max/2 - rect.getWidth()/2), y); + y += line; + } + } + + + private void drawFlightInformation() { + double height = drawFlightData(); + + if (calculatingData) { + //// Calculating... + GlyphVector calculating = createText(trans.get("RocketInfo.Calculating")); + g2.setColor(Color.BLACK); + g2.drawGlyphVector(calculating, x1, (float)(y2-height)); + } + } + + + private double drawFlightData() { + if (flightData == null) + return 0; + + double width=0; + + //// Apogee: + GlyphVector apogee = createText(trans.get("RocketInfo.Apogee")+" "); + //// Max. velocity: + GlyphVector maxVelocity = createText(trans.get("RocketInfo.Maxvelocity") +" "); + //// Max. acceleration: + GlyphVector maxAcceleration = createText(trans.get("RocketInfo.Maxacceleration") + " "); + + GlyphVector apogeeValue, velocityValue, accelerationValue; + if (!Double.isNaN(flightData.getMaxAltitude())) { + apogeeValue = createText( + UnitGroup.UNITS_DISTANCE.toStringUnit(flightData.getMaxAltitude())); + } else { + //// N/A + apogeeValue = createText(trans.get("RocketInfo.apogeeValue")); + } + if (!Double.isNaN(flightData.getMaxVelocity())) { + velocityValue = createText( + UnitGroup.UNITS_VELOCITY.toStringUnit(flightData.getMaxVelocity()) + + //// (Mach + " " +trans.get("RocketInfo.Mach") +" " + + UnitGroup.UNITS_COEFFICIENT.toString(flightData.getMaxMachNumber()) + ")"); + } else { + //// N/A + velocityValue = createText(trans.get("RocketInfo.velocityValue")); + } + if (!Double.isNaN(flightData.getMaxAcceleration())) { + accelerationValue = createText( + UnitGroup.UNITS_ACCELERATION.toStringUnit(flightData.getMaxAcceleration())); + } else { + //// N/A + accelerationValue = createText(trans.get("RocketInfo.accelerationValue")); + } + + Rectangle2D rect; + rect = apogee.getVisualBounds(); + width = MathUtil.max(width, rect.getWidth()); + + rect = maxVelocity.getVisualBounds(); + width = MathUtil.max(width, rect.getWidth()); + + rect = maxAcceleration.getVisualBounds(); + width = MathUtil.max(width, rect.getWidth()); + + width += 5; + + if (!calculatingData) + g2.setColor(new Color(0,0,127)); + else + g2.setColor(new Color(0,0,127,127)); + + + g2.drawGlyphVector(apogee, (float)x1, (float)(y2-2*line)); + g2.drawGlyphVector(maxVelocity, (float)x1, (float)(y2-line)); + g2.drawGlyphVector(maxAcceleration, (float)x1, (float)(y2)); + + g2.drawGlyphVector(apogeeValue, (float)(x1+width), (float)(y2-2*line)); + g2.drawGlyphVector(velocityValue, (float)(x1+width), (float)(y2-line)); + g2.drawGlyphVector(accelerationValue, (float)(x1+width), (float)(y2)); + + return 3*line; + } + + + + private GlyphVector createText(String text) { + return FONT.createGlyphVector(g2.getFontRenderContext(), text); + } + + private GlyphVector createSmallText(String text) { + return SMALLFONT.createGlyphVector(g2.getFontRenderContext(), text); + } + +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java b/core/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java new file mode 100644 index 00000000..baff4a5b --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java @@ -0,0 +1,182 @@ +package net.sf.openrocket.gui.help.tours; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; + +import javax.swing.AbstractListModel; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JEditorPane; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.components.StyledLabel.Style; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Named; + +public class GuidedTourSelectionDialog extends JDialog { + + private static final Translator trans = Application.getTranslator(); + + + + private final SlideSetManager slideSetManager; + private final List tourNames; + + private SlideShowDialog slideShowDialog; + + private JList tourList; + private JEditorPane tourDescription; + private JLabel tourLength; + + + public GuidedTourSelectionDialog(Window parent) { + super(parent, trans.get("title"), ModalityType.MODELESS); + + slideSetManager = SlideSetManager.getSlideSetManager(); + tourNames = slideSetManager.getSlideSetNames(); + + JPanel panel = new JPanel(new MigLayout("fill")); + + panel.add(new StyledLabel(trans.get("lbl.selectTour"), Style.BOLD), "spanx, wrap rel"); + + tourList = new JList(new TourListModel()); + tourList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + tourList.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + @Override + public void valueChanged(ListSelectionEvent e) { + updateText(); + } + }); + tourList.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + startTour(); + } + } + }); + panel.add(new JScrollPane(tourList), "grow, gapright unrel, w 200lp, h 250lp"); + + + + // Sub-panel containing description and start button + JPanel sub = new JPanel(new MigLayout("fill, ins 0")); + sub.add(new StyledLabel(trans.get("lbl.description"), -1), "wrap rel"); + + tourDescription = new JEditorPane("text/html", ""); + tourDescription.setEditable(false); + StyleSheet ss = slideSetManager.getSlideSet(tourNames.get(0)).getStyleSheet(); + ((HTMLDocument) tourDescription.getDocument()).getStyleSheet().addStyleSheet(ss); + sub.add(new JScrollPane(tourDescription), "grow, wrap rel"); + + tourLength = new StyledLabel(-1); + sub.add(tourLength, "wrap unrel"); + + JButton start = new JButton(trans.get("btn.start")); + start.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + startTour(); + } + }); + sub.add(start, "growx"); + + panel.add(sub, "grow, wrap para, w 350lp, h 250lp"); + + + + JButton close = new JButton(trans.get("button.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + GuidedTourSelectionDialog.this.dispose(); + } + }); + panel.add(close, "spanx, right"); + + this.add(panel); + GUIUtil.setDisposableDialogOptions(this, close); + GUIUtil.rememberWindowPosition(this); + tourList.setSelectedIndex(0); + } + + + private void startTour() { + SlideSet ss = getSelectedSlideSet(); + if (ss == null) { + return; + } + + if (slideShowDialog != null && !slideShowDialog.isVisible()) { + closeTour(); + } + + if (slideShowDialog == null) { + slideShowDialog = new SlideShowDialog(this); + } + + slideShowDialog.setSlideSet(ss, 0); + slideShowDialog.setVisible(true); + } + + + private void closeTour() { + if (slideShowDialog != null) { + slideShowDialog.dispose(); + slideShowDialog = null; + } + } + + + private void updateText() { + SlideSet ss = getSelectedSlideSet(); + if (ss != null) { + tourDescription.setText(ss.getDescription()); + tourLength.setText(trans.get("lbl.length") + " " + ss.getSlideCount()); + } else { + tourDescription.setText(""); + tourLength.setText(trans.get("lbl.length")); + } + } + + + @SuppressWarnings("unchecked") + private SlideSet getSelectedSlideSet() { + return ((Named) tourList.getSelectedValue()).get(); + } + + private class TourListModel extends AbstractListModel { + + @Override + public Object getElementAt(int index) { + String name = tourNames.get(index); + SlideSet set = slideSetManager.getSlideSet(name); + return new Named(set, set.getTitle()); + } + + @Override + public int getSize() { + return tourNames.size(); + } + + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/Slide.java b/core/src/net/sf/openrocket/gui/help/tours/Slide.java new file mode 100644 index 00000000..c3c1fab8 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/Slide.java @@ -0,0 +1,78 @@ +package net.sf.openrocket.gui.help.tours; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.net.URL; + +import javax.imageio.ImageIO; + +/** + * An individual slide in a guided tour. It contains a image (or reference to an + * image file) plus a text description (in HTML). + * + * @author Sampo Niskanen + */ +public class Slide { + private static final String NO_IMAGE = "none"; + + private final String imageFile; + private SoftReference imageReference = null; + + private final String text; + + + + public Slide(String imageFile, String text) { + this.imageFile = imageFile; + this.text = text; + } + + + + public BufferedImage getImage() { + + if (imageFile.equals(NO_IMAGE)) { + return new BufferedImage(0, 0, BufferedImage.TYPE_INT_ARGB); + } + + // Check the cache + if (imageReference != null) { + BufferedImage image = imageReference.get(); + if (image != null) { + return image; + } + } + + // Otherwise load and cache + BufferedImage image = loadImage(); + imageReference = new SoftReference(image); + + return image; + } + + public String getText() { + return text; + } + + + + private BufferedImage loadImage() { + BufferedImage img; + + try { + URL url = ClassLoader.getSystemResource(imageFile); + if (url != null) { + img = ImageIO.read(url); + } else { + //FIXME + img = null; + } + } catch (IOException e) { + // FIXME + img = null; + } + + return img; + } +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/SlideSet.java b/core/src/net/sf/openrocket/gui/help/tours/SlideSet.java new file mode 100644 index 00000000..459bda23 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/SlideSet.java @@ -0,0 +1,62 @@ +package net.sf.openrocket.gui.help.tours; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.text.html.StyleSheet; + +/** + * A set of slides that composes a tour. + * + * A slide set contains a (localized, plain-text) title for the tour, a (possibly + * multiline, HTML-formatted) description and a number of slides. + * + * @author Sampo Niskanen + */ +public class SlideSet { + + private String title = ""; + private String description = ""; + private final List slides = new ArrayList(); + private StyleSheet styleSheet = new StyleSheet(); + + + + public String getTitle() { + return title; + } + + public void setTitle(String name) { + this.title = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + + public Slide getSlide(int index) { + return this.slides.get(index); + } + + public void addSlide(Slide slide) { + this.slides.add(slide); + } + + public int getSlideCount() { + return this.slides.size(); + } + + public StyleSheet getStyleSheet() { + return styleSheet; + } + + public void setStyleSheet(StyleSheet styleSheet) { + this.styleSheet = styleSheet; + } + +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java b/core/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java new file mode 100644 index 00000000..1a32cb47 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java @@ -0,0 +1,173 @@ +package net.sf.openrocket.gui.help.tours; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.sf.openrocket.util.BugException; + +/** + * Class that loads a slide set from a file. + * + * @author Sampo Niskanen + */ +public class SlideSetLoader { + + private static final Pattern NEW_SLIDE_PATTERN = Pattern.compile("^\\[(.*)\\]$"); + + private final String baseDir; + private TextLineReader source; + private Locale locale; + + + + + /** + * Constructor. + * + * @param baseDir The base directory from which to load from. It is prepended to the loaded + * file names and image file names. + */ + public SlideSetLoader(String baseDir) { + this(baseDir, Locale.getDefault()); + } + + + /** + * Constructor. + * + * @param baseDir The base directory from which to load from. It is prepended to the loaded + * file names and image file names. + * @param locale The locale for which the files are loaded. + */ + public SlideSetLoader(String baseDir, Locale locale) { + if (baseDir.length() > 0 && !baseDir.endsWith("/")) { + baseDir = baseDir + "/"; + } + this.baseDir = baseDir; + this.locale = locale; + } + + + /** + * Load a slide set from a file. The base directory is prepended to the + * file name first. + * + * @param filename the file to read in the base directory. + * @return the slide set + */ + public SlideSet load(String filename) throws IOException { + String file = baseDir + filename; + InputStream in = getLocalizedFile(file); + + try { + InputStreamReader reader = new InputStreamReader(in, "UTF-8"); + return load(reader); + } finally { + in.close(); + } + } + + + private InputStream getLocalizedFile(String filename) throws IOException { + for (String file : generateLocalizedFiles(filename)) { + InputStream in = ClassLoader.getSystemResourceAsStream(file); + if (in != null) { + return in; + } + } + throw new FileNotFoundException("File '" + filename + "' not found."); + } + + private List generateLocalizedFiles(String filename) { + String base, ext; + int index = filename.lastIndexOf('.'); + if (index >= 0) { + base = filename.substring(0, index); + ext = filename.substring(index); + } else { + base = filename; + ext = ""; + } + + + List list = new ArrayList(); + list.add(base + "_" + locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant() + ext); + list.add(base + "_" + locale.getLanguage() + "_" + locale.getCountry() + ext); + list.add(base + "_" + locale.getLanguage() + ext); + list.add(base + ext); + return list; + } + + + /** + * Load slide set from a reader. + * + * @param reader the reader to read from. + * @return the slide set. + */ + public SlideSet load(Reader reader) throws IOException { + source = new TextLineReader(reader); + + // Read title and description + String title = source.next(); + StringBuilder desc = new StringBuilder(); + while (!nextLineStartsSlide()) { + if (desc.length() > 0) { + desc.append('\n'); + } + desc.append(source.next()); + } + + // Create the slide set + SlideSet set = new SlideSet(); + set.setTitle(title); + set.setDescription(desc.toString()); + + + // Read the slides + while (source.hasNext()) { + Slide s = readSlide(); + set.addSlide(s); + } + + return set; + } + + + private Slide readSlide() { + + String imgLine = source.next(); + Matcher matcher = NEW_SLIDE_PATTERN.matcher(imgLine); + if (!matcher.matches()) { + throw new BugException("Line did not match new slide pattern: " + imgLine); + } + + String imageFile = matcher.group(1); + + StringBuffer desc = new StringBuffer(); + while (source.hasNext() && !nextLineStartsSlide()) { + if (desc.length() > 0) { + desc.append('\n'); + } + desc.append(source.next()); + } + + return new Slide(baseDir + imageFile, desc.toString()); + } + + + + private boolean nextLineStartsSlide() { + return NEW_SLIDE_PATTERN.matcher(source.peek()).matches(); + } + + +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java b/core/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java new file mode 100644 index 00000000..0d9e3815 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java @@ -0,0 +1,165 @@ +package net.sf.openrocket.gui.help.tours; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.text.html.StyleSheet; + +import net.sf.openrocket.util.BugException; + +/** + * A manager that loads a number of slide sets from a defined base directory + * and provides access to them. + * + * @author Sampo Niskanen + */ +public class SlideSetManager { + private static final String TOURS_BASE_DIR = "datafiles/tours"; + + private static final String TOURS_FILE = "tours.txt"; + private static final String STYLESHEET_FILE = "style.css"; + + private static SlideSetManager slideSetManager = null; + + + private final String baseDir; + private final Map slideSets = new LinkedHashMap(); + + + /** + * Sole constructor. + * + * @param baseDir the base directory containing the tours and style files. + */ + public SlideSetManager(String baseDir) { + if (baseDir.length() > 0 && !baseDir.endsWith("/")) { + baseDir = baseDir + "/"; + } + this.baseDir = baseDir; + } + + + /** + * Load all the tours. + */ + public void load() throws IOException { + slideSets.clear(); + + List tours = loadTourList(); + StyleSheet styleSheet = loadStyleSheet(); + + for (String fileAndDir : tours) { + String base; + String file; + + String fullFileAndDir = baseDir + fileAndDir; + int index = fullFileAndDir.lastIndexOf('/'); + if (index >= 0) { + base = fullFileAndDir.substring(0, index); + file = fullFileAndDir.substring(index + 1); + } else { + base = ""; + file = ""; + } + + SlideSetLoader loader = new SlideSetLoader(base); + SlideSet set = loader.load(file); + set.setStyleSheet(styleSheet); + slideSets.put(fileAndDir, set); + } + + } + + + /** + * Return a set containing all the slide set names. + */ + public List getSlideSetNames() { + return new ArrayList(slideSets.keySet()); + } + + /** + * Retrieve an individual slide set. + * + * @param name the name of the slide set to retrieve. + * @return the slide set (never null) + * @throws IllegalArgumentException if the slide set with the name does not exist. + */ + public SlideSet getSlideSet(String name) { + SlideSet s = slideSets.get(name); + if (s == null) { + throw new IllegalArgumentException("Slide set with name '" + name + "' not found."); + } + return s; + } + + + private List loadTourList() throws IOException { + InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + TOURS_FILE); + if (in == null) { + throw new FileNotFoundException("File '" + baseDir + TOURS_FILE + "' not found."); + } + + try { + + List tours = new ArrayList(); + TextLineReader reader = new TextLineReader(in); + while (reader.hasNext()) { + tours.add(reader.next()); + } + return tours; + + } finally { + in.close(); + } + } + + + private StyleSheet loadStyleSheet() throws IOException { + InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + STYLESHEET_FILE); + if (in == null) { + throw new FileNotFoundException("File '" + baseDir + STYLESHEET_FILE + "' not found."); + } + + try { + + StyleSheet ss = new StyleSheet(); + InputStreamReader reader = new InputStreamReader(in, "UTF-8"); + ss.loadRules(reader, null); + return ss; + + } finally { + in.close(); + } + + } + + + + /** + * Return a singleton implementation that has loaded the default tours. + */ + public static SlideSetManager getSlideSetManager() { + if (slideSetManager == null) { + try { + SlideSetManager ssm = new SlideSetManager(TOURS_BASE_DIR); + ssm.load(); + + if (ssm.getSlideSetNames().isEmpty()) { + throw new FileNotFoundException("No tours found."); + } + + slideSetManager = ssm; + } catch (IOException e) { + throw new BugException(e); + } + } + return slideSetManager; + } +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java b/core/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java new file mode 100644 index 00000000..842e4a86 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java @@ -0,0 +1,77 @@ +package net.sf.openrocket.gui.help.tours; + +import java.awt.Dimension; + +import javax.swing.JEditorPane; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.event.HyperlinkListener; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; + +import net.sf.openrocket.gui.components.ImageDisplayComponent; + +/** + * Component that displays a single slide, with the image on top and + * text below it. The portions are resizeable. + * + * @author Sampo Niskanen + */ +public class SlideShowComponent extends JSplitPane { + + private final int WIDTH = 600; + private final int HEIGHT_IMAGE = 400; + private final int HEIGHT_TEXT = 100; + + private final ImageDisplayComponent imageDisplay; + private final JEditorPane textPane; + + + public SlideShowComponent() { + super(VERTICAL_SPLIT); + + imageDisplay = new ImageDisplayComponent(); + imageDisplay.setPreferredSize(new Dimension(WIDTH, HEIGHT_IMAGE)); + this.setLeftComponent(imageDisplay); + + textPane = new JEditorPane("text/html", ""); + textPane.setEditable(false); + textPane.setPreferredSize(new Dimension(WIDTH, HEIGHT_TEXT)); + + JScrollPane scrollPanel = new JScrollPane(textPane); + this.setRightComponent(scrollPanel); + + this.setResizeWeight(((double) HEIGHT_IMAGE) / (HEIGHT_IMAGE + HEIGHT_TEXT)); + } + + + + public void setSlide(Slide slide) { + this.imageDisplay.setImage(slide.getImage()); + this.textPane.setText(slide.getText()); + this.textPane.setCaretPosition(0); + } + + + /** + * Replace the current HTML style sheet with a new style sheet. + */ + public void setStyleSheet(StyleSheet newStyleSheet) { + HTMLDocument doc = (HTMLDocument) textPane.getDocument(); + StyleSheet base = doc.getStyleSheet(); + StyleSheet[] linked = base.getStyleSheets(); + if (linked != null) { + for (StyleSheet ss : linked) { + base.removeStyleSheet(ss); + } + } + + base.addStyleSheet(newStyleSheet); + } + + + public void addHyperlinkListener(HyperlinkListener listener) { + textPane.addHyperlinkListener(listener); + } + +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java b/core/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java new file mode 100644 index 00000000..61936b91 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java @@ -0,0 +1,173 @@ +package net.sf.openrocket.gui.help.tours; + +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JRootPane; +import javax.swing.KeyStroke; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Chars; + +public class SlideShowDialog extends JDialog { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private SlideShowComponent slideShowComponent; + private SlideSet slideSet; + private int position; + + private JButton nextButton; + private JButton prevButton; + private JButton closeButton; + + + public SlideShowDialog(Window parent) { + super(parent, ModalityType.MODELESS); + + JPanel panel = new JPanel(new MigLayout("fill")); + + slideShowComponent = new SlideShowComponent(); + slideShowComponent.addHyperlinkListener(new SlideShowLinkListener(parent)); + panel.add(slideShowComponent, "spanx, grow, wrap para"); + + + JPanel sub = new JPanel(new MigLayout("ins 0, fill")); + + prevButton = new JButton(Chars.LEFT_ARROW + " " + trans.get("btn.prev")); + prevButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Clicked previous button"); + setPosition(position - 1); + } + }); + sub.add(prevButton, "left"); + + + + nextButton = new JButton(trans.get("btn.next") + " " + Chars.RIGHT_ARROW); + nextButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Clicked next button"); + setPosition(position + 1); + } + }); + sub.add(nextButton, "left, gapleft para"); + + + sub.add(new JPanel(), "growx"); + + + closeButton = new JButton(trans.get("button.close")); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SlideShowDialog.this.dispose(); + } + }); + sub.add(closeButton, "right"); + + + panel.add(sub, "growx"); + + this.add(panel); + updateEnabled(); + addKeyActions(); + GUIUtil.setDisposableDialogOptions(this, nextButton); + nextButton.grabFocus(); + GUIUtil.rememberWindowPosition(this); + GUIUtil.rememberWindowSize(this); + this.setAlwaysOnTop(true); + } + + public void setSlideSet(SlideSet slideSet, int position) { + this.slideSet = slideSet; + this.setTitle(slideSet.getTitle() + " " + Chars.EMDASH + " OpenRocket"); + slideShowComponent.setStyleSheet(slideSet.getStyleSheet()); + setPosition(position); + } + + public void setPosition(int position) { + if (this.slideSet == null) { + throw new BugException("setPosition called when slideSet is null"); + } + + if (position < 0 || position >= slideSet.getSlideCount()) { + throw new BugException("position exceeds slide count, position=" + position + + " slideCount=" + slideSet.getSlideCount()); + } + + this.position = position; + slideShowComponent.setSlide(slideSet.getSlide(position)); + updateEnabled(); + } + + + private void updateEnabled() { + if (slideSet == null) { + prevButton.setEnabled(false); + nextButton.setEnabled(false); + return; + } + + prevButton.setEnabled(position > 0); + nextButton.setEnabled(position < slideSet.getSlideCount() - 1); + } + + + + + + private void addKeyActions() { + Action next = new AbstractAction() { + @Override + public void actionPerformed(ActionEvent event) { + log.user("Key action for next slide"); + if (position < slideSet.getSlideCount() - 1) { + setPosition(position + 1); + } + } + }; + + Action previous = new AbstractAction() { + @Override + public void actionPerformed(ActionEvent event) { + log.user("Key action for previous slide"); + if (position > 0) { + setPosition(position - 1); + } + } + }; + + String nextKey = "slide:next"; + String prevKey = "slide:previous"; + + JRootPane root = this.getRootPane(); + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), nextKey); + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), nextKey); + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), prevKey); + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), prevKey); + + root.getActionMap().put(nextKey, next); + root.getActionMap().put(prevKey, previous); + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/SlideShowLinkListener.java b/core/src/net/sf/openrocket/gui/help/tours/SlideShowLinkListener.java new file mode 100644 index 00000000..5fc1d888 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/SlideShowLinkListener.java @@ -0,0 +1,55 @@ +package net.sf.openrocket.gui.help.tours; + +import java.awt.Desktop; +import java.awt.Window; +import java.net.URL; + +import javax.swing.event.HyperlinkEvent; +import javax.swing.event.HyperlinkEvent.EventType; +import javax.swing.event.HyperlinkListener; + +import net.sf.openrocket.startup.Application; + +public class SlideShowLinkListener implements HyperlinkListener { + + private final Window parent; + + public SlideShowLinkListener(Window parent) { + this.parent = parent; + } + + @Override + public void hyperlinkUpdate(HyperlinkEvent event) { + + if (event.getEventType() != EventType.ACTIVATED) { + return; + } + + URL url = event.getURL(); + if (url != null && (url.getProtocol().equalsIgnoreCase("http") || url.getProtocol().equals("https"))) { + + if (Desktop.isDesktopSupported()) { + try { + Desktop.getDesktop().browse(url.toURI()); + } catch (Exception e) { + // Ignore + } + } + + } else { + + String name = event.getDescription(); + try { + SlideSet ss = SlideSetManager.getSlideSetManager().getSlideSet(name); + + SlideShowDialog dialog = new SlideShowDialog(parent); + dialog.setSlideSet(ss, 0); + dialog.setVisible(true); + } catch (IllegalArgumentException e) { + Application.getExceptionHandler().handleErrorCondition("Guided tour '" + name + "' not found."); + } + + } + + } +} diff --git a/core/src/net/sf/openrocket/gui/help/tours/TextLineReader.java b/core/src/net/sf/openrocket/gui/help/tours/TextLineReader.java new file mode 100644 index 00000000..fd3ddaf0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/help/tours/TextLineReader.java @@ -0,0 +1,120 @@ +package net.sf.openrocket.gui.help.tours; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import net.sf.openrocket.util.BugException; + +/** + * Read from a Reader object one line at a time, ignoring blank lines, + * preceding and trailing whitespace and comment lines starting with '#'. + * + * @author Sampo Niskanen + */ +public class TextLineReader implements Iterator { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + + + private final BufferedReader reader; + + private String next = null; + + /** + * Read from an input stream with UTF-8 character encoding. + */ + public TextLineReader(InputStream inputStream) { + this(new InputStreamReader(inputStream, UTF8)); + } + + + /** + * Read from a reader. + */ + public TextLineReader(Reader reader) { + if (reader instanceof BufferedReader) { + this.reader = (BufferedReader) reader; + } else { + this.reader = new BufferedReader(reader); + } + } + + + /** + * Test whether the file has more lines available. + */ + @Override + public boolean hasNext() { + if (next != null) { + return true; + } + + try { + next = readLine(); + } catch (IOException e) { + throw new BugException(e); + } + + return next != null; + } + + + /** + * Retrieve the next non-blank, non-comment line. + */ + @Override + public String next() { + if (hasNext()) { + String ret = next; + next = null; + return ret; + } + + throw new NoSuchElementException("End of file reached"); + } + + + /** + * Peek what the next line would be. + */ + public String peek() { + if (hasNext()) { + return next; + } + + throw new NoSuchElementException("End of file reached"); + } + + + private String readLine() throws IOException { + + while (true) { + // Read the next line + String line = reader.readLine(); + if (line == null) { + return null; + } + + // Check whether to accept the line + line = line.trim(); + if (line.length() > 0 && line.charAt(0) != '#') { + return line; + } + } + + } + + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + +} diff --git a/core/src/net/sf/openrocket/gui/main/BasicFrame.java b/core/src/net/sf/openrocket/gui/main/BasicFrame.java new file mode 100644 index 00000000..b6bab939 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -0,0 +1,1499 @@ +package net.sf.openrocket.gui.main; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.GeneralRocketLoader; +import net.sf.openrocket.file.RocketLoadException; +import net.sf.openrocket.file.RocketLoader; +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.file.openrocket.OpenRocketSaver; +import net.sf.openrocket.file.rocksim.export.RocksimSaver; +import net.sf.openrocket.gui.StorageOptionChooser; +import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.dialogs.AboutDialog; +import net.sf.openrocket.gui.dialogs.BugReportDialog; +import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog; +import net.sf.openrocket.gui.dialogs.DebugLogDialog; +import net.sf.openrocket.gui.dialogs.DetailDialog; +import net.sf.openrocket.gui.dialogs.ExampleDesignDialog; +import net.sf.openrocket.gui.dialogs.LicenseDialog; +import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog; +import net.sf.openrocket.gui.dialogs.PrintDialog; +import net.sf.openrocket.gui.dialogs.ScaleDialog; +import net.sf.openrocket.gui.dialogs.SwingWorkerDialog; +import net.sf.openrocket.gui.dialogs.WarningDialog; +import net.sf.openrocket.gui.dialogs.optimization.GeneralOptimizationDialog; +import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog; +import net.sf.openrocket.gui.help.tours.GuidedTourSelectionDialog; +import net.sf.openrocket.gui.main.componenttree.ComponentTree; +import net.sf.openrocket.gui.scalefigure.RocketPanel; +import net.sf.openrocket.gui.util.FileHelper; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.gui.util.OpenFileWorker; +import net.sf.openrocket.gui.util.SaveFileWorker; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MemoryManagement; +import net.sf.openrocket.util.MemoryManagement.MemoryData; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.TestRockets; + +import javax.swing.*; +import javax.swing.border.BevelBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +public class BasicFrame extends JFrame { + private static final LogHelper log = Application.getLogger(); + + /** + * The RocketLoader instance used for loading all rocket designs. + */ + private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader(); + + private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver(); + + private static final Translator trans = Application.getTranslator(); + + public static final int COMPONENT_TAB = 0; + public static final int SIMULATION_TAB = 1; + + + /** + * List of currently open frames. When the list goes empty + * it is time to exit the application. + */ + private static final ArrayList frames = new ArrayList(); + + + /** + * Whether "New" and "Open" should replace this frame. + * Should be set to false on the first rocket modification. + */ + private boolean replaceable = false; + + + + private final OpenRocketDocument document; + private final Rocket rocket; + + private JTabbedPane tabbedPane; + private RocketPanel rocketpanel; + private ComponentTree tree = null; + + private final DocumentSelectionModel selectionModel; + private final TreeSelectionModel componentSelectionModel; + private final ListSelectionModel simulationSelectionModel; + + /** Actions available for rocket modifications */ + private final RocketActions actions; + + + + /** + * Sole constructor. Creates a new frame based on the supplied document + * and adds it to the current frames list. + * + * @param document the document to show. + */ + public BasicFrame(OpenRocketDocument document) { + log.debug("Instantiating new BasicFrame"); + + this.document = document; + this.rocket = document.getRocket(); + this.rocket.getDefaultConfiguration().setAllStages(); + + + // Set replaceable flag to false at first modification + rocket.addComponentChangeListener(new ComponentChangeListener() { + @Override + public void componentChanged(ComponentChangeEvent e) { + replaceable = false; + BasicFrame.this.rocket.removeComponentChangeListener(this); + } + }); + + + // Create the component tree selection model that will be used + componentSelectionModel = new DefaultTreeSelectionModel(); + componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + + // Obtain the simulation selection model that will be used + SimulationPanel simulationPanel = new SimulationPanel(document); + simulationSelectionModel = simulationPanel.getSimulationListSelectionModel(); + + // Combine into a DocumentSelectionModel + selectionModel = new DocumentSelectionModel(document); + selectionModel.attachComponentTreeSelectionModel(componentSelectionModel); + selectionModel.attachSimulationListSelectionModel(simulationSelectionModel); + + + actions = new RocketActions(document, selectionModel, this); + + + log.debug("Constructing the BasicFrame UI"); + + // The main vertical split pane + JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true); + vertical.setResizeWeight(0.5); + this.add(vertical); + + + // The top tabbed pane + tabbedPane = new JTabbedPane(); + //// Rocket design + tabbedPane.addTab(trans.get("BasicFrame.tab.Rocketdesign"), null, designTab()); + //// Flight simulations + tabbedPane.addTab(trans.get("BasicFrame.tab.Flightsim"), null, simulationPanel); + + vertical.setTopComponent(tabbedPane); + + + + // Bottom segment, rocket figure + + rocketpanel = new RocketPanel(document); + vertical.setBottomComponent(rocketpanel); + + rocketpanel.setSelectionModel(tree.getSelectionModel()); + + + createMenu(); + + + rocket.addComponentChangeListener(new ComponentChangeListener() { + @Override + public void componentChanged(ComponentChangeEvent e) { + setTitle(); + } + }); + + setTitle(); + this.pack(); + + + // Set initial window size + Dimension size = Toolkit.getDefaultToolkit().getScreenSize(); + size.width = size.width * 9 / 10; + size.height = size.height * 9 / 10; + this.setSize(size); + + // Remember changed size + GUIUtil.rememberWindowSize(this); + + this.setLocationByPlatform(true); + + GUIUtil.setWindowIcons(this); + + this.validate(); + vertical.setDividerLocation(0.4); + setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + closeAction(); + } + }); + + frames.add(this); + log.debug("BasicFrame instantiation complete"); + } + + + /** + * Construct the "Rocket design" tab. This contains a horizontal split pane + * with the left component the design tree and the right component buttons + * for adding components. + */ + private JComponent designTab() { + JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); + horizontal.setResizeWeight(0.5); + + + // Upper-left segment, component tree + + JPanel panel = new JPanel(new MigLayout("fill, flowy", "", "[grow]")); + + tree = new ComponentTree(document); + tree.setSelectionModel(componentSelectionModel); + + // Remove JTree key events that interfere with menu accelerators + InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null); + + + + // Double-click opens config dialog + MouseListener ml = new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + int selRow = tree.getRowForLocation(e.getX(), e.getY()); + TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); + if (selRow != -1) { + if ((e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { + // Double-click + RocketComponent c = (RocketComponent) selPath.getLastPathComponent(); + ComponentConfigDialog.showDialog(BasicFrame.this, + BasicFrame.this.document, c); + } + } + } + }; + tree.addMouseListener(ml); + + // Update dialog when selection is changed + componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() { + @Override + public void valueChanged(TreeSelectionEvent e) { + // Scroll tree to the selected item + TreePath path = componentSelectionModel.getSelectionPath(); + if (path == null) + return; + tree.scrollPathToVisible(path); + + if (!ComponentConfigDialog.isDialogVisible()) + return; + RocketComponent c = (RocketComponent) path.getLastPathComponent(); + ComponentConfigDialog.showDialog(BasicFrame.this, + BasicFrame.this.document, c); + } + }); + + // Place tree inside scroll pane + JScrollPane scroll = new JScrollPane(tree); + panel.add(scroll, "spany, grow, wrap"); + + + // Buttons + JButton button = new JButton(actions.getMoveUpAction()); + panel.add(button, "sizegroup buttons, aligny 65%"); + + button = new JButton(actions.getMoveDownAction()); + panel.add(button, "sizegroup buttons, aligny 0%"); + + button = new JButton(actions.getEditAction()); + panel.add(button, "sizegroup buttons"); + + button = new JButton(actions.getNewStageAction()); + panel.add(button, "sizegroup buttons"); + + button = new JButton(actions.getDeleteAction()); + button.setIcon(null); + button.setMnemonic(0); + panel.add(button, "sizegroup buttons"); + + horizontal.setLeftComponent(panel); + + + // Upper-right segment, component addition buttons + + panel = new JPanel(new MigLayout("fill, insets 0", "[0::]")); + + scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel, + scroll.getViewport())); + scroll.setBorder(null); + scroll.setViewportBorder(null); + + TitledBorder border = BorderFactory.createTitledBorder(trans.get("BasicFrame.title.Addnewcomp")); + GUIUtil.changeFontStyle(border, Font.BOLD); + scroll.setBorder(border); + + panel.add(scroll, "grow"); + + horizontal.setRightComponent(panel); + + return horizontal; + } + + + + /** + * Return the currently selected rocket component, or null if none selected. + */ + private RocketComponent getSelectedComponent() { + TreePath path = componentSelectionModel.getSelectionPath(); + if (path == null) + return null; + tree.scrollPathToVisible(path); + + return (RocketComponent) path.getLastPathComponent(); + } + + + /** + * Creates the menu for the window. + */ + private void createMenu() { + JMenuBar menubar = new JMenuBar(); + JMenu menu; + JMenuItem item; + + //// File + menu = new JMenu(trans.get("main.menu.file")); + menu.setMnemonic(KeyEvent.VK_F); + //// File-handling related tasks + menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.desc")); + menubar.add(menu); + + //// New + item = new JMenuItem(trans.get("main.menu.file.new"), KeyEvent.VK_N); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK)); + item.setMnemonic(KeyEvent.VK_N); + //// Create a new rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.new.desc")); + item.setIcon(Icons.FILE_NEW); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("New... selected"); + newAction(); + if (replaceable) { + log.info("Closing previous window"); + closeAction(); + } + } + }); + menu.add(item); + + //// Open... + item = new JMenuItem(trans.get("main.menu.file.open"), KeyEvent.VK_O); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK)); + //// Open a rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Openrocketdesign")); + item.setIcon(Icons.FILE_OPEN); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Open... selected"); + openAction(); + } + }); + menu.add(item); + + //// Open example... + item = new JMenuItem(trans.get("main.menu.file.openExample")); + //// Open an example rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Openexamplerocketdesign")); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, + ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK)); + item.setIcon(Icons.FILE_OPEN_EXAMPLE); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Open example... selected"); + URL[] urls = ExampleDesignDialog.selectExampleDesigns(BasicFrame.this); + if (urls != null) { + for (URL u : urls) { + log.user("Opening example " + u); + open(u, BasicFrame.this); + } + } + } + }); + menu.add(item); + + menu.addSeparator(); + + //// Save + item = new JMenuItem(trans.get("main.menu.file.save"), KeyEvent.VK_S); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK)); + //// Save the current rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.SavecurRocketdesign")); + item.setIcon(Icons.FILE_SAVE); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Save selected"); + saveAction(); + } + }); + menu.add(item); + + //// Save as... + item = new JMenuItem(trans.get("main.menu.file.saveAs"), KeyEvent.VK_A); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, + ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK)); + //// Save the current rocket design to a new file + item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.SavecurRocketdesnewfile")); + item.setIcon(Icons.FILE_SAVE_AS); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Save as... selected"); + saveAsAction(); + } + }); + menu.add(item); + + //// Print... + item = new JMenuItem(trans.get("main.menu.file.print"), KeyEvent.VK_P); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.CTRL_MASK)); + //// Print parts list and fin template + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.print.desc")); + item.setIcon(Icons.FILE_PRINT); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Print action selected"); + printAction(); + } + }); + menu.add(item); + + + menu.addSeparator(); + + //// Close + item = new JMenuItem(trans.get("main.menu.file.close"), KeyEvent.VK_C); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK)); + //// Close the current rocket design + item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Closedesign")); + item.setIcon(Icons.FILE_CLOSE); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Close selected"); + closeAction(); + } + }); + menu.add(item); + + menu.addSeparator(); + + //// Quit + item = new JMenuItem(trans.get("main.menu.file.quit"), KeyEvent.VK_Q); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK)); + //// Quit the program + item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Quitprogram")); + item.setIcon(Icons.FILE_QUIT); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Quit selected"); + quitAction(); + } + }); + menu.add(item); + + + + //// Edit + menu = new JMenu(trans.get("main.menu.edit")); + menu.setMnemonic(KeyEvent.VK_E); + //// Rocket editing + menu.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.menu.Rocketedt")); + menubar.add(menu); + + + Action action = UndoRedoAction.newUndoAction(document); + item = new JMenuItem(action); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK)); + item.setMnemonic(KeyEvent.VK_U); + //// Undo the previous operation + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.undo.desc")); + + menu.add(item); + + action = UndoRedoAction.newRedoAction(document); + item = new JMenuItem(action); + item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK)); + item.setMnemonic(KeyEvent.VK_R); + //// Redo the previously undone operation + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.redo.desc")); + menu.add(item); + + menu.addSeparator(); + + + item = new JMenuItem(actions.getCutAction()); + menu.add(item); + + item = new JMenuItem(actions.getCopyAction()); + menu.add(item); + + item = new JMenuItem(actions.getPasteAction()); + menu.add(item); + + item = new JMenuItem(actions.getDeleteAction()); + menu.add(item); + + menu.addSeparator(); + + + + item = new JMenuItem(trans.get("main.menu.edit.resize")); + item.setIcon(Icons.EDIT_SCALE); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.resize.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Scale... selected"); + ScaleDialog dialog = new ScaleDialog(document, getSelectedComponent(), BasicFrame.this); + dialog.setVisible(true); + dialog.dispose(); + } + }); + menu.add(item); + + + + //// Preferences + item = new JMenuItem(trans.get("main.menu.edit.preferences")); + item.setIcon(Icons.PREFERENCES); + //// Setup the application preferences + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.preferences.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Preferences selected"); + PreferencesDialog.showPreferences(BasicFrame.this); + } + }); + menu.add(item); + + + + + //// Analyze + menu = new JMenu(trans.get("main.menu.analyze")); + menu.setMnemonic(KeyEvent.VK_A); + //// Analyzing the rocket + menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.desc")); + menubar.add(menu); + + //// Component analysis + item = new JMenuItem(trans.get("main.menu.analyze.componentAnalysis"), KeyEvent.VK_C); + //// Analyze the rocket components separately + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.componentAnalysis.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Component analysis selected"); + ComponentAnalysisDialog.showDialog(rocketpanel); + } + }); + menu.add(item); + + + item = new JMenuItem(trans.get("main.menu.analyze.optimization"), KeyEvent.VK_O); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.optimization.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Rocket optimization selected"); + new GeneralOptimizationDialog(document, BasicFrame.this).setVisible(true); + } + }); + menu.add(item); + + + + //// Debug + // (shown if openrocket.debug.menu is defined) + if (System.getProperty("openrocket.debug.menu") != null) { + menubar.add(makeDebugMenu()); + } + + + + //// Help + + menu = new JMenu(trans.get("main.menu.help")); + menu.setMnemonic(KeyEvent.VK_H); + menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.desc")); + menubar.add(menu); + + + // Guided tours + + item = new JMenuItem(trans.get("main.menu.help.tours"), KeyEvent.VK_L); + // TODO: Icon + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.tours.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Guided tours selected"); + // FIXME: Singleton + new GuidedTourSelectionDialog(BasicFrame.this).setVisible(true); + } + }); + menu.add(item); + + menu.addSeparator(); + + + //// License + item = new JMenuItem(trans.get("main.menu.help.license"), KeyEvent.VK_L); + item.setIcon(Icons.HELP_LICENSE); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.license.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("License selected"); + new LicenseDialog(BasicFrame.this).setVisible(true); + } + }); + menu.add(item); + + menu.addSeparator(); + + //// Bug report + item = new JMenuItem(trans.get("main.menu.help.bugReport"), KeyEvent.VK_B); + item.setIcon(Icons.HELP_BUG_REPORT); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.bugReport.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Bug report selected"); + BugReportDialog.showBugReportDialog(BasicFrame.this); + } + }); + menu.add(item); + + //// Debug log + item = new JMenuItem(trans.get("main.menu.help.debugLog")); + item.setIcon(Icons.HELP_DEBUG_LOG); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.debugLog.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Debug log selected"); + new DebugLogDialog(BasicFrame.this).setVisible(true); + } + }); + menu.add(item); + + menu.addSeparator(); + + //// About + item = new JMenuItem(trans.get("main.menu.help.about"), KeyEvent.VK_A); + item.setIcon(Icons.HELP_ABOUT); + item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.about.desc")); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("About selected"); + new AboutDialog(BasicFrame.this).setVisible(true); + } + }); + menu.add(item); + + + this.setJMenuBar(menubar); + } + + private JMenu makeDebugMenu() { + JMenu menu; + JMenuItem item; + + /* + * This menu is intentionally left untranslated. + */ + + //// Debug menu + menu = new JMenu("Debug"); + //// OpenRocket debugging tasks + menu.getAccessibleContext().setAccessibleDescription("OpenRocket debugging tasks"); + + //// What is this menu? + item = new JMenuItem("What is this menu?"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("What is this menu? selected"); + JOptionPane.showMessageDialog(BasicFrame.this, + new Object[] { + "The 'Debug' menu includes actions for testing and debugging " + + "OpenRocket.", " ", + "The menu is made visible by defining the system property " + + "'openrocket.debug.menu' when starting OpenRocket.", + "It should not be visible by default." }, + "Debug menu", JOptionPane.INFORMATION_MESSAGE); + } + }); + menu.add(item); + + menu.addSeparator(); + + //// Create test rocket + item = new JMenuItem("Create test rocket"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Create test rocket selected"); + JTextField field = new JTextField(); + int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] { + "Input text key to generate random rocket:", + field + }, "Generate random test rocket", JOptionPane.DEFAULT_OPTION, + JOptionPane.QUESTION_MESSAGE, null, new Object[] { + "Random", "OK" + }, "OK"); + + Rocket r; + if (sel == 0) { + r = new TestRockets(null).makeTestRocket(); + } else if (sel == 1) { + r = new TestRockets(field.getText()).makeTestRocket(); + } else { + return; + } + + OpenRocketDocument doc = new OpenRocketDocument(r); + doc.setSaved(true); + BasicFrame frame = new BasicFrame(doc); + frame.setVisible(true); + } + }); + menu.add(item); + + + + item = new JMenuItem("Create 'Iso-Haisu'"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Create Iso-Haisu selected"); + Rocket r = TestRockets.makeIsoHaisu(); + OpenRocketDocument doc = new OpenRocketDocument(r); + doc.setSaved(true); + BasicFrame frame = new BasicFrame(doc); + frame.setVisible(true); + } + }); + menu.add(item); + + + item = new JMenuItem("Create 'Big Blue'"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Create Big Blue selected"); + Rocket r = TestRockets.makeBigBlue(); + OpenRocketDocument doc = new OpenRocketDocument(r); + doc.setSaved(true); + BasicFrame frame = new BasicFrame(doc); + frame.setVisible(true); + } + }); + menu.add(item); + + menu.addSeparator(); + + + item = new JMenuItem("Memory statistics"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Memory statistics selected"); + + // Get discarded but remaining objects (this also runs System.gc multiple times) + List objects = MemoryManagement.getRemainingCollectableObjects(); + StringBuilder sb = new StringBuilder(); + sb.append("Objects that should have been garbage-collected but have not been:\n"); + int count = 0; + for (MemoryData data : objects) { + Object o = data.getReference().get(); + if (o == null) + continue; + sb.append("Age ").append(System.currentTimeMillis() - data.getRegistrationTime()) + .append(" ms: ").append(o).append('\n'); + count++; + // Explicitly null the strong reference to avoid possibility of invisible references + o = null; + } + sb.append("Total: " + count); + + // Get basic memory stats + System.gc(); + long max = Runtime.getRuntime().maxMemory(); + long free = Runtime.getRuntime().freeMemory(); + long used = max - free; + String[] stats = new String[4]; + stats[0] = "Memory usage:"; + stats[1] = String.format(" Max memory: %.1f MB", max / 1024.0 / 1024.0); + stats[2] = String.format(" Used memory: %.1f MB (%.0f%%)", used / 1024.0 / 1024.0, 100.0 * used / max); + stats[3] = String.format(" Free memory: %.1f MB (%.0f%%)", free / 1024.0 / 1024.0, 100.0 * free / max); + + + DetailDialog.showDetailedMessageDialog(BasicFrame.this, stats, sb.toString(), + "Memory statistics", JOptionPane.INFORMATION_MESSAGE); + } + }); + menu.add(item); + + //// Exhaust memory + item = new JMenuItem("Exhaust memory"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Exhaust memory selected"); + LinkedList data = new LinkedList(); + int count = 0; + final int bytesPerArray = 10240; + try { + while (true) { + byte[] array = new byte[bytesPerArray]; + for (int i = 0; i < bytesPerArray; i++) { + array[i] = (byte) i; + } + data.add(array); + count++; + } + } catch (OutOfMemoryError error) { + data = null; + long size = bytesPerArray * (long) count; + String s = String.format("OutOfMemory occurred after %d iterations (approx. %.1f MB consumed)", + count, size / 1024.0 / 1024.0); + log.debug(s, error); + JOptionPane.showMessageDialog(BasicFrame.this, s); + } + } + }); + menu.add(item); + + + menu.addSeparator(); + + //// Exception here + item = new JMenuItem("Exception here"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Exception here selected"); + throw new RuntimeException("Testing exception from menu action listener"); + } + }); + menu.add(item); + + item = new JMenuItem("Exception from EDT"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Exception from EDT selected"); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + throw new RuntimeException("Testing exception from " + + "later invoked EDT thread"); + } + }); + } + }); + menu.add(item); + + item = new JMenuItem("Exception from other thread"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Exception from other thread selected"); + new Thread() { + @Override + public void run() { + throw new RuntimeException("Testing exception from newly created thread"); + } + }.start(); + } + }); + menu.add(item); + + item = new JMenuItem("OutOfMemoryError here"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("OutOfMemoryError here selected"); + throw new OutOfMemoryError("Testing OutOfMemoryError from menu action listener"); + } + }); + menu.add(item); + + + menu.addSeparator(); + + + item = new JMenuItem("Test popup"); + item.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + log.user("Test popup selected"); + JPanel panel = new JPanel(); + panel.add(new JTextField(40)); + panel.add(new JSpinner()); + JPopupMenu popup = new JPopupMenu(); + popup.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); + popup.add(panel); + popup.show(BasicFrame.this, -50, 100); + } + }); + menu.add(item); + + + + + return menu; + } + + + /** + * Select the tab on the main pane. + * + * @param tab one of {@link #COMPONENT_TAB} or {@link #SIMULATION_TAB}. + */ + public void selectTab(int tab) { + tabbedPane.setSelectedIndex(tab); + } + + + + private void openAction() { + JFileChooser chooser = new JFileChooser(); + + chooser.addChoosableFileFilter(FileHelper.ALL_DESIGNS_FILTER); + chooser.addChoosableFileFilter(FileHelper.OPENROCKET_DESIGN_FILTER); + chooser.addChoosableFileFilter(FileHelper.ROCKSIM_DESIGN_FILTER); + chooser.setFileFilter(FileHelper.ALL_DESIGNS_FILTER); + + chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); + chooser.setMultiSelectionEnabled(true); + chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + int option = chooser.showOpenDialog(this); + if (option != JFileChooser.APPROVE_OPTION) { + log.user("Decided not to open files, option=" + option); + return; + } + + ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); + + File[] files = chooser.getSelectedFiles(); + log.user("Opening files " + Arrays.toString(files)); + + for (File file : files) { + log.info("Opening file: " + file); + if (open(file, this)) { + + // Close previous window if replacing + if (replaceable && document.isSaved()) { + // We are replacing the frame, make new window have current location + BasicFrame newFrame = frames.get(frames.size() - 1); + newFrame.setLocation(this.getLocation()); + + log.info("Closing window because it is replaceable"); + closeAction(); + replaceable = false; + } + } + } + } + + + /** + * Open a file based on a URL. + * @param url the file to open. + * @param parent the parent window for dialogs. + * @return true if opened successfully. + */ + private static boolean open(URL url, BasicFrame parent) { + String filename = null; + + // First figure out the file name from the URL + + // Try using URI.getPath(); + try { + URI uri = url.toURI(); + filename = uri.getPath(); + } catch (URISyntaxException ignore) { + } + + // Try URL-decoding the URL + if (filename == null) { + try { + filename = URLDecoder.decode(url.toString(), "UTF-8"); + } catch (UnsupportedEncodingException ignore) { + } + } + + // Last resort + if (filename == null) { + filename = ""; + } + + // Remove path from filename + if (filename.lastIndexOf('/') >= 0) { + filename = filename.substring(filename.lastIndexOf('/') + 1); + } + + + // Open the file + log.info("Opening file from url=" + url + " filename=" + filename); + try { + InputStream is = url.openStream(); + if (open(is, filename, parent)) { + // Close previous window if replacing + if (parent.replaceable && parent.document.isSaved()) { + parent.closeAction(); + parent.replaceable = false; + } + } + } catch (IOException e) { + log.warn("Error opening file" + e); + JOptionPane.showMessageDialog(parent, + "An error occurred while opening the file " + filename, + "Error loading file", JOptionPane.ERROR_MESSAGE); + } + + return false; + } + + + /** + * Open the specified file from an InputStream in a new design frame. If an error + * occurs, an error dialog is shown and false is returned. + * + * @param stream the stream to load from. + * @param filename the file name to display in dialogs (not set to the document). + * @param parent the parent component for which a progress dialog is opened. + * @return whether the file was successfully loaded and opened. + */ + private static boolean open(InputStream stream, String filename, Window parent) { + OpenFileWorker worker = new OpenFileWorker(stream, ROCKET_LOADER); + return open(worker, filename, null, parent); + } + + + /** + * Open the specified file in a new design frame. If an error occurs, an error + * dialog is shown and false is returned. + * + * @param file the file to open. + * @param parent the parent component for which a progress dialog is opened. + * @return whether the file was successfully loaded and opened. + */ + public static boolean open(File file, Window parent) { + OpenFileWorker worker = new OpenFileWorker(file, ROCKET_LOADER); + return open(worker, file.getName(), file, parent); + } + + + /** + * Open the specified file using the provided worker. + * + * @param worker the OpenFileWorker that loads the file. + * @param filename the file name to display in dialogs. + * @param file the File to set the document to (may be null). + * @param parent + * @return + */ + private static boolean open(OpenFileWorker worker, String filename, File file, Window parent) { + + MotorDatabaseLoadingDialog.check(parent); + + // Open the file in a Swing worker thread + log.info("Starting OpenFileWorker"); + if (!SwingWorkerDialog.runWorker(parent, "Opening file", "Reading " + filename + "...", worker)) { + // User cancelled the operation + log.info("User cancelled the OpenFileWorker"); + return false; + } + + + // Handle the document + OpenRocketDocument doc = null; + try { + + doc = worker.get(); + + } catch (ExecutionException e) { + + Throwable cause = e.getCause(); + + if (cause instanceof FileNotFoundException) { + + log.warn("File not found", cause); + JOptionPane.showMessageDialog(parent, + "File not found: " + filename, + "Error opening file", JOptionPane.ERROR_MESSAGE); + return false; + + } else if (cause instanceof RocketLoadException) { + + log.warn("Error loading the file", cause); + JOptionPane.showMessageDialog(parent, + "Unable to open file '" + filename + "': " + + cause.getMessage(), + "Error opening file", JOptionPane.ERROR_MESSAGE); + return false; + + } else { + + throw new BugException("Unknown error when opening file", e); + + } + + } catch (InterruptedException e) { + throw new BugException("EDT was interrupted", e); + } + + if (doc == null) { + throw new BugException("Document loader returned null"); + } + + + // Show warnings + WarningSet warnings = worker.getRocketLoader().getWarnings(); + if (!warnings.isEmpty()) { + log.info("Warnings while reading file: " + warnings); + WarningDialog.showWarnings(parent, + new Object[] { + //// The following problems were encountered while opening + trans.get("BasicFrame.WarningDialog.txt1") + " " + filename + ".", + //// Some design features may not have been loaded correctly. + trans.get("BasicFrame.WarningDialog.txt2") + }, + //// Warnings while opening file + trans.get("BasicFrame.WarningDialog.title"), warnings); + } + + + // Set document state + doc.setFile(file); + doc.setSaved(true); + + + // Open the frame + log.debug("Opening new frame with the document"); + BasicFrame frame = new BasicFrame(doc); + frame.setVisible(true); + + return true; + } + + + + + + private boolean saveAction() { + File file = document.getFile(); + if (file == null) { + log.info("Document does not contain file, opening save as dialog instead"); + return saveAsAction(); + } + log.info("Saving document to " + file); + + // Saving RockSim designs is not supported + if (FileHelper.ROCKSIM_DESIGN_FILTER.accept(file)) { + file = new File(file.getAbsolutePath().replaceAll(".[rR][kK][tT](.[gG][zZ])?$", + ".ork")); + + log.info("Attempting to save in RockSim format, renaming to " + file); + int option = JOptionPane.showConfirmDialog(this, new Object[] { + "Saving designs in RockSim format is not supported.", + "Save in OpenRocket format instead (" + file.getName() + ")?" + }, "Save " + file.getName(), JOptionPane.YES_NO_OPTION, + JOptionPane.QUESTION_MESSAGE, null); + if (option != JOptionPane.YES_OPTION) { + log.user("User chose not to save"); + return false; + } + + document.setFile(file); + } + return saveAs(file); + } + + + private boolean saveAsAction() { + File file = null; + + // TODO: HIGH: what if *.rkt chosen? + StorageOptionChooser storageChooser = + new StorageOptionChooser(document, document.getDefaultStorageOptions()); + JFileChooser chooser = new JFileChooser(); + chooser.addChoosableFileFilter(FileHelper.OPENROCKET_DESIGN_FILTER); + chooser.addChoosableFileFilter(FileHelper.ROCKSIM_DESIGN_FILTER); + chooser.setFileFilter(FileHelper.OPENROCKET_DESIGN_FILTER); + chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); + chooser.setAccessory(storageChooser); + if (document.getFile() != null) + chooser.setSelectedFile(document.getFile()); + + int option = chooser.showSaveDialog(BasicFrame.this); + if (option != JFileChooser.APPROVE_OPTION) { + log.user("User decided not to save, option=" + option); + return false; + } + + file = chooser.getSelectedFile(); + if (file == null) { + log.user("User did not select a file"); + return false; + } + + ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); + storageChooser.storeOptions(document.getDefaultStorageOptions()); + + if (chooser.getFileFilter().equals(FileHelper.OPENROCKET_DESIGN_FILTER)) { + file = FileHelper.ensureExtension(file, "ork"); + if (!FileHelper.confirmWrite(file, this)) { + return false; + } + + return saveAs(file); + } + else if (chooser.getFileFilter().equals(FileHelper.ROCKSIM_DESIGN_FILTER)) { + file = FileHelper.ensureExtension(file, "rkt"); + if (!FileHelper.confirmWrite(file, this)) { + return false; + } + + try { + new RocksimSaver().save(file, document); + return true; + } catch (IOException e) { + return false; + } + } + else { + return false; + } + } + + private boolean saveAs(File file) { + log.info("Saving document as " + file); + boolean saved = false; + + if (!StorageOptionChooser.verifyStorageOptions(document, this)) { + // User cancelled the dialog + log.user("User cancelled saving in storage options dialog"); + return false; + } + + + SaveFileWorker worker = new SaveFileWorker(document, file, ROCKET_SAVER); + + if (!SwingWorkerDialog.runWorker(this, "Saving file", + "Writing " + file.getName() + "...", worker)) { + + // User cancelled the save + log.user("User cancelled the save, deleting the file"); + file.delete(); + return false; + } + + try { + worker.get(); + document.setFile(file); + document.setSaved(true); + saved = true; + setTitle(); + } catch (ExecutionException e) { + + Throwable cause = e.getCause(); + + if (cause instanceof IOException) { + log.warn("An I/O error occurred while saving " + file, cause); + JOptionPane.showMessageDialog(this, new String[] { + "An I/O error occurred while saving:", + e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE); + return false; + } else { + Reflection.handleWrappedException(e); + } + + } catch (InterruptedException e) { + throw new BugException("EDT was interrupted", e); + } + + return saved; + } + + + private boolean closeAction() { + if (!document.isSaved()) { + log.info("Confirming whether to save the design"); + ComponentConfigDialog.hideDialog(); + int result = JOptionPane.showConfirmDialog(this, + trans.get("BasicFrame.dlg.lbl1") + rocket.getName() + + trans.get("BasicFrame.dlg.lbl2") + " " + + trans.get("BasicFrame.dlg.lbl3"), + trans.get("BasicFrame.dlg.title"), JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE); + if (result == JOptionPane.YES_OPTION) { + // Save + log.user("User requested file save"); + if (!saveAction()) { + log.info("File save was interrupted, not closing"); + return false; + } + } else if (result == JOptionPane.NO_OPTION) { + // Don't save: No-op + log.user("User requested to discard design"); + } else { + // Cancel or close + log.user("User cancelled closing, result=" + result); + return false; + } + } + + // Rocket has been saved or discarded + log.debug("Disposing window"); + this.dispose(); + + ComponentConfigDialog.hideDialog(); + ComponentAnalysisDialog.hideDialog(); + + frames.remove(this); + if (frames.isEmpty()) { + log.info("Last frame closed, exiting"); + System.exit(0); + } + return true; + } + + + + /** + * + */ + public void printAction() { + new PrintDialog(this, document).setVisible(true); + } + + /** + * Open a new design window with a basic rocket+stage. + */ + public static void newAction() { + log.info("New action initiated"); + + Rocket rocket = new Rocket(); + Stage stage = new Stage(); + //// Sustainer + stage.setName(trans.get("BasicFrame.StageName.Sustainer")); + rocket.addChild(stage); + OpenRocketDocument doc = new OpenRocketDocument(rocket); + doc.setSaved(true); + + BasicFrame frame = new BasicFrame(doc); + frame.replaceable = true; + frame.setVisible(true); + ComponentConfigDialog.showDialog(frame, doc, rocket); + } + + /** + * Quit the application. Confirms saving unsaved designs. The action of File->Quit. + */ + public static void quitAction() { + log.info("Quit action initiated"); + for (int i = frames.size() - 1; i >= 0; i--) { + log.debug("Closing frame " + frames.get(i)); + if (!frames.get(i).closeAction()) { + // Close canceled + log.info("Quit was cancelled"); + return; + } + } + // Should not be reached, but just in case + log.error("Should already have exited application"); + System.exit(0); + } + + + /** + * Set the title of the frame, taking into account the name of the rocket, file it + * has been saved to (if any) and saved status. + */ + private void setTitle() { + File file = document.getFile(); + boolean saved = document.isSaved(); + String title; + + title = rocket.getName(); + if (file != null) { + title = title + " (" + file.getName() + ")"; + } + if (!saved) + title = "*" + title; + + setTitle(title); + } + + + + /** + * Find a currently open BasicFrame containing the specified rocket. This method + * can be used to map a Rocket to a BasicFrame from GUI methods. + * + * @param rocket the Rocket. + * @return the corresponding BasicFrame, or null if none found. + */ + public static BasicFrame findFrame(Rocket rocket) { + for (BasicFrame f : frames) { + if (f.rocket == rocket) { + log.debug("Found frame " + f + " for rocket " + rocket); + return f; + } + } + log.debug("Could not find frame for rocket " + rocket); + return null; + } + + /** + * Find a currently open document by the rocket object. This method can be used + * to map a Rocket to OpenRocketDocument from GUI methods. + * + * @param rocket the Rocket. + * @return the corresponding OpenRocketDocument, or null if not found. + */ + public static OpenRocketDocument findDocument(Rocket rocket) { + BasicFrame frame = findFrame(rocket); + if (frame != null) { + return frame.document; + } else { + return null; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/main/ClipboardListener.java b/core/src/net/sf/openrocket/gui/main/ClipboardListener.java new file mode 100644 index 00000000..87fd4456 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/ClipboardListener.java @@ -0,0 +1,7 @@ +package net.sf.openrocket.gui.main; + +public interface ClipboardListener { + + public void clipboardChanged(); + +} diff --git a/core/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/core/src/net/sf/openrocket/gui/main/ComponentAddButtons.java new file mode 100644 index 00000000..fa92a671 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/ComponentAddButtons.java @@ -0,0 +1,651 @@ +package net.sf.openrocket.gui.main; + + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JViewport; +import javax.swing.Scrollable; +import javax.swing.SwingConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.BodyComponent; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.ShockCord; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Pair; +import net.sf.openrocket.util.Reflection; + +/** + * A component that contains addition buttons to add different types of rocket components + * to a rocket. It enables and disables buttons according to the current selection of a + * TreeSelectionModel. + * + * @author Sampo Niskanen + */ + +public class ComponentAddButtons extends JPanel implements Scrollable { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private static final int ROWS = 3; + private static final int MAXCOLS = 6; + private static final String BUTTONPARAM = "grow, sizegroup buttons"; + + private static final int GAP = 5; + private static final int EXTRASPACE = 0; + + private final ComponentButton[][] buttons; + + private final OpenRocketDocument document; + private final TreeSelectionModel selectionModel; + private final JViewport viewport; + private final MigLayout layout; + + private final int width, height; + + + public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model, + JViewport viewport) { + + super(); + String constaint = "[min!]"; + for (int i = 1; i < MAXCOLS; i++) + constaint = constaint + GAP + "[min!]"; + + layout = new MigLayout("fill", constaint); + setLayout(layout); + this.document = document; + this.selectionModel = model; + this.viewport = viewport; + + buttons = new ComponentButton[ROWS][]; + int row = 0; + + //////////////////////////////////////////// + + //// Body components and fin sets + addButtonRow(trans.get("compaddbuttons.Bodycompandfinsets"), row, + //// Nose cone + new BodyComponentButton(NoseCone.class, trans.get("compaddbuttons.Nosecone")), + //// Body tube + new BodyComponentButton(BodyTube.class, trans.get("compaddbuttons.Bodytube")), + //// Transition + new BodyComponentButton(Transition.class, trans.get("compaddbuttons.Transition")), + //// Trapezoidal + new FinButton(TrapezoidFinSet.class, trans.get("compaddbuttons.Trapezoidal")), // TODO: MEDIUM: freer fin placing + //// Elliptical + new FinButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")), + //// Freeform + new FinButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")), + //// Launch lug + new FinButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug"))); + + row++; + + + ///////////////////////////////////////////// + + //// Inner component + addButtonRow(trans.get("compaddbuttons.Innercomponent"), row, + //// Inner tube + new ComponentButton(InnerTube.class, trans.get("compaddbuttons.Innertube")), + //// Coupler + new ComponentButton(TubeCoupler.class, trans.get("compaddbuttons.Coupler")), + //// Centering\nring + new ComponentButton(CenteringRing.class, trans.get("compaddbuttons.Centeringring")), + //// Bulkhead + new ComponentButton(Bulkhead.class, trans.get("compaddbuttons.Bulkhead")), + //// Engine\nblock + new ComponentButton(EngineBlock.class, trans.get("compaddbuttons.Engineblock"))); + + row++; + + //////////////////////////////////////////// + + //// Mass objects + addButtonRow(trans.get("compaddbuttons.Massobjects"), row, + //// Parachute + new ComponentButton(Parachute.class, trans.get("compaddbuttons.Parachute")), + //// Streamer + new ComponentButton(Streamer.class, trans.get("compaddbuttons.Streamer")), + //// Shock cord + new ComponentButton(ShockCord.class, trans.get("compaddbuttons.Shockcord")), + // new ComponentButton("Motor clip"), + // new ComponentButton("Payload"), + //// Mass\ncomponent + new ComponentButton(MassComponent.class, trans.get("compaddbuttons.Masscomponent"))); + + + // Get maximum button size + int w = 0, h = 0; + + for (row = 0; row < buttons.length; row++) { + for (int col = 0; col < buttons[row].length; col++) { + Dimension d = buttons[row][col].getPreferredSize(); + if (d.width > w) + w = d.width; + if (d.height > h) + h = d.height; + } + } + + // Set all buttons to maximum size + width = w; + height = h; + Dimension d = new Dimension(width, height); + for (row = 0; row < buttons.length; row++) { + for (int col = 0; col < buttons[row].length; col++) { + buttons[row][col].setMinimumSize(d); + buttons[row][col].setPreferredSize(d); + buttons[row][col].getComponent(0).validate(); + } + } + + // Add viewport listener if viewport provided + if (viewport != null) { + viewport.addChangeListener(new ChangeListener() { + private int oldWidth = -1; + + public void stateChanged(ChangeEvent e) { + Dimension d = ComponentAddButtons.this.viewport.getExtentSize(); + if (d.width != oldWidth) { + oldWidth = d.width; + flowButtons(); + } + } + }); + } + + add(new JPanel(), "grow"); + } + + + /** + * Adds a row of buttons to the panel. + * @param label Label placed before the row + * @param row Row number + * @param b List of ComponentButtons to place on the row + */ + private void addButtonRow(String label, int row, ComponentButton... b) { + if (row > 0) + add(new JLabel(label), "span, gaptop unrel, wrap"); + else + add(new JLabel(label), "span, gaptop 0, wrap"); + + int col = 0; + buttons[row] = new ComponentButton[b.length]; + + for (int i = 0; i < b.length; i++) { + buttons[row][col] = b[i]; + if (i < b.length - 1) + add(b[i], BUTTONPARAM); + else + add(b[i], BUTTONPARAM + ", wrap"); + col++; + } + } + + + /** + * Flows the buttons in all rows of the panel. If a button would come too close + * to the right edge of the viewport, "newline" is added to its constraints flowing + * it to the next line. + */ + private void flowButtons() { + if (viewport == null) + return; + + int w; + + Dimension d = viewport.getExtentSize(); + + for (int row = 0; row < buttons.length; row++) { + w = 0; + for (int col = 0; col < buttons[row].length; col++) { + w += GAP + width; + String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!"; + + if (w + EXTRASPACE > d.width) { + param = param + ",newline"; + w = GAP + width; + } + if (col == buttons[row].length - 1) + param = param + ",wrap"; + layout.setComponentConstraints(buttons[row][col], param); + } + } + revalidate(); + } + + + + /** + * Class for a component button. + */ + private class ComponentButton extends JButton implements TreeSelectionListener { + protected Class componentClass = null; + private Constructor constructor = null; + + /** Only label, no icon. */ + public ComponentButton(String text) { + this(text, null, null); + } + + /** + * Constructor with icon and label. The icon and label are placed into the button. + * The label may contain "\n" as a newline. + */ + public ComponentButton(String text, Icon enabled, Icon disabled) { + super(); + setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", "")); + + add(new JLabel(), "push, sizegroup spacing"); + + // Add Icon + if (enabled != null) { + JLabel label = new JLabel(enabled); + if (disabled != null) + label.setDisabledIcon(disabled); + add(label, "growx"); + } + + // Add labels + String[] l = text.split("\n"); + for (int i = 0; i < l.length; i++) { + add(new StyledLabel(l[i], SwingConstants.CENTER, -3.0f), "growx"); + } + + add(new JLabel(), "push, sizegroup spacing"); + + valueChanged(null); // Update enabled status + selectionModel.addTreeSelectionListener(this); + } + + + /** + * Main constructor that should be used. The generated component type is specified + * and the text. The icons are fetched based on the component type. + */ + public ComponentButton(Class c, String text) { + this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c)); + + if (c == null) + return; + + componentClass = c; + + try { + constructor = c.getConstructor(); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Unable to get default " + + "constructor for class " + c, e); + } + } + + + /** + * Return whether the current component is addable when the component c is selected. + * c is null if there is no selection. The default is to use c.isCompatible(class). + */ + public boolean isAddable(RocketComponent c) { + if (c == null) + return false; + if (componentClass == null) + return false; + return c.isCompatible(componentClass); + } + + /** + * Return the position to add the component if component c is selected currently. + * The first element of the returned array is the RocketComponent to add the component + * to, and the second (if non-null) an Integer telling the position of the component. + * A return value of null means that the user cancelled addition of the component. + * If the Integer is null, the component is added at the end of the sibling + * list. By default returns the end of the currently selected component. + * + * @param c The component currently selected + * @return The position to add the new component to, or null if should not add. + */ + public Pair getAdditionPosition(RocketComponent c) { + return new Pair(c, null); + } + + /** + * Updates the enabled status of the button. + * TODO: LOW: What about updates to the rocket tree? + */ + public void valueChanged(TreeSelectionEvent e) { + updateEnabled(); + } + + /** + * Sets the enabled status of the button and all subcomponents. + */ + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + Component[] c = getComponents(); + for (int i = 0; i < c.length; i++) + c[i].setEnabled(enabled); + } + + + /** + * Update the enabled status of the button. + */ + private void updateEnabled() { + RocketComponent c = null; + TreePath p = selectionModel.getSelectionPath(); + if (p != null) + c = (RocketComponent) p.getLastPathComponent(); + setEnabled(isAddable(c)); + } + + + @Override + protected void fireActionPerformed(ActionEvent event) { + super.fireActionPerformed(event); + log.user("Adding component of type " + componentClass.getSimpleName()); + RocketComponent c = null; + Integer position = null; + + TreePath p = selectionModel.getSelectionPath(); + if (p != null) + c = (RocketComponent) p.getLastPathComponent(); + + Pair pos = getAdditionPosition(c); + if (pos == null) { + // Cancel addition + log.info("No position to add component"); + return; + } + c = pos.getU(); + position = pos.getV(); + + + if (c == null) { + // Should not occur + Application.getExceptionHandler().handleErrorCondition("ERROR: Could not place new component."); + updateEnabled(); + return; + } + + if (constructor == null) { + Application.getExceptionHandler().handleErrorCondition("ERROR: Construction of type not supported yet."); + return; + } + + RocketComponent component; + try { + component = (RocketComponent) constructor.newInstance(); + } catch (InstantiationException e) { + throw new BugException("Could not construct new instance of class " + constructor, e); + } catch (IllegalAccessException e) { + throw new BugException("Could not construct new instance of class " + constructor, e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + + // Next undo position is set by opening the configuration dialog + document.addUndoPosition("Add " + component.getComponentName()); + + log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() + + " position=" + position); + + if (position == null) + c.addChild(component); + else + c.addChild(component, position); + + // Select new component and open config dialog + selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component)); + + JFrame parent = null; + for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) { + if (comp instanceof JFrame) { + parent = (JFrame) comp; + break; + } + } + + ComponentConfigDialog.showDialog(parent, document, component); + } + } + + /** + * A class suitable for BodyComponents. Addition is allowed ... + */ + private class BodyComponentButton extends ComponentButton { + + public BodyComponentButton(Class c, String text) { + super(c, text); + } + + public BodyComponentButton(String text, Icon enabled, Icon disabled) { + super(text, enabled, disabled); + } + + public BodyComponentButton(String text) { + super(text); + } + + @Override + public boolean isAddable(RocketComponent c) { + if (super.isAddable(c)) + return true; + // Handled separately: + if (c instanceof BodyComponent) + return true; + if (c == null || c instanceof Rocket) + return true; + return false; + } + + @Override + public Pair getAdditionPosition(RocketComponent c) { + if (super.isAddable(c)) // Handled automatically + return super.getAdditionPosition(c); + + + if (c == null || c instanceof Rocket) { + // Add as last body component of the last stage + Rocket rocket = document.getRocket(); + return new Pair(rocket.getChild(rocket.getStageCount() - 1), + null); + } + + if (!(c instanceof BodyComponent)) + return null; + RocketComponent parent = c.getParent(); + if (parent == null) { + throw new BugException("Component " + c.getComponentName() + " is the root component, " + + "componentClass=" + componentClass); + } + + // Check whether to insert between or at the end. + // 0 = ask, 1 = in between, 2 = at the end + int pos = Application.getPreferences().getChoice(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0); + if (pos == 0) { + if (parent.getChildPosition(c) == parent.getChildCount() - 1) + pos = 2; // Selected component is the last component + else + pos = askPosition(); + } + + switch (pos) { + case 0: + // Cancel + return null; + case 1: + // Insert after current position + return new Pair(parent, parent.getChildPosition(c) + 1); + case 2: + // Insert at the end of the parent + return new Pair(parent, null); + default: + Application.getExceptionHandler().handleErrorCondition("ERROR: Bad position type: " + pos); + return null; + } + } + + private int askPosition() { + //// Insert here + //// Add to the end + //// Cancel + Object[] options = { trans.get("compaddbuttons.askPosition.Inserthere"), + trans.get("compaddbuttons.askPosition.Addtotheend"), + trans.get("compaddbuttons.askPosition.Cancel") }; + + JPanel panel = new JPanel(new MigLayout()); + //// Do not ask me again + JCheckBox check = new JCheckBox(trans.get("compaddbuttons.Donotaskmeagain")); + panel.add(check, "wrap"); + //// You can change the default operation in the preferences. + panel.add(new StyledLabel(trans.get("compaddbuttons.lbl.Youcanchange"), -2)); + + int sel = JOptionPane.showOptionDialog(null, // parent component + //// Insert the component after the current component or as the last component? + new Object[] { + trans.get("compaddbuttons.lbl.insertcomp"), + panel }, + //// Select component position + trans.get("compaddbuttons.Selectcomppos"), // title + JOptionPane.DEFAULT_OPTION, // default selections + JOptionPane.QUESTION_MESSAGE, // dialog type + null, // icon + options, // options + options[0]); // initial value + + switch (sel) { + case JOptionPane.CLOSED_OPTION: + case 2: + // Cancel + return 0; + case 0: + // Insert + sel = 1; + break; + case 1: + // Add + sel = 2; + break; + default: + Application.getExceptionHandler().handleErrorCondition("ERROR: JOptionPane returned " + sel); + return 0; + } + + if (check.isSelected()) { + // Save the preference + Application.getPreferences().putInt(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, sel); + } + return sel; + } + + } + + + + /** + * Class for fin sets, that attach only to BodyTubes. + */ + private class FinButton extends ComponentButton { + public FinButton(Class c, String text) { + super(c, text); + } + + public FinButton(String text, Icon enabled, Icon disabled) { + super(text, enabled, disabled); + } + + public FinButton(String text) { + super(text); + } + + @Override + public boolean isAddable(RocketComponent c) { + if (c == null) + return false; + return (c.getClass().equals(BodyTube.class)); + } + } + + + + ///////// Scrolling functionality + + @Override + public Dimension getPreferredScrollableViewportSize() { + return getPreferredSize(); + } + + + @Override + public int getScrollableBlockIncrement(Rectangle visibleRect, + int orientation, int direction) { + if (orientation == SwingConstants.VERTICAL) + return visibleRect.height * 8 / 10; + return 10; + } + + + @Override + public boolean getScrollableTracksViewportHeight() { + return false; + } + + + @Override + public boolean getScrollableTracksViewportWidth() { + return true; + } + + + @Override + public int getScrollableUnitIncrement(Rectangle visibleRect, + int orientation, int direction) { + return 10; + } + +} diff --git a/core/src/net/sf/openrocket/gui/main/ComponentIcons.java b/core/src/net/sf/openrocket/gui/main/ComponentIcons.java new file mode 100644 index 00000000..b0071caa --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/ComponentIcons.java @@ -0,0 +1,183 @@ +package net.sf.openrocket.gui.main; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.util.HashMap; + +import javax.imageio.ImageIO; +import javax.swing.Icon; +import javax.swing.ImageIcon; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.EngineBlock; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.ShockCord; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.startup.Application; + + +public class ComponentIcons { + private static final Translator trans = Application.getTranslator(); + + private static final String ICON_DIRECTORY = "pix/componenticons/"; + private static final String SMALL_SUFFIX = "-small.png"; + private static final String LARGE_SUFFIX = "-large.png"; + + private static final HashMap, ImageIcon> SMALL_ICONS = + new HashMap, ImageIcon>(); + private static final HashMap, ImageIcon> LARGE_ICONS = + new HashMap, ImageIcon>(); + private static final HashMap, ImageIcon> DISABLED_ICONS = + new HashMap, ImageIcon>(); + + static { + //// Nose cone + load("nosecone", trans.get("ComponentIcons.Nosecone"), NoseCone.class); + //// Body tube + load("bodytube", trans.get("ComponentIcons.Bodytube"), BodyTube.class); + //// Transition + load("transition", trans.get("ComponentIcons.Transition"), Transition.class); + //// Trapezoidal fin set + load("trapezoidfin", trans.get("ComponentIcons.Trapezoidalfinset"), TrapezoidFinSet.class); + //// Elliptical fin set + load("ellipticalfin", trans.get("ComponentIcons.Ellipticalfinset"), EllipticalFinSet.class); + //// Freeform fin set + load("freeformfin", trans.get("ComponentIcons.Freeformfinset"), FreeformFinSet.class); + //// Launch lug + load("launchlug", trans.get("ComponentIcons.Launchlug"), LaunchLug.class); + //// Inner tube + load("innertube", trans.get("ComponentIcons.Innertube"), InnerTube.class); + //// Tube coupler + load("tubecoupler", trans.get("ComponentIcons.Tubecoupler"), TubeCoupler.class); + //// Centering ring + load("centeringring", trans.get("ComponentIcons.Centeringring"), CenteringRing.class); + //// Bulk head + load("bulkhead", trans.get("ComponentIcons.Bulkhead"), Bulkhead.class); + //// Engine block + load("engineblock", trans.get("ComponentIcons.Engineblock"), EngineBlock.class); + //// Parachute + load("parachute", trans.get("ComponentIcons.Parachute"), Parachute.class); + //// Streamer + load("streamer", trans.get("ComponentIcons.Streamer"), Streamer.class); + //// Shock cord + load("shockcord", trans.get("ComponentIcons.Shockcord"), ShockCord.class); + //// Mass component + load("mass", trans.get("ComponentIcons.Masscomponent"), MassComponent.class); + } + + private static void load(String filename, String name, Class componentClass) { + ImageIcon icon = loadSmall(ICON_DIRECTORY + filename + SMALL_SUFFIX, name); + SMALL_ICONS.put(componentClass, icon); + + ImageIcon[] icons = loadLarge(ICON_DIRECTORY + filename + LARGE_SUFFIX, name); + LARGE_ICONS.put(componentClass, icons[0]); + DISABLED_ICONS.put(componentClass, icons[1]); + } + + + /** + * Return the small icon for a component type. + * + * @param c the component class. + * @return the icon, or null if none available. + */ + public static Icon getSmallIcon(Class c) { + return SMALL_ICONS.get(c); + } + + /** + * Return the large icon for a component type. + * + * @param c the component class. + * @return the icon, or null if none available. + */ + public static Icon getLargeIcon(Class c) { + return LARGE_ICONS.get(c); + } + + /** + * Return the large disabled icon for a component type. + * + * @param c the component class. + * @return the icon, or null if none available. + */ + public static Icon getLargeDisabledIcon(Class c) { + return DISABLED_ICONS.get(c); + } + + + + + private static ImageIcon loadSmall(String file, String desc) { + URL url = ClassLoader.getSystemResource(file); + if (url == null) { + Application.getExceptionHandler().handleErrorCondition("ERROR: Couldn't find file: " + file); + return null; + } + return new ImageIcon(url, desc); + } + + + private static ImageIcon[] loadLarge(String file, String desc) { + ImageIcon[] icons = new ImageIcon[2]; + + URL url = ClassLoader.getSystemResource(file); + if (url != null) { + BufferedImage bi, bi2; + try { + bi = ImageIO.read(url); + bi2 = ImageIO.read(url); // How the fsck can one duplicate a BufferedImage??? + } catch (IOException e) { + Application.getExceptionHandler().handleErrorCondition("ERROR: Couldn't read file: " + file, e); + return new ImageIcon[] { null, null }; + } + + icons[0] = new ImageIcon(bi, desc); + + // Create disabled icon + if (false) { // Fade using alpha + + int rgb[] = bi2.getRGB(0, 0, bi2.getWidth(), bi2.getHeight(), null, 0, bi2.getWidth()); + for (int i = 0; i < rgb.length; i++) { + final int alpha = (rgb[i] >> 24) & 0xFF; + rgb[i] = (rgb[i] & 0xFFFFFF) | (alpha / 3) << 24; + + //rgb[i] = (rgb[i]&0xFFFFFF) | ((rgb[i]>>1)&0x3F000000); + } + bi2.setRGB(0, 0, bi2.getWidth(), bi2.getHeight(), rgb, 0, bi2.getWidth()); + + } else { // Raster alpha + + for (int x = 0; x < bi.getWidth(); x++) { + for (int y = 0; y < bi.getHeight(); y++) { + if ((x + y) % 2 == 0) { + bi2.setRGB(x, y, 0); + } + } + } + + } + + //// (disabled) + icons[1] = new ImageIcon(bi2, desc + " " + trans.get("ComponentIcons.disabled")); + + return icons; + } else { + Application.getExceptionHandler().handleErrorCondition("ERROR: Couldn't find file: " + file); + return new ImageIcon[] { null, null }; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/main/DocumentSelectionListener.java b/core/src/net/sf/openrocket/gui/main/DocumentSelectionListener.java new file mode 100644 index 00000000..055f1116 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/DocumentSelectionListener.java @@ -0,0 +1,15 @@ +package net.sf.openrocket.gui.main; + +public interface DocumentSelectionListener { + + public static final int COMPONENT_SELECTION_CHANGE = 1; + public static final int SIMULATION_SELECTION_CHANGE = 2; + + /** + * Called when the selection changes. + * + * @param changeType a bitmask of the type of change. + */ + public void valueChanged(int changeType); + +} diff --git a/core/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java b/core/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java new file mode 100644 index 00000000..409620cb --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java @@ -0,0 +1,211 @@ +package net.sf.openrocket.gui.main; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.swing.ListSelectionModel; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; +import net.sf.openrocket.rocketcomponent.RocketComponent; + +public class DocumentSelectionModel { + + private static final Simulation[] NO_SIMULATION = new Simulation[0]; + + private final ComponentTreeSelectionListener componentTreeSelectionListener = + new ComponentTreeSelectionListener(); + private final SimulationListSelectionListener simulationListSelectionListener = + new SimulationListSelectionListener(); + + + private final OpenRocketDocument document; + + private RocketComponent componentSelection = null; + private Simulation[] simulationSelection = NO_SIMULATION; + + private TreeSelectionModel componentTreeSelectionModel = null; + private ListSelectionModel simulationListSelectionModel = null; + + private final List listeners = + new ArrayList(); + + + + public DocumentSelectionModel(OpenRocketDocument document) { + this.document = document; + } + + + + + /** + * Return the currently selected simulations. Returns an empty array if none + * are selected. + * + * @return an array of the currently selected simulations, may be of zero length. + */ + public Simulation[] getSelectedSimulations() { + return Arrays.copyOf(simulationSelection, simulationSelection.length); + } + + public void setSelectedSimulations(Simulation[] sims) { + simulationSelection = sims; + clearComponentSelection(); + + simulationListSelectionModel.clearSelection(); + for (Simulation s: sims) { + int index = document.getSimulationIndex(s); + if (index >= 0) { + simulationListSelectionModel.addSelectionInterval(index, index); + } + } + } + + /** + * Return the currently selected rocket component. Returns null + * if no rocket component is selected. + * + * @return the currently selected rocket component, or null. + */ + public RocketComponent getSelectedComponent() { + return componentSelection; + } + + public void setSelectedComponent(RocketComponent component) { + componentSelection = component; + clearSimulationSelection(); + + TreePath path = ComponentTreeModel.makeTreePath(component); + componentTreeSelectionModel.setSelectionPath(path); + } + + + + + + public void attachComponentTreeSelectionModel(TreeSelectionModel model) { + if (componentTreeSelectionModel != null) + componentTreeSelectionModel.removeTreeSelectionListener( + componentTreeSelectionListener); + + componentTreeSelectionModel = model; + if (model != null) + model.addTreeSelectionListener(componentTreeSelectionListener); + clearComponentSelection(); + } + + + + public void attachSimulationListSelectionModel(ListSelectionModel model) { + if (simulationListSelectionModel != null) + simulationListSelectionModel.removeListSelectionListener( + simulationListSelectionListener); + + simulationListSelectionModel = model; + if (model != null) + model.addListSelectionListener(simulationListSelectionListener); + clearSimulationSelection(); + } + + + + public void clearSimulationSelection() { + if (simulationSelection.length == 0) + return; + + simulationSelection = NO_SIMULATION; + if (simulationListSelectionModel != null) + simulationListSelectionModel.clearSelection(); + + fireDocumentSelection(DocumentSelectionListener.SIMULATION_SELECTION_CHANGE); + } + + + public void clearComponentSelection() { + if (componentSelection == null) + return; + + componentSelection = null; + if (componentTreeSelectionModel != null) + componentTreeSelectionModel.clearSelection(); + + fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE); + } + + + + public void addDocumentSelectionListener(DocumentSelectionListener l) { + listeners.add(l); + } + + public void removeDocumentSelectionListener(DocumentSelectionListener l) { + listeners.remove(l); + } + + protected void fireDocumentSelection(int type) { + DocumentSelectionListener[] array = + listeners.toArray(new DocumentSelectionListener[0]); + + for (DocumentSelectionListener l: array) { + l.valueChanged(type); + } + } + + + + private class ComponentTreeSelectionListener implements TreeSelectionListener { + + @Override + public void valueChanged(TreeSelectionEvent e) { + TreePath path = componentTreeSelectionModel.getSelectionPath(); + if (path == null) { + componentSelection = null; + fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE); + return; + } + + componentSelection = (RocketComponent)path.getLastPathComponent(); + + clearSimulationSelection(); + fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE); + } + + } + + private class SimulationListSelectionListener implements ListSelectionListener { + + @Override + public void valueChanged(ListSelectionEvent e) { + int min = simulationListSelectionModel.getMinSelectionIndex(); + int max = simulationListSelectionModel.getMaxSelectionIndex(); + if (min < 0 || max < 0) { + simulationSelection = NO_SIMULATION; + fireDocumentSelection(DocumentSelectionListener.SIMULATION_SELECTION_CHANGE); + return; + } + + ArrayList list = new ArrayList(); + for (int i = min; i <= max; i++) { + if (simulationListSelectionModel.isSelectedIndex(i) && + (i < document.getSimulationCount())) { + list.add(document.getSimulation(i)); + } + } + simulationSelection = list.toArray(NO_SIMULATION); + + clearComponentSelection(); + fireDocumentSelection(DocumentSelectionListener.SIMULATION_SELECTION_CHANGE); + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java b/core/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java new file mode 100644 index 00000000..5541efb0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java @@ -0,0 +1,70 @@ +package net.sf.openrocket.gui.main; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.rocketcomponent.RocketComponent; + +public final class OpenRocketClipboard { + + private static RocketComponent clipboardComponent = null; + private static Simulation[] clipboardSimulations = null; + + private static List listeners = new ArrayList(); + + private OpenRocketClipboard() { + // Disallow instantiation + } + + + /** + * Return the RocketComponent contained in the clipboard, or + * null. The component is returned verbatim, and must be copied + * before attaching to any rocket design! + * + * @return the rocket component contained in the clipboard, or null + * if the clipboard does not currently contain a rocket component. + */ + public static RocketComponent getClipboardComponent() { + return clipboardComponent; + } + + + public static void setClipboard(RocketComponent component) { + clipboardComponent = component; + clipboardSimulations = null; + fireClipboardChanged(); + } + + + public static Simulation[] getClipboardSimulations() { + if (clipboardSimulations == null || clipboardSimulations.length == 0) + return null; + return clipboardSimulations.clone(); + } + + public static void setClipboard(Simulation[] simulations) { + clipboardSimulations = simulations.clone(); + clipboardComponent = null; + fireClipboardChanged(); + } + + + + public static void addClipboardListener(ClipboardListener listener) { + listeners.add(listener); + } + + public static void removeClipboardListener(ClipboardListener listener) { + listeners.remove(listener); + } + + private static void fireClipboardChanged() { + ClipboardListener[] array = listeners.toArray(new ClipboardListener[0]); + for (ClipboardListener l: array) { + l.clipboardChanged(); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/main/RocketActions.java b/core/src/net/sf/openrocket/gui/main/RocketActions.java new file mode 100644 index 00000000..a8da9c7e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/RocketActions.java @@ -0,0 +1,705 @@ +package net.sf.openrocket.gui.main; + + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.ArrayList; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JCheckBox; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.KeyStroke; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.util.Pair; + + + +/** + * A class that holds Actions for common rocket and simulation operations such as + * cut/copy/paste/delete etc. + * + * @author Sampo Niskanen + */ +public class RocketActions { + + public static final KeyStroke CUT_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_X, + ActionEvent.CTRL_MASK); + public static final KeyStroke COPY_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_C, + ActionEvent.CTRL_MASK); + public static final KeyStroke PASTE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_V, + ActionEvent.CTRL_MASK); + + private final OpenRocketDocument document; + private final Rocket rocket; + private final BasicFrame parentFrame; + private final DocumentSelectionModel selectionModel; + + + private final RocketAction deleteComponentAction; + private final RocketAction deleteSimulationAction; + private final RocketAction deleteAction; + private final RocketAction cutAction; + private final RocketAction copyAction; + private final RocketAction pasteAction; + private final RocketAction editAction; + private final RocketAction newStageAction; + private final RocketAction moveUpAction; + private final RocketAction moveDownAction; + private static final Translator trans = Application.getTranslator(); + + + public RocketActions(OpenRocketDocument document, DocumentSelectionModel selectionModel, + BasicFrame parentFrame) { + this.document = document; + this.rocket = document.getRocket(); + this.selectionModel = selectionModel; + this.parentFrame = parentFrame; + + // Add action also to updateActions() + this.deleteAction = new DeleteAction(); + this.deleteComponentAction = new DeleteComponentAction(); + this.deleteSimulationAction = new DeleteSimulationAction(); + this.cutAction = new CutAction(); + this.copyAction = new CopyAction(); + this.pasteAction = new PasteAction(); + this.editAction = new EditAction(); + this.newStageAction = new NewStageAction(); + this.moveUpAction = new MoveUpAction(); + this.moveDownAction = new MoveDownAction(); + + OpenRocketClipboard.addClipboardListener(this.pasteAction); + updateActions(); + + // Update all actions when tree selection or rocket changes + + selectionModel.addDocumentSelectionListener(new DocumentSelectionListener() { + @Override + public void valueChanged(int changeType) { + updateActions(); + } + }); + document.getRocket().addComponentChangeListener(new ComponentChangeListener() { + @Override + public void componentChanged(ComponentChangeEvent e) { + updateActions(); + } + }); + } + + /** + * Update the state of all of the actions. + */ + private void updateActions() { + deleteAction.clipboardChanged(); + cutAction.clipboardChanged(); + copyAction.clipboardChanged(); + pasteAction.clipboardChanged(); + editAction.clipboardChanged(); + newStageAction.clipboardChanged(); + moveUpAction.clipboardChanged(); + moveDownAction.clipboardChanged(); + } + + + + + public Action getDeleteComponentAction() { + return deleteAction; + } + + public Action getDeleteSimulationAction() { + return deleteAction; + } + + public Action getDeleteAction() { + return deleteAction; + } + + public Action getCutAction() { + return cutAction; + } + + public Action getCopyAction() { + return copyAction; + } + + public Action getPasteAction() { + return pasteAction; + } + + public Action getEditAction() { + return editAction; + } + + public Action getNewStageAction() { + return newStageAction; + } + + public Action getMoveUpAction() { + return moveUpAction; + } + + public Action getMoveDownAction() { + return moveDownAction; + } + + + + //////// Helper methods for the actions + + private boolean isDeletable(RocketComponent c) { + // Sanity check + if (c == null || c.getParent() == null) + return false; + + // Cannot remove Rocket + if (c instanceof Rocket) + return false; + + // Cannot remove last stage + if ((c instanceof Stage) && (c.getParent().getChildCount() == 1)) { + return false; + } + + return true; + } + + private void delete(RocketComponent c) { + if (!isDeletable(c)) { + throw new IllegalArgumentException("Report bug! Component " + c + + " not deletable."); + } + + RocketComponent parent = c.getParent(); + parent.removeChild(c); + } + + private boolean isCopyable(RocketComponent c) { + if (c==null) + return false; + if (c instanceof Rocket) + return false; + return true; + } + + + private boolean isSimulationSelected() { + Simulation[] selection = selectionModel.getSelectedSimulations(); + return (selection != null && selection.length > 0); + } + + + + private boolean verifyDeleteSimulation() { + boolean verify = Application.getPreferences().getBoolean(Preferences.CONFIRM_DELETE_SIMULATION, true); + if (verify) { + JPanel panel = new JPanel(new MigLayout()); + //// Do not ask me again + JCheckBox dontAsk = new JCheckBox(trans.get("RocketActions.checkbox.Donotaskmeagain")); + panel.add(dontAsk,"wrap"); + //// You can change the default operation in the preferences. + panel.add(new StyledLabel(trans.get("RocketActions.lbl.Youcanchangedefop"),-2)); + + int ret = JOptionPane.showConfirmDialog( + parentFrame, + new Object[] { + //// Delete the selected simulations? + trans.get("RocketActions.showConfirmDialog.lbl1"), + //// This operation cannot be undone. + trans.get("RocketActions.showConfirmDialog.lbl2"), + "", + panel }, + //// Delete simulations + trans.get("RocketActions.showConfirmDialog.title"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + if (ret != JOptionPane.OK_OPTION) + return false; + + if (dontAsk.isSelected()) { + Application.getPreferences().putBoolean(Preferences.CONFIRM_DELETE_SIMULATION, false); + } + } + + return true; + } + + + /** + * Return the component and position to which the current clipboard + * should be pasted. Returns null if the clipboard is empty or if the + * clipboard cannot be pasted to the current selection. + * + * @param clipboard the component on the clipboard. + * @return a Pair with both components defined, or null. + */ + private Pair getPastePosition(RocketComponent clipboard) { + RocketComponent selected = selectionModel.getSelectedComponent(); + if (selected == null) + return null; + + if (clipboard == null) + return null; + + if (selected.isCompatible(clipboard)) + return new Pair(selected, selected.getChildCount()); + + RocketComponent parent = selected.getParent(); + if (parent != null && parent.isCompatible(clipboard)) { + int index = parent.getChildPosition(selected) + 1; + return new Pair(parent, index); + } + + return null; + } + + + + + + /////// Action classes + + private abstract class RocketAction extends AbstractAction implements ClipboardListener { + public abstract void clipboardChanged(); + } + + + /** + * Action that deletes the selected component. + */ + private class DeleteComponentAction extends RocketAction { + public DeleteComponentAction() { + //// Delete + this.putValue(NAME, trans.get("RocketActions.DelCompAct.Delete")); + //// Delete the selected component. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DelCompAct.ttip.Delete")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); +// this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); + this.putValue(SMALL_ICON, Icons.EDIT_DELETE); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent c = selectionModel.getSelectedComponent(); + + if (isDeletable(c)) { + ComponentConfigDialog.hideDialog(); + + document.addUndoPosition("Delete " + c.getComponentName()); + delete(c); + } + } + + @Override + public void clipboardChanged() { + this.setEnabled(isDeletable(selectionModel.getSelectedComponent())); + } + } + + + + /** + * Action that deletes the selected component. + */ + private class DeleteSimulationAction extends RocketAction { + public DeleteSimulationAction() { + //// Delete + this.putValue(NAME, trans.get("RocketActions.DelSimuAct.Delete")); + //// Delete the selected simulation. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DelSimuAct.ttip.Delete")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); +// this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); + this.putValue(SMALL_ICON, Icons.EDIT_DELETE); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + Simulation[] sims = selectionModel.getSelectedSimulations(); + if (sims.length > 0) { + if (verifyDeleteSimulation()) { + for (Simulation s: sims) { + document.removeSimulation(s); + } + } + } + } + + @Override + public void clipboardChanged() { + this.setEnabled(isSimulationSelected()); + } + } + + + + /** + * Action that deletes the selected component. + */ + private class DeleteAction extends RocketAction { + public DeleteAction() { + //// Delete + this.putValue(NAME, trans.get("RocketActions.DelAct.Delete")); + //// Delete the selected component or simulation. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DelAct.ttip.Delete")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); + this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); + this.putValue(SMALL_ICON, Icons.EDIT_DELETE); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + if (isSimulationSelected()) { + deleteSimulationAction.actionPerformed(e); + parentFrame.selectTab(BasicFrame.SIMULATION_TAB); + } else { + deleteComponentAction.actionPerformed(e); + parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + } + } + + @Override + public void clipboardChanged() { + this.setEnabled(isDeletable(selectionModel.getSelectedComponent()) || + isSimulationSelected()); + } + } + + + + /** + * Action the cuts the selected component (copies to clipboard and deletes). + */ + private class CutAction extends RocketAction { + public CutAction() { + //// Cut + this.putValue(NAME, trans.get("RocketActions.CutAction.Cut")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_T); + this.putValue(ACCELERATOR_KEY, CUT_KEY_STROKE); + //// Cut this component or simulation to the clipboard and remove from this design + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.CutAction.ttip.Cut")); + this.putValue(SMALL_ICON, Icons.EDIT_CUT); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent c = selectionModel.getSelectedComponent(); + Simulation[] sims = selectionModel.getSelectedSimulations(); + + if (isDeletable(c) && isCopyable(c)) { + ComponentConfigDialog.hideDialog(); + + document.addUndoPosition("Cut " + c.getComponentName()); + OpenRocketClipboard.setClipboard(c.copy()); + delete(c); + parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + } else if (isSimulationSelected()) { + + Simulation[] simsCopy = new Simulation[sims.length]; + for (int i=0; i < sims.length; i++) { + simsCopy[i] = sims[i].copy(); + } + OpenRocketClipboard.setClipboard(simsCopy); + + for (Simulation s: sims) { + document.removeSimulation(s); + } + parentFrame.selectTab(BasicFrame.SIMULATION_TAB); + } + } + + @Override + public void clipboardChanged() { + RocketComponent c = selectionModel.getSelectedComponent(); + this.setEnabled((isDeletable(c) && isCopyable(c)) || isSimulationSelected()); + } + } + + + + /** + * Action that copies the selected component to the clipboard. + */ + private class CopyAction extends RocketAction { + public CopyAction() { + //// Copy + this.putValue(NAME, trans.get("RocketActions.CopyAct.Copy")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_C); + this.putValue(ACCELERATOR_KEY, COPY_KEY_STROKE); + //// Copy this component (and subcomponents) to the clipboard. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.CopyAct.Copy")); + this.putValue(SMALL_ICON, Icons.EDIT_COPY); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent c = selectionModel.getSelectedComponent(); + Simulation[] sims = selectionModel.getSelectedSimulations(); + + if (isCopyable(c)) { + OpenRocketClipboard.setClipboard(c.copy()); + parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + } else if (sims.length >= 0) { + + Simulation[] simsCopy = new Simulation[sims.length]; + for (int i=0; i < sims.length; i++) { + simsCopy[i] = sims[i].copy(); + } + OpenRocketClipboard.setClipboard(simsCopy); + parentFrame.selectTab(BasicFrame.SIMULATION_TAB); + } + } + + @Override + public void clipboardChanged() { + this.setEnabled(isCopyable(selectionModel.getSelectedComponent()) || + isSimulationSelected()); + } + + } + + + + /** + * Action that pastes the current clipboard to the selected position. + * It first tries to paste the component to the end of the selected component + * as a child, and after that as a sibling after the selected component. + */ + private class PasteAction extends RocketAction { + public PasteAction() { + //// Paste + this.putValue(NAME, trans.get("RocketActions.PasteAct.Paste")); + this.putValue(MNEMONIC_KEY, KeyEvent.VK_P); + this.putValue(ACCELERATOR_KEY, PASTE_KEY_STROKE); + //// Paste the component or simulation on the clipboard to the design. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.PasteAct.ttip.Paste")); + this.putValue(SMALL_ICON, Icons.EDIT_PASTE); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent clipboard = OpenRocketClipboard.getClipboardComponent(); + Simulation[] sims = OpenRocketClipboard.getClipboardSimulations(); + + Pair position = getPastePosition(clipboard); + if (position != null) { + ComponentConfigDialog.hideDialog(); + + RocketComponent pasted = clipboard.copy(); + document.addUndoPosition("Paste " + pasted.getComponentName()); + position.getU().addChild(pasted, position.getV()); + selectionModel.setSelectedComponent(pasted); + + parentFrame.selectTab(BasicFrame.COMPONENT_TAB); + + } else if (sims != null) { + + ArrayList copySims = new ArrayList(); + + for (Simulation s: sims) { + Simulation copy = s.duplicateSimulation(rocket); + String name = copy.getName(); + if (name.matches(OpenRocketDocument.SIMULATION_NAME_PREFIX + "[0-9]+ *")) { + copy.setName(document.getNextSimulationName()); + } + document.addSimulation(copy); + copySims.add(copy); + } + selectionModel.setSelectedSimulations(copySims.toArray(new Simulation[0])); + + parentFrame.selectTab(BasicFrame.SIMULATION_TAB); + } + } + + @Override + public void clipboardChanged() { + this.setEnabled( + (getPastePosition(OpenRocketClipboard.getClipboardComponent()) != null) || + (OpenRocketClipboard.getClipboardSimulations() != null)); + } + } + + + + + + + /** + * Action to edit the currently selected component. + */ + private class EditAction extends RocketAction { + public EditAction() { + //// Edit + this.putValue(NAME, trans.get("RocketActions.EditAct.Edit")); + //// Edit the selected component. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.EditAct.ttip.Edit")); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent c = selectionModel.getSelectedComponent(); + if (c == null) + return; + + ComponentConfigDialog.showDialog(parentFrame, document, c); + } + + @Override + public void clipboardChanged() { + this.setEnabled(selectionModel.getSelectedComponent() != null); + } + } + + + + + + + + /** + * Action to add a new stage to the rocket. + */ + private class NewStageAction extends RocketAction { + public NewStageAction() { + //// New stage + this.putValue(NAME, trans.get("RocketActions.NewStageAct.Newstage")); + //// Add a new stage to the rocket design. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.NewStageAct.Newstage")); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + + ComponentConfigDialog.hideDialog(); + + RocketComponent stage = new Stage(); + //// Booster stage + stage.setName(trans.get("RocketActions.ActBoosterstage")); + //// Add stage + document.addUndoPosition("Add stage"); + rocket.addChild(stage); + rocket.getDefaultConfiguration().setAllStages(); + selectionModel.setSelectedComponent(stage); + ComponentConfigDialog.showDialog(parentFrame, document, stage); + + } + + @Override + public void clipboardChanged() { + this.setEnabled(true); + } + } + + + + + /** + * Action to move the selected component upwards in the parent's child list. + */ + private class MoveUpAction extends RocketAction { + public MoveUpAction() { + //// Move up + this.putValue(NAME, trans.get("RocketActions.MoveUpAct.Moveup")); + //// Move this component upwards. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.MoveUpAct.ttip.Moveup")); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent selected = selectionModel.getSelectedComponent(); + if (!canMove(selected)) + return; + + ComponentConfigDialog.hideDialog(); + + RocketComponent parent = selected.getParent(); + document.addUndoPosition("Move "+selected.getComponentName()); + parent.moveChild(selected, parent.getChildPosition(selected)-1); + selectionModel.setSelectedComponent(selected); + } + + @Override + public void clipboardChanged() { + this.setEnabled(canMove(selectionModel.getSelectedComponent())); + } + + private boolean canMove(RocketComponent c) { + if (c == null || c.getParent() == null) + return false; + RocketComponent parent = c.getParent(); + if (parent.getChildPosition(c) > 0) + return true; + return false; + } + } + + + + /** + * Action to move the selected component down in the parent's child list. + */ + private class MoveDownAction extends RocketAction { + public MoveDownAction() { + //// Move down + this.putValue(NAME, trans.get("RocketActions.MoveDownAct.Movedown")); + //// Move this component downwards. + this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.MoveDownAct.ttip.Movedown")); + clipboardChanged(); + } + + @Override + public void actionPerformed(ActionEvent e) { + RocketComponent selected = selectionModel.getSelectedComponent(); + if (!canMove(selected)) + return; + + ComponentConfigDialog.hideDialog(); + + RocketComponent parent = selected.getParent(); + document.addUndoPosition("Move "+selected.getComponentName()); + parent.moveChild(selected, parent.getChildPosition(selected)+1); + selectionModel.setSelectedComponent(selected); + } + + @Override + public void clipboardChanged() { + this.setEnabled(canMove(selectionModel.getSelectedComponent())); + } + + private boolean canMove(RocketComponent c) { + if (c == null || c.getParent() == null) + return false; + RocketComponent parent = c.getParent(); + if (parent.getChildPosition(c) < parent.getChildCount()-1) + return true; + return false; + } + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java b/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java new file mode 100644 index 00000000..2b5ca7e7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/SimulationEditDialog.java @@ -0,0 +1,1057 @@ +package net.sf.openrocket.gui.main; + + +import java.awt.Color; +import java.awt.Component; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; +import java.util.List; + +import javax.swing.AbstractListModel; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JSpinner; +import javax.swing.JTabbedPane; +import javax.swing.JTextField; +import javax.swing.ListCellRenderer; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.SpinnerEditor; +import net.sf.openrocket.gui.adaptors.BooleanModel; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.EnumModel; +import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.SimulationExportPanel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.plot.Axis; +import net.sf.openrocket.gui.plot.PlotConfiguration; +import net.sf.openrocket.gui.plot.SimulationPlotPanel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.models.atmosphere.ExtendedISAModel; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.RK4SimulationStepper; +import net.sf.openrocket.simulation.SimulationOptions; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.example.CSVSaveListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.GeodeticComputationStrategy; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.ValueMarker; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.StandardXYItemRenderer; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + + +public class SimulationEditDialog extends JDialog { + + public static final int DEFAULT = -1; + public static final int EDIT = 1; + public static final int PLOT = 2; + + + private final Window parentWindow; + private final Simulation simulation; + private final SimulationOptions conditions; + private final Configuration configuration; + private static final Translator trans = Application.getTranslator(); + + + public SimulationEditDialog(Window parent, Simulation s) { + this(parent, s, 0); + } + + public SimulationEditDialog(Window parent, Simulation s, int tab) { + //// Edit simulation + super(parent, trans.get("simedtdlg.title.Editsim"), JDialog.ModalityType.DOCUMENT_MODAL); + + this.parentWindow = parent; + this.simulation = s; + this.conditions = simulation.getOptions(); + configuration = simulation.getConfiguration(); + + JPanel mainPanel = new JPanel(new MigLayout("fill", "[grow, fill]")); + + //// Simulation name: + mainPanel.add(new JLabel(trans.get("simedtdlg.lbl.Simname") + " "), "span, split 2, shrink"); + final JTextField field = new JTextField(simulation.getName()); + field.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void changedUpdate(DocumentEvent e) { + setText(); + } + + @Override + public void insertUpdate(DocumentEvent e) { + setText(); + } + + @Override + public void removeUpdate(DocumentEvent e) { + setText(); + } + + private void setText() { + String name = field.getText(); + if (name == null || name.equals("")) + return; + System.out.println("Setting name:" + name); + simulation.setName(name); + + } + }); + mainPanel.add(field, "shrinky, growx, wrap"); + + JTabbedPane tabbedPane = new JTabbedPane(); + + //// Launch conditions + tabbedPane.addTab(trans.get("simedtdlg.tab.Launchcond"), flightConditionsTab()); + //// Simulation options + tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), simulationOptionsTab()); + //// Plot data + tabbedPane.addTab(trans.get("simedtdlg.tab.Plotdata"), plotTab()); + //// Export data + tabbedPane.addTab(trans.get("simedtdlg.tab.Exportdata"), exportTab()); + + // Select the initial tab + if (tab == EDIT) { + tabbedPane.setSelectedIndex(0); + } else if (tab == PLOT) { + tabbedPane.setSelectedIndex(2); + } else { + FlightData data = s.getSimulatedData(); + if (data == null || data.getBranchCount() == 0) + tabbedPane.setSelectedIndex(0); + else + tabbedPane.setSelectedIndex(2); + } + + mainPanel.add(tabbedPane, "spanx, grow, wrap"); + + + // Buttons + mainPanel.add(new JPanel(), "spanx, split, growx"); + + JButton button; + //// Run simulation button + button = new JButton(trans.get("simedtdlg.but.runsimulation")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SimulationEditDialog.this.dispose(); + SimulationRunDialog.runSimulations(parentWindow, simulation); + } + }); + mainPanel.add(button, "gapright para"); + + //// Close button + JButton close = new JButton(trans.get("dlg.but.close")); + close.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SimulationEditDialog.this.dispose(); + } + }); + mainPanel.add(close, ""); + + + this.add(mainPanel); + this.validate(); + this.pack(); + this.setLocationByPlatform(true); + + GUIUtil.setDisposableDialogOptions(this, button); + } + + + + + + private JPanel flightConditionsTab() { + JPanel panel = new JPanel(new MigLayout("fill")); + JPanel sub; + String tip; + UnitSelector unit; + BasicSlider slider; + DoubleModel m; + JSpinner spin; + + //// Motor selector + //// Motor configuration: + JLabel label = new JLabel(trans.get("simedtdlg.lbl.Motorcfg")); + //// Select the motor configuration to use. + label.setToolTipText(trans.get("simedtdlg.lbl.ttip.Motorcfg")); + panel.add(label, "shrinkx, spanx, split 2"); + + JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration)); + //// Select the motor configuration to use. + combo.setToolTipText(trans.get("simedtdlg.combo.ttip.motorconf")); + combo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + conditions.setMotorConfigurationID(configuration.getMotorConfigurationID()); + } + }); + panel.add(combo, "growx, wrap para"); + + + //// Wind settings: Average wind speed, turbulence intensity, std. deviation + sub = new JPanel(new MigLayout("fill, gap rel unrel", + "[grow][65lp!][30lp!][75lp!]", "")); + //// Wind + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.lbl.Wind"))); + panel.add(sub, "growx, split 2, aligny 0, flowy, gapright para"); + + + // Wind average + //// Average windspeed: + label = new JLabel(trans.get("simedtdlg.lbl.Averwindspeed")); + //// The average windspeed relative to the ground. + tip = trans.get("simedtdlg.lbl.ttip.Averwindspeed"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "WindSpeedAverage", UnitGroup.UNITS_VELOCITY, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(0, 10.0)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + + // Wind std. deviation + //// Standard deviation: + label = new JLabel(trans.get("simedtdlg.lbl.Stddeviation")); + //// The standard deviation of the windspeed.
+ //// The windspeed is within twice the standard deviation from the average for 95% of the time. + tip = trans.get("simedtdlg.lbl.ttip.Stddeviation"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "WindSpeedDeviation", UnitGroup.UNITS_VELOCITY, 0); + DoubleModel m2 = new DoubleModel(conditions, "WindSpeedAverage", 0.25, + UnitGroup.UNITS_COEFFICIENT, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(new DoubleModel(0), m2)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + // Wind turbulence intensity + //// Turbulence intensity: + label = new JLabel(trans.get("simedtdlg.lbl.Turbulenceintensity")); + //// The turbulence intensity is the standard deviation divided by the average windspeed.
+ //// Typical values range from + //// to + tip = trans.get("simedtdlg.lbl.ttip.Turbulenceintensity1") + + trans.get("simedtdlg.lbl.ttip.Turbulenceintensity2") + " " + + UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.05) + + " " + trans.get("simedtdlg.lbl.ttip.Turbulenceintensity3") + " " + + UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.20) + "."; + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "WindTurbulenceIntensity", UnitGroup.UNITS_RELATIVE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + + final JLabel intensityLabel = new JLabel( + getIntensityDescription(conditions.getWindTurbulenceIntensity())); + intensityLabel.setToolTipText(tip); + sub.add(intensityLabel, "w 75lp, wrap"); + m.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + intensityLabel.setText( + getIntensityDescription(conditions.getWindTurbulenceIntensity())); + } + }); + + + + + + //// Temperature and pressure + sub = new JPanel(new MigLayout("fill, gap rel unrel", + "[grow][65lp!][30lp!][75lp!]", "")); + //// Atmospheric conditions + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Atmoscond"))); + panel.add(sub, "growx, aligny 0, gapright para"); + + + BooleanModel isa = new BooleanModel(conditions, "ISAAtmosphere"); + JCheckBox check = new JCheckBox(isa); + //// Use International Standard Atmosphere + check.setText(trans.get("simedtdlg.checkbox.InterStdAtmosphere")); + //// Select to use the International Standard Atmosphere model. + ////
This model has a temperature of + //// and a pressure of + //// at sea level. + check.setToolTipText(trans.get("simedtdlg.checkbox.ttip.InterStdAtmosphere1") + " " + + UnitGroup.UNITS_TEMPERATURE.toStringUnit(ExtendedISAModel.STANDARD_TEMPERATURE) + + " " + trans.get("simedtdlg.checkbox.ttip.InterStdAtmosphere2") + " " + + UnitGroup.UNITS_PRESSURE.toStringUnit(ExtendedISAModel.STANDARD_PRESSURE) + + " " + trans.get("simedtdlg.checkbox.ttip.InterStdAtmosphere3")); + sub.add(check, "spanx, wrap unrel"); + + // Temperature: + label = new JLabel(trans.get("simedtdlg.lbl.Temperature")); + //// The temperature at the launch site. + tip = trans.get("simedtdlg.lbl.ttip.Temperature"); + label.setToolTipText(tip); + isa.addEnableComponent(label, false); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchTemperature", UnitGroup.UNITS_TEMPERATURE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + isa.addEnableComponent(spin, false); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + isa.addEnableComponent(unit, false); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(253.15, 308.15)); // -20 ... 35 + slider.setToolTipText(tip); + isa.addEnableComponent(slider, false); + sub.add(slider, "w 75lp, wrap"); + + + + // Pressure: + label = new JLabel(trans.get("simedtdlg.lbl.Pressure")); + //// The atmospheric pressure at the launch site. + tip = trans.get("simedtdlg.lbl.ttip.Pressure"); + label.setToolTipText(tip); + isa.addEnableComponent(label, false); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchPressure", UnitGroup.UNITS_PRESSURE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + isa.addEnableComponent(spin, false); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + isa.addEnableComponent(unit, false); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(0.950e5, 1.050e5)); + slider.setToolTipText(tip); + isa.addEnableComponent(slider, false); + sub.add(slider, "w 75lp, wrap"); + + + + + + //// Launch site conditions + sub = new JPanel(new MigLayout("fill, gap rel unrel", + "[grow][65lp!][30lp!][75lp!]", "")); + //// Launch site + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.lbl.Launchsite"))); + panel.add(sub, "growx, split 2, aligny 0, flowy"); + + + // Latitude: + label = new JLabel(trans.get("simedtdlg.lbl.Latitude")); + //// The launch site latitude affects the gravitational pull of Earth.
+ //// Positive values are on the Northern hemisphere, negative values on the Southern hemisphere. + tip = trans.get("simedtdlg.lbl.ttip.Latitude"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchLatitude", UnitGroup.UNITS_NONE, -90, 90); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + label = new JLabel(Chars.DEGREE + " N"); + label.setToolTipText(tip); + sub.add(label, "growx"); + slider = new BasicSlider(m.getSliderModel(-90, 90)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + // Longitude: + label = new JLabel(trans.get("simedtdlg.lbl.Longitude")); + tip = trans.get("simedtdlg.lbl.ttip.Longitude"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchLongitude", UnitGroup.UNITS_NONE, -180, 180); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + label = new JLabel(Chars.DEGREE + " E"); + label.setToolTipText(tip); + sub.add(label, "growx"); + slider = new BasicSlider(m.getSliderModel(-180, 180)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + // Altitude: + label = new JLabel(trans.get("simedtdlg.lbl.Altitude")); + //// The launch altitude above mean sea level.
+ //// This affects the position of the rocket in the atmospheric model. + tip = trans.get("simedtdlg.lbl.ttip.Altitude"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchAltitude", UnitGroup.UNITS_DISTANCE, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(0, 250, 1000)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + + + + //// Launch rod + sub = new JPanel(new MigLayout("fill, gap rel unrel", + "[grow][65lp!][30lp!][75lp!]", "")); + //// Launch rod + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Launchrod"))); + panel.add(sub, "growx, aligny 0, wrap"); + + + // Length: + label = new JLabel(trans.get("simedtdlg.lbl.Length")); + //// The length of the launch rod. + tip = trans.get("simedtdlg.lbl.ttip.Length"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchRodLength", UnitGroup.UNITS_LENGTH, 0); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(0, 1, 5)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + + // Angle: + label = new JLabel(trans.get("simedtdlg.lbl.Angle")); + //// The angle of the launch rod from vertical. + tip = trans.get("simedtdlg.lbl.ttip.Angle"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchRodAngle", UnitGroup.UNITS_ANGLE, + 0, SimulationOptions.MAX_LAUNCH_ROD_ANGLE); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(0, Math.PI / 9, + SimulationOptions.MAX_LAUNCH_ROD_ANGLE)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + + + // Direction: + label = new JLabel(trans.get("simedtdlg.lbl.Direction")); + //// Direction of the launch rod relative to the wind.
+ //// = towards the wind, + //// = downwind. + tip = trans.get("simedtdlg.lbl.ttip.Direction1") + + UnitGroup.UNITS_ANGLE.toStringUnit(0) + + " " + trans.get("simedtdlg.lbl.ttip.Direction2") + " " + + UnitGroup.UNITS_ANGLE.toStringUnit(Math.PI) + + " " + trans.get("simedtdlg.lbl.ttip.Direction3"); + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "LaunchRodDirection", UnitGroup.UNITS_ANGLE, + -Math.PI, Math.PI); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "growx"); + slider = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + + return panel; + } + + + private String getIntensityDescription(double i) { + if (i < 0.001) + //// None + return trans.get("simedtdlg.IntensityDesc.None"); + if (i < 0.05) + //// Very low + return trans.get("simedtdlg.IntensityDesc.Verylow"); + if (i < 0.10) + //// Low + return trans.get("simedtdlg.IntensityDesc.Low"); + if (i < 0.15) + //// Medium + return trans.get("simedtdlg.IntensityDesc.Medium"); + if (i < 0.20) + //// High + return trans.get("simedtdlg.IntensityDesc.High"); + if (i < 0.25) + //// Very high + return trans.get("simedtdlg.IntensityDesc.Veryhigh"); + //// Extreme + return trans.get("simedtdlg.IntensityDesc.Extreme"); + } + + + + private JPanel simulationOptionsTab() { + JPanel panel = new JPanel(new MigLayout("fill")); + JPanel sub, subsub; + String tip; + JLabel label; + DoubleModel m; + JSpinner spin; + UnitSelector unit; + BasicSlider slider; + + + //// Simulation options + sub = new JPanel(new MigLayout("fill, gap rel unrel", + "[grow][65lp!][30lp!][75lp!]", "")); + //// Simulator options + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Simopt"))); + panel.add(sub, "growx, growy, aligny 0"); + + + // Separate panel for computation methods, as they use a different layout + subsub = new JPanel(new MigLayout("insets 0, fill")); + + + //// Calculation method: + tip = trans.get("simedtdlg.lbl.ttip.Calcmethod"); + label = new JLabel(trans.get("simedtdlg.lbl.Calcmethod")); + label.setToolTipText(tip); + subsub.add(label, "gapright para"); + + //// Extended Barrowman + label = new JLabel(trans.get("simedtdlg.lbl.ExtBarrowman")); + label.setToolTipText(tip); + subsub.add(label, "growx, wrap para"); + + + // Simulation method + tip = trans.get("simedtdlg.lbl.ttip.Simmethod1") + + trans.get("simedtdlg.lbl.ttip.Simmethod2"); + label = new JLabel(trans.get("simedtdlg.lbl.Simmethod")); + label.setToolTipText(tip); + subsub.add(label, "gapright para"); + + label = new JLabel("6-DOF Runge-Kutta 4"); + label.setToolTipText(tip); + subsub.add(label, "growx, wrap para"); + + + //// Geodetic calculation method: + label = new JLabel(trans.get("simedtdlg.lbl.GeodeticMethod")); + label.setToolTipText(trans.get("simedtdlg.lbl.ttip.GeodeticMethodTip")); + subsub.add(label, "gapright para"); + + EnumModel gcsModel = new EnumModel(conditions, "GeodeticComputation"); + final JComboBox gcsCombo = new JComboBox(gcsModel); + ActionListener gcsTTipListener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + GeodeticComputationStrategy gcs = (GeodeticComputationStrategy) gcsCombo.getSelectedItem(); + gcsCombo.setToolTipText(gcs.getDescription()); + } + }; + gcsCombo.addActionListener(gcsTTipListener); + gcsTTipListener.actionPerformed(null); + subsub.add(gcsCombo, "growx, wrap para"); + + sub.add(subsub, "spanx, wrap para"); + + + //// Time step: + label = new JLabel(trans.get("simedtdlg.lbl.Timestep")); + tip = trans.get("simedtdlg.lbl.ttip.Timestep1") + + trans.get("simedtdlg.lbl.ttip.Timestep2") + " " + + UnitGroup.UNITS_TIME_STEP.toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + + "."; + label.setToolTipText(tip); + sub.add(label); + + m = new DoubleModel(conditions, "TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1); + + spin = new JSpinner(m.getSpinnerModel()); + spin.setEditor(new SpinnerEditor(spin)); + spin.setToolTipText(tip); + sub.add(spin, "w 65lp!"); + //sub.add(spin, "nogrid"); + + unit = new UnitSelector(m); + unit.setToolTipText(tip); + sub.add(unit, "w 25"); + //sub.add(unit, "nogrid"); + slider = new BasicSlider(m.getSliderModel(0, 0.2)); + slider.setToolTipText(tip); + sub.add(slider, "w 75lp, wrap"); + //sub.add(slider,"wrap"); + + + + + //// Reset to default button + JButton button = new JButton(trans.get("simedtdlg.but.resettodefault")); + //// Reset the time step to its default value ( + button.setToolTipText(trans.get("simedtdlg.but.ttip.resettodefault") + + UnitGroup.UNITS_SHORT_TIME.toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + + ")."); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + conditions.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP); + conditions.setGeodeticComputation(GeodeticComputationStrategy.SPHERICAL); + } + }); + + sub.add(button, "align left"); + + + + + //// Simulation listeners + sub = new JPanel(new MigLayout("fill, gap 0 0")); + //// Simulator listeners + sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Simlist"))); + panel.add(sub, "growx, growy"); + + + DescriptionArea desc = new DescriptionArea(5); + //// Simulation listeners is an advanced feature that allows user-written code to listen to and interact with the simulation. + //// For details on writing simulation listeners, see the OpenRocket technical documentation. + desc.setText(trans.get("simedtdlg.txt.longA1") + + trans.get("simedtdlg.txt.longA2")); + sub.add(desc, "aligny 0, growx, wrap para"); + + //// Current listeners: + label = new JLabel(trans.get("simedtdlg.lbl.Curlist")); + sub.add(label, "spanx, wrap rel"); + + final ListenerListModel listenerModel = new ListenerListModel(); + final JList list = new JList(listenerModel); + list.setCellRenderer(new ListenerCellRenderer()); + JScrollPane scroll = new JScrollPane(list); + // scroll.setPreferredSize(new Dimension(1,1)); + sub.add(scroll, "height 1px, grow, wrap rel"); + + //// Add button + button = new JButton(trans.get("simedtdlg.but.add")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String previous = Application.getPreferences().getString("previousListenerName", ""); + String input = (String) JOptionPane.showInputDialog(SimulationEditDialog.this, + new Object[] { + //// Type the full Java class name of the simulation listener, for example: + "Type the full Java class name of the simulation listener, for example:", + "" + CSVSaveListener.class.getName() + "" }, + //// Add simulation listener + trans.get("simedtdlg.lbl.Addsimlist"), + JOptionPane.QUESTION_MESSAGE, + null, null, + previous + ); + if (input == null || input.equals("")) + return; + + Application.getPreferences().putString("previousListenerName", input); + simulation.getSimulationListeners().add(input); + listenerModel.fireContentsChanged(); + } + }); + sub.add(button, "split 2, sizegroup buttons, alignx 50%, gapright para"); + + //// Remove button + button = new JButton(trans.get("simedtdlg.but.remove")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int[] selected = list.getSelectedIndices(); + Arrays.sort(selected); + for (int i = selected.length - 1; i >= 0; i--) { + simulation.getSimulationListeners().remove(selected[i]); + } + listenerModel.fireContentsChanged(); + } + }); + sub.add(button, "sizegroup buttons, alignx 50%"); + + + return panel; + } + + + private class ListenerListModel extends AbstractListModel { + @Override + public String getElementAt(int index) { + if (index < 0 || index >= getSize()) + return null; + return simulation.getSimulationListeners().get(index); + } + + @Override + public int getSize() { + return simulation.getSimulationListeners().size(); + } + + public void fireContentsChanged() { + super.fireContentsChanged(this, 0, getSize()); + } + } + + + + + /** + * A panel for plotting the previously calculated data. + */ + private JPanel plotTab() { + + // Check that data exists + if (simulation.getSimulatedData() == null || + simulation.getSimulatedData().getBranchCount() == 0) { + return noDataPanel(); + } + + return new SimulationPlotPanel(simulation); + } + + + + /** + * A panel for exporting the data. + */ + private JPanel exportTab() { + FlightData data = simulation.getSimulatedData(); + + // Check that data exists + if (data == null || data.getBranchCount() == 0 || + data.getBranch(0).getTypes().length == 0) { + return noDataPanel(); + } + + return new SimulationExportPanel(simulation); + } + + + + + + /** + * Return a panel stating that there is no data available, and that the user + * should run the simulation first. + */ + public static JPanel noDataPanel() { + JPanel panel = new JPanel(new MigLayout("fill")); + + // No data available + //// No flight data available. + panel.add(new JLabel(trans.get("simedtdlg.lbl.Noflightdata")), + "alignx 50%, aligny 100%, wrap para"); + //// Please run the simulation first. + panel.add(new JLabel(trans.get("simedtdlg.lbl.runsimfirst")), + "alignx 50%, aligny 0%, wrap"); + return panel; + } + + + private void performPlot(PlotConfiguration config) { + + // Fill the auto-selections + FlightDataBranch branch = simulation.getSimulatedData().getBranch(0); + PlotConfiguration filled = config.fillAutoAxes(branch); + List axes = filled.getAllAxes(); + + + // Create the data series for both axes + XYSeriesCollection[] data = new XYSeriesCollection[2]; + data[0] = new XYSeriesCollection(); + data[1] = new XYSeriesCollection(); + + + // Get the domain axis type + final FlightDataType domainType = filled.getDomainAxisType(); + final Unit domainUnit = filled.getDomainAxisUnit(); + if (domainType == null) { + throw new IllegalArgumentException("Domain axis type not specified."); + } + List x = branch.get(domainType); + + + // Create the XYSeries objects from the flight data and store into the collections + int length = filled.getTypeCount(); + String[] axisLabel = new String[2]; + for (int i = 0; i < length; i++) { + // Get info + FlightDataType type = filled.getType(i); + Unit unit = filled.getUnit(i); + int axis = filled.getAxis(i); + String name = getLabel(type, unit); + + // Store data in provided units + List y = branch.get(type); + XYSeries series = new XYSeries(name, false, true); + for (int j = 0; j < x.size(); j++) { + series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j))); + } + data[axis].addSeries(series); + + // Update axis label + if (axisLabel[axis] == null) + axisLabel[axis] = type.getName(); + else + axisLabel[axis] += "; " + type.getName(); + } + + + // Create the chart using the factory to get all default settings + JFreeChart chart = ChartFactory.createXYLineChart( + //// Simulated flight + trans.get("simedtdlg.chart.Simflight"), + null, + null, + null, + PlotOrientation.VERTICAL, + true, + true, + false + ); + + + // Add the data and formatting to the plot + XYPlot plot = chart.getXYPlot(); + int axisno = 0; + for (int i = 0; i < 2; i++) { + // Check whether axis has any data + if (data[i].getSeriesCount() > 0) { + // Create and set axis + double min = axes.get(i).getMinValue(); + double max = axes.get(i).getMaxValue(); + NumberAxis axis = new PresetNumberAxis(min, max); + axis.setLabel(axisLabel[i]); + // axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue()); + plot.setRangeAxis(axisno, axis); + + // Add data and map to the axis + plot.setDataset(axisno, data[i]); + plot.setRenderer(axisno, new StandardXYItemRenderer()); + plot.mapDatasetToRangeAxis(axisno, axisno); + axisno++; + } + } + + plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit)); + plot.addDomainMarker(new ValueMarker(0)); + plot.addRangeMarker(new ValueMarker(0)); + + + // Create the dialog + //// Simulation results + final JDialog dialog = new JDialog(this, trans.get("simedtdlg.dlg.Simres")); + dialog.setModalityType(ModalityType.DOCUMENT_MODAL); + + JPanel panel = new JPanel(new MigLayout("fill")); + dialog.add(panel); + + ChartPanel chartPanel = new ChartPanel(chart, + false, // properties + true, // save + false, // print + true, // zoom + true); // tooltips + chartPanel.setMouseWheelEnabled(true); + chartPanel.setEnforceFileExtensions(true); + chartPanel.setInitialDelay(500); + + chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); + + panel.add(chartPanel, "grow, wrap 20lp"); + + //// Close button + JButton button = new JButton(trans.get("dlg.but.close")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dialog.setVisible(false); + } + }); + panel.add(button, "right"); + + dialog.setLocationByPlatform(true); + dialog.pack(); + + GUIUtil.setDisposableDialogOptions(dialog, button); + + dialog.setVisible(true); + } + + + private class PresetNumberAxis extends NumberAxis { + private final double min; + private final double max; + + public PresetNumberAxis(double min, double max) { + this.min = min; + this.max = max; + autoAdjustRange(); + } + + @Override + protected void autoAdjustRange() { + this.setRange(min, max); + } + } + + + private String getLabel(FlightDataType type, Unit unit) { + String name = type.getName(); + if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) && + !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0) + name += " (" + unit.getUnit() + ")"; + return name; + } + + + + private class ListenerCellRenderer extends JLabel implements ListCellRenderer { + + @Override + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + String s = value.toString(); + setText(s); + + // Attempt instantiating, catch any exceptions + Exception ex = null; + try { + Class c = Class.forName(s); + @SuppressWarnings("unused") + SimulationListener l = (SimulationListener) c.newInstance(); + } catch (Exception e) { + ex = e; + } + + if (ex == null) { + setIcon(Icons.SIMULATION_LISTENER_OK); + //// Listener instantiated successfully. + setToolTipText("Listener instantiated successfully."); + } else { + setIcon(Icons.SIMULATION_LISTENER_ERROR); + //// Unable to instantiate listener due to exception:
+ setToolTipText("Unable to instantiate listener due to exception:
" + + ex.toString()); + } + + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + } + setOpaque(true); + return this; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/main/SimulationPanel.java b/core/src/net/sf/openrocket/gui/main/SimulationPanel.java new file mode 100644 index 00000000..11b707ea --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/SimulationPanel.java @@ -0,0 +1,586 @@ +package net.sf.openrocket.gui.main; + + +import java.awt.Color; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Arrays; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.table.DefaultTableCellRenderer; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.document.events.DocumentChangeEvent; +import net.sf.openrocket.document.events.DocumentChangeListener; +import net.sf.openrocket.document.events.SimulationChangeEvent; +import net.sf.openrocket.gui.adaptors.Column; +import net.sf.openrocket.gui.adaptors.ColumnTableModel; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.unit.UnitGroup; + +public class SimulationPanel extends JPanel { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + private static final Color WARNING_COLOR = Color.RED; + private static final String WARNING_TEXT = "\uFF01"; // Fullwidth exclamation mark + + private static final Color OK_COLOR = new Color(60, 150, 0); + private static final String OK_TEXT = "\u2714"; // Heavy check mark + + + + private final OpenRocketDocument document; + + private final ColumnTableModel simulationTableModel; + private final JTable simulationTable; + + + public SimulationPanel(OpenRocketDocument doc) { + super(new MigLayout("fill", "[grow][][][][][][grow]")); + + JButton button; + + + this.document = doc; + + + + //////// The simulation action buttons + + //// New simulation button + button = new JButton(trans.get("simpanel.but.newsimulation")); + //// Add a new simulation + button.setToolTipText(trans.get("simpanel.but.ttip.newsimulation")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + Simulation sim = new Simulation(document.getRocket()); + sim.setName(document.getNextSimulationName()); + + int n = document.getSimulationCount(); + document.addSimulation(sim); + simulationTableModel.fireTableDataChanged(); + simulationTable.clearSelection(); + simulationTable.addRowSelectionInterval(n, n); + + openDialog(sim, SimulationEditDialog.EDIT); + } + }); + this.add(button, "skip 1, gapright para"); + + //// Edit simulation button + button = new JButton(trans.get("simpanel.but.editsimulation")); + //// Edit the selected simulation + button.setToolTipText(trans.get("simpanel.but.ttip.editsim")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int selected = simulationTable.getSelectedRow(); + if (selected < 0) + return; // TODO: MEDIUM: "None selected" dialog + + selected = simulationTable.convertRowIndexToModel(selected); + simulationTable.clearSelection(); + simulationTable.addRowSelectionInterval(selected, selected); + + openDialog(document.getSimulations().get(selected), SimulationEditDialog.EDIT); + } + }); + this.add(button, "gapright para"); + + //// Run simulations + button = new JButton(trans.get("simpanel.but.runsimulations")); + //// Re-run the selected simulations + button.setToolTipText(trans.get("simpanel.but.ttip.runsimu")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int[] selection = simulationTable.getSelectedRows(); + if (selection.length == 0) + return; // TODO: LOW: "None selected" dialog + + Simulation[] sims = new Simulation[selection.length]; + for (int i = 0; i < selection.length; i++) { + selection[i] = simulationTable.convertRowIndexToModel(selection[i]); + sims[i] = document.getSimulation(selection[i]); + } + + long t = System.currentTimeMillis(); + new SimulationRunDialog(SwingUtilities.getWindowAncestor( + SimulationPanel.this), sims).setVisible(true); + log.info("Running simulations took " + (System.currentTimeMillis() - t) + " ms"); + fireMaintainSelection(); + } + }); + this.add(button, "gapright para"); + + //// Delete simulations button + button = new JButton(trans.get("simpanel.but.deletesimulations")); + //// Delete the selected simulations + button.setToolTipText(trans.get("simpanel.but.ttip.deletesim")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int[] selection = simulationTable.getSelectedRows(); + if (selection.length == 0) + return; // TODO: LOW: "None selected" dialog + + // Verify deletion + boolean verify = Application.getPreferences().getBoolean(Preferences.CONFIRM_DELETE_SIMULATION, true); + if (verify) { + + JPanel panel = new JPanel(new MigLayout()); + //// Do not ask me again + JCheckBox dontAsk = new JCheckBox(trans.get("simpanel.checkbox.donotask")); + panel.add(dontAsk, "wrap"); + //// You can change the default operation in the preferences. + panel.add(new StyledLabel(trans.get("simpanel.lbl.defpref"), -2)); + + int ret = JOptionPane.showConfirmDialog(SimulationPanel.this, + new Object[] { + //// Delete the selected simulations? + trans.get("simpanel.dlg.lbl.DeleteSim1"), + //// This operation cannot be undone. + trans.get("simpanel.dlg.lbl.DeleteSim2"), + "", + panel }, + //// Delete simulations + trans.get("simpanel.dlg.lbl.DeleteSim3"), + JOptionPane.OK_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE); + if (ret != JOptionPane.OK_OPTION) + return; + + if (dontAsk.isSelected()) { + Application.getPreferences().putBoolean(Preferences.CONFIRM_DELETE_SIMULATION, false); + } + } + + // Delete simulations + for (int i = 0; i < selection.length; i++) { + selection[i] = simulationTable.convertRowIndexToModel(selection[i]); + } + Arrays.sort(selection); + for (int i = selection.length - 1; i >= 0; i--) { + document.removeSimulation(selection[i]); + } + simulationTableModel.fireTableDataChanged(); + } + }); + this.add(button, "gapright para"); + + //// Plot / export button + button = new JButton(trans.get("simpanel.but.plotexport")); + // button = new JButton("Plot flight"); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int selected = simulationTable.getSelectedRow(); + if (selected < 0) + return; // TODO: MEDIUM: "None selected" dialog + + selected = simulationTable.convertRowIndexToModel(selected); + simulationTable.clearSelection(); + simulationTable.addRowSelectionInterval(selected, selected); + + openDialog(document.getSimulations().get(selected), SimulationEditDialog.PLOT); + } + }); + this.add(button, "wrap para"); + + + + + //////// The simulation table + + simulationTableModel = new ColumnTableModel( + + //// Status and warning column + new Column("") { + private JLabel label = null; + + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + // Initialize the label + if (label == null) { + label = new StyledLabel(2f); + label.setIconTextGap(1); + // label.setFont(label.getFont().deriveFont(Font.BOLD)); + } + + // Set simulation status icon + Simulation.Status status = document.getSimulation(row).getStatus(); + label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status)); + + + // Set warning marker + if (status == Simulation.Status.NOT_SIMULATED || + status == Simulation.Status.EXTERNAL) { + + label.setText(""); + + } else { + + WarningSet w = document.getSimulation(row).getSimulatedWarnings(); + if (w == null) { + label.setText(""); + } else if (w.isEmpty()) { + label.setForeground(OK_COLOR); + label.setText(OK_TEXT); + } else { + label.setForeground(WARNING_COLOR); + label.setText(WARNING_TEXT); + } + } + + return label; + } + + @Override + public int getExactWidth() { + return 36; + } + + @Override + public Class getColumnClass() { + return JLabel.class; + } + }, + + //// Simulation name + //// Name + new Column(trans.get("simpanel.col.Name")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + return document.getSimulation(row).getName(); + } + + @Override + public int getDefaultWidth() { + return 125; + } + }, + + //// Simulation motors + //// Motors + new Column(trans.get("simpanel.col.Motors")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + return document.getSimulation(row).getConfiguration() + .getMotorConfigurationDescription(); + } + + @Override + public int getDefaultWidth() { + return 125; + } + }, + + //// Apogee + new Column(trans.get("simpanel.col.Apogee")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + FlightData data = document.getSimulation(row).getSimulatedData(); + if (data == null) + return null; + + return UnitGroup.UNITS_DISTANCE.getDefaultUnit().toStringUnit( + data.getMaxAltitude()); + } + }, + + //// Maximum velocity + new Column(trans.get("simpanel.col.Maxvelocity")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + FlightData data = document.getSimulation(row).getSimulatedData(); + if (data == null) + return null; + + return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit( + data.getMaxVelocity()); + } + }, + + //// Maximum acceleration + new Column(trans.get("simpanel.col.Maxacceleration")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + FlightData data = document.getSimulation(row).getSimulatedData(); + if (data == null) + return null; + + return UnitGroup.UNITS_ACCELERATION.getDefaultUnit().toStringUnit( + data.getMaxAcceleration()); + } + }, + + //// Time to apogee + new Column(trans.get("simpanel.col.Timetoapogee")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + FlightData data = document.getSimulation(row).getSimulatedData(); + if (data == null) + return null; + + return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit( + data.getTimeToApogee()); + } + }, + + //// Flight time + new Column(trans.get("simpanel.col.Flighttime")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + FlightData data = document.getSimulation(row).getSimulatedData(); + if (data == null) + return null; + + return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit( + data.getFlightTime()); + } + }, + + //// Ground hit velocity + new Column(trans.get("simpanel.col.Groundhitvelocity")) { + @Override + public Object getValueAt(int row) { + if (row < 0 || row >= document.getSimulationCount()) + return null; + + FlightData data = document.getSimulation(row).getSimulatedData(); + if (data == null) + return null; + + return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit( + data.getGroundHitVelocity()); + } + } + + ) { + @Override + public int getRowCount() { + return document.getSimulationCount(); + } + }; + + // Override processKeyBinding so that the JTable does not catch + // key bindings used in menu accelerators + simulationTable = new JTable(simulationTableModel) { + @Override + protected boolean processKeyBinding(KeyStroke ks, + KeyEvent e, + int condition, + boolean pressed) { + return false; + } + }; + simulationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer()); + simulationTableModel.setColumnWidths(simulationTable.getColumnModel()); + + + // Mouse listener to act on double-clicks + simulationTable.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { + int selected = simulationTable.getSelectedRow(); + if (selected < 0) + return; + + selected = simulationTable.convertRowIndexToModel(selected); + simulationTable.clearSelection(); + simulationTable.addRowSelectionInterval(selected, selected); + + openDialog(document.getSimulations().get(selected), + SimulationEditDialog.DEFAULT); + } + } + }); + + document.addDocumentChangeListener(new DocumentChangeListener() { + @Override + public void documentChanged(DocumentChangeEvent event) { + if (!(event instanceof SimulationChangeEvent)) + return; + simulationTableModel.fireTableDataChanged(); + } + }); + + + + + // Fire table change event when the rocket changes + document.getRocket().addComponentChangeListener(new ComponentChangeListener() { + @Override + public void componentChanged(ComponentChangeEvent e) { + fireMaintainSelection(); + } + }); + + + JScrollPane scrollpane = new JScrollPane(simulationTable); + this.add(scrollpane, "spanx, grow, wrap rel"); + + + } + + + public ListSelectionModel getSimulationListSelectionModel() { + return simulationTable.getSelectionModel(); + } + + private void openDialog(final Simulation sim, int position) { + new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), sim, position) + .setVisible(true); + fireMaintainSelection(); + } + + private void fireMaintainSelection() { + int[] selection = simulationTable.getSelectedRows(); + simulationTableModel.fireTableDataChanged(); + for (int row : selection) { + if (row >= simulationTableModel.getRowCount()) + break; + simulationTable.addRowSelectionInterval(row, row); + } + } + + + private class JLabelRenderer extends DefaultTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(JTable table, + Object value, boolean isSelected, boolean hasFocus, int row, + int column) { + + if (row < 0 || row >= document.getSimulationCount()) + return super.getTableCellRendererComponent(table, value, + isSelected, hasFocus, row, column); + + // A JLabel is self-contained and has set its own tool tip + if (value instanceof JLabel) { + JLabel label = (JLabel) value; + if (isSelected) + label.setBackground(table.getSelectionBackground()); + else + label.setBackground(table.getBackground()); + label.setOpaque(true); + + label.setToolTipText(getSimulationToolTip(document.getSimulation(row))); + return label; + } + + Component component = super.getTableCellRendererComponent(table, value, + isSelected, hasFocus, row, column); + + if (component instanceof JComponent) { + ((JComponent) component).setToolTipText(getSimulationToolTip( + document.getSimulation(row))); + } + return component; + } + + private String getSimulationToolTip(Simulation sim) { + String tip; + FlightData data = sim.getSimulatedData(); + + tip = "" + sim.getName() + "
"; + switch (sim.getStatus()) { + case UPTODATE: + //// Up to date
+ tip += "Up to date
"; + break; + + case LOADED: + //// Data loaded from a file
+ tip += "Data loaded from a file
"; + break; + + case OUTDATED: + tip += "Data is out of date
"; + tip += "Click Run simulations to simulate.
"; + break; + + case EXTERNAL: + tip += "Imported data
"; + return tip; + + case NOT_SIMULATED: + tip += "Not simulated yet
"; + tip += "Click Run simulations to simulate."; + return tip; + } + + if (data == null) { + tip += "No simulation data available."; + return tip; + } + WarningSet warnings = data.getWarningSet(); + + if (warnings.isEmpty()) { + tip += "No warnings."; + return tip; + } + + tip += "Warnings:"; + for (Warning w : warnings) { + tip += "
" + w.toString(); + } + + return tip; + } + + } +} diff --git a/core/src/net/sf/openrocket/gui/main/SimulationRunDialog.java b/core/src/net/sf/openrocket/gui/main/SimulationRunDialog.java new file mode 100644 index 00000000..c1080305 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/SimulationRunDialog.java @@ -0,0 +1,462 @@ +package net.sf.openrocket.gui.main; + + +import java.awt.Dialog; +import java.awt.Dimension; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JProgressBar; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.dialogs.DetailDialog; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationCancelledException; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationLaunchException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.MathUtil; + + +public class SimulationRunDialog extends JDialog { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + /** Update the dialog status every this many ms */ + private static final long UPDATE_MS = 200; + + /** Flight progress at motor burnout */ + private static final double BURNOUT_PROGRESS = 0.4; + + /** Flight progress at apogee */ + private static final double APOGEE_PROGRESS = 0.7; + + + /* + * The executor service is not static since we want concurrent simulation + * dialogs to run in parallel, ie. they both have their own executor service. + */ + private final ExecutorService executor = Executors.newFixedThreadPool( + SwingPreferences.getMaxThreadCount()); + + + private final JLabel simLabel, timeLabel, altLabel, velLabel; + private final JProgressBar progressBar; + + + /* + * NOTE: Care must be used when accessing the simulation parameters, since they + * are being run in another thread. Mutexes are used to avoid concurrent usage, which + * will result in an exception being thrown! + */ + private final Simulation[] simulations; + private final String[] simulationNames; + private final SimulationWorker[] simulationWorkers; + private final SimulationStatus[] simulationStatuses; + private final double[] simulationMaxAltitude; + private final double[] simulationMaxVelocity; + private final boolean[] simulationDone; + + public SimulationRunDialog(Window window, Simulation... simulations) { + //// Running simulations... + super(window, trans.get("SimuRunDlg.title.RunSim"), Dialog.ModalityType.DOCUMENT_MODAL); + + if (simulations.length == 0) { + throw new IllegalArgumentException("Called with no simulations to run"); + } + + this.simulations = simulations; + + + // Randomize the simulation random seeds + for (Simulation sim : simulations) { + sim.getOptions().randomizeSeed(); + } + + // Initialize the simulations + int n = simulations.length; + simulationNames = new String[n]; + simulationWorkers = new SimulationWorker[n]; + simulationStatuses = new SimulationStatus[n]; + simulationMaxAltitude = new double[n]; + simulationMaxVelocity = new double[n]; + simulationDone = new boolean[n]; + + for (int i = 0; i < n; i++) { + simulationNames[i] = simulations[i].getName(); + simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i); + executor.execute(simulationWorkers[i]); + } + + // Build the dialog + JPanel panel = new JPanel(new MigLayout("fill", "[][grow]")); + + //// Running ... + simLabel = new JLabel(trans.get("SimuRunDlg.lbl.Running")); + panel.add(simLabel, "spanx, wrap para"); + //// Simulation time: + panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Simutime") + " "), "gapright para"); + timeLabel = new JLabel(""); + panel.add(timeLabel, "growx, wmin 200lp, wrap rel"); + + //// Altitude: + panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Altitude") + " ")); + altLabel = new JLabel(""); + panel.add(altLabel, "growx, wrap rel"); + + //// Velocity: + panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") + " ")); + velLabel = new JLabel(""); + panel.add(velLabel, "growx, wrap para"); + + progressBar = new JProgressBar(); + panel.add(progressBar, "spanx, growx, wrap para"); + + + // Add cancel button + JButton cancel = new JButton(trans.get("dlg.but.cancel")); + cancel.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + cancelSimulations(); + } + }); + panel.add(cancel, "spanx, tag cancel"); + + + // Cancel simulations when user closes the window + this.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + cancelSimulations(); + } + }); + + + this.add(panel); + this.setMinimumSize(new Dimension(300, 0)); + this.setLocationByPlatform(true); + this.validate(); + this.pack(); + + GUIUtil.setDisposableDialogOptions(this, null); + + updateProgress(); + } + + + /** + * Cancel the currently running simulations. This is equivalent to clicking + * the Cancel button on the dialog. + */ + public void cancelSimulations() { + executor.shutdownNow(); + for (SimulationWorker w : simulationWorkers) { + w.cancel(true); + } + } + + + /** + * Static helper method to run simulations. + * + * @param parent the parent Window of the dialog to use. + * @param simulations the simulations to run. + */ + public static void runSimulations(Window parent, Simulation... simulations) { + new SimulationRunDialog(parent, simulations).setVisible(true); + } + + + + + private void updateProgress() { + int index; + for (index = 0; index < simulations.length; index++) { + if (!simulationDone[index]) + break; + } + + if (index >= simulations.length) { + // Everything is done, close the dialog + log.debug("Everything done."); + this.dispose(); + return; + } + + // Update the progress bar status + int progress = 0; + for (SimulationWorker s : simulationWorkers) { + progress += s.getProgress(); + } + progress /= simulationWorkers.length; + progressBar.setValue(progress); + log.debug("Progressbar value " + progress); + + // Update the simulation fields + simLabel.setText("Running " + simulationNames[index]); + if (simulationStatuses[index] == null) { + log.debug("No simulation status data available, setting empty labels"); + timeLabel.setText(""); + altLabel.setText(""); + velLabel.setText(""); + return; + } + + Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit(); + timeLabel.setText(u.toStringUnit(simulationStatuses[index].getSimulationTime())); + + u = UnitGroup.UNITS_DISTANCE.getDefaultUnit(); + altLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketPosition().z) + " (max. " + + u.toStringUnit(simulationMaxAltitude[index]) + ")"); + + u = UnitGroup.UNITS_VELOCITY.getDefaultUnit(); + velLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketVelocity().z) + " (max. " + + u.toStringUnit(simulationMaxVelocity[index]) + ")"); + } + + + + /** + * A SwingWorker that performs a flight simulation. It periodically updates the + * simulation statuses of the parent class and calls updateProgress(). + * The progress of the simulation is stored in the progress property of the + * SwingWorker. + * + * @author Sampo Niskanen + */ + private class InteractiveSimulationWorker extends SimulationWorker { + + private final int index; + private final double burnoutTimeEstimate; + private volatile double burnoutVelocity; + private volatile double apogeeAltitude; + + /* + * -2 = time from 0 ... burnoutTimeEstimate + * -1 = velocity from v(burnoutTimeEstimate) ... 0 + * 0 ... n = stages from alt(max) ... 0 + */ + private volatile int simulationStage = -2; + + private int progress = 0; + + + public InteractiveSimulationWorker(Simulation sim, int index) { + super(sim); + this.index = index; + + // Calculate estimate of motor burn time + double launchBurn = 0; + double otherBurn = 0; + Configuration config = simulation.getConfiguration(); + String id = simulation.getOptions().getMotorConfigurationID(); + Iterator iterator = config.motorIterator(); + while (iterator.hasNext()) { + MotorMount m = iterator.next(); + if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH) + launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getBurnTimeEstimate()); + else + otherBurn = otherBurn + m.getMotor(id).getBurnTimeEstimate(); + } + burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1); + + } + + + /** + * Return the extra listeners to use, a progress listener and cancel listener. + */ + @Override + protected SimulationListener[] getExtraListeners() { + return new SimulationListener[] { new SimulationProgressListener() }; + } + + + /** + * Processes simulation statuses published by the simulation listener. + * The statuses of the parent class and the progress property are updated. + */ + @Override + protected void process(List chunks) { + + // Update max. altitude and velocity + for (SimulationStatus s : chunks) { + simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index], + s.getRocketPosition().z); + simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index], + s.getRocketVelocity().length()); + } + + // Calculate the progress + SimulationStatus status = chunks.get(chunks.size() - 1); + simulationStatuses[index] = status; + + // 1. time = 0 ... burnoutTimeEstimate + if (simulationStage == -2 && status.getSimulationTime() < burnoutTimeEstimate) { + log.debug("Method 1: t=" + status.getSimulationTime() + " est=" + burnoutTimeEstimate); + setSimulationProgress(MathUtil.map(status.getSimulationTime(), 0, burnoutTimeEstimate, + 0.0, BURNOUT_PROGRESS)); + updateProgress(); + return; + } + + if (simulationStage == -2) { + simulationStage++; + burnoutVelocity = MathUtil.max(status.getRocketVelocity().z, 0.1); + log.debug("CHANGING to Method 2, vel=" + burnoutVelocity); + } + + // 2. z-velocity from burnout velocity to zero + if (simulationStage == -1 && status.getRocketVelocity().z >= 0) { + log.debug("Method 2: vel=" + status.getRocketVelocity().z + " burnout=" + burnoutVelocity); + setSimulationProgress(MathUtil.map(status.getRocketVelocity().z, burnoutVelocity, 0, + BURNOUT_PROGRESS, APOGEE_PROGRESS)); + updateProgress(); + return; + } + + if (simulationStage == -1 && status.getRocketVelocity().z < 0) { + simulationStage++; + apogeeAltitude = MathUtil.max(status.getRocketPosition().z, 1); + log.debug("CHANGING to Method 3, apogee=" + apogeeAltitude); + } + + // 3. z-position from apogee to zero + // TODO: MEDIUM: several stages + log.debug("Method 3: alt=" + status.getRocketPosition().z + " apogee=" + apogeeAltitude); + setSimulationProgress(MathUtil.map(status.getRocketPosition().z, + apogeeAltitude, 0, APOGEE_PROGRESS, 1.0)); + updateProgress(); + } + + /** + * Marks this simulation as done and calls the progress update. + */ + @Override + protected void simulationDone() { + simulationDone[index] = true; + log.debug("Simulation done"); + setSimulationProgress(1.0); + updateProgress(); + } + + + /** + * Marks the simulation as done and shows a dialog presenting + * the error, unless the simulation was cancelled. + */ + @Override + protected void simulationInterrupted(Throwable t) { + + if (t instanceof SimulationCancelledException) { + simulationDone(); + return; // Ignore cancellations + } + + // Analyze the exception type + if (t instanceof SimulationLaunchException) { + + DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, + new Object[] { + //// Unable to simulate: + trans.get("SimuRunDlg.msg.Unabletosim"), + t.getMessage() + }, + null, simulation.getName(), JOptionPane.ERROR_MESSAGE); + + } else if (t instanceof SimulationException) { + + DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, + new Object[] { + //// A error occurred during the simulation: + trans.get("SimuRunDlg.msg.errorOccurred"), + t.getMessage() + }, + null, simulation.getName(), JOptionPane.ERROR_MESSAGE); + + } else { + + Application.getExceptionHandler().handleErrorCondition("An exception occurred during the simulation", t); + + } + simulationDone(); + } + + + private void setSimulationProgress(double p) { + int exact = Math.max(progress, (int) (100 * p + 0.5)); + progress = MathUtil.clamp(exact, 0, 100); + log.debug("Setting progress to " + progress + " (real " + exact + ")"); + super.setProgress(progress); + } + + + /** + * A simulation listener that regularly updates the progress property of the + * SimulationWorker and publishes the simulation status for the run dialog to process. + * + * @author Sampo Niskanen + */ + private class SimulationProgressListener extends AbstractSimulationListener { + private long time = 0; + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { + switch (event.getType()) { + case APOGEE: + simulationStage = 0; + apogeeAltitude = status.getRocketPosition().z; + log.debug("APOGEE, setting progress"); + setSimulationProgress(APOGEE_PROGRESS); + publish(status); + break; + + case LAUNCH: + publish(status); + break; + + case SIMULATION_END: + log.debug("END, setting progress"); + setSimulationProgress(1.0); + break; + } + return true; + } + + @Override + public void postStep(SimulationStatus status) { + if (System.currentTimeMillis() >= time + UPDATE_MS) { + time = System.currentTimeMillis(); + publish(status); + } + } + } + } +} diff --git a/core/src/net/sf/openrocket/gui/main/SimulationWorker.java b/core/src/net/sf/openrocket/gui/main/SimulationWorker.java new file mode 100644 index 00000000..f348fbcd --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/SimulationWorker.java @@ -0,0 +1,121 @@ +package net.sf.openrocket.gui.main; + +import java.util.Arrays; + +import javax.swing.SwingWorker; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationCancelledException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; +import net.sf.openrocket.simulation.listeners.SimulationListener; + + + +/** + * A SwingWorker that runs a simulation in a background thread. The simulation + * always includes a listener that checks whether this SwingWorked has been cancelled, + * and throws a {@link SimulationCancelledException} if it has. This allows the + * {@link #cancel(boolean)} method to be used to cancel the simulation. + * + * @author Sampo Niskanen + */ +public abstract class SimulationWorker extends SwingWorker { + + protected final Simulation simulation; + private Throwable throwable = null; + + public SimulationWorker(Simulation sim) { + this.simulation = sim; + } + + + /** + * Runs the simulation. + */ + @Override + protected FlightData doInBackground() { + if (isCancelled()) { + throwable = new SimulationCancelledException("The simulation was interrupted."); + return null; + } + + SimulationListener[] listeners = getExtraListeners(); + + if (listeners != null) { + listeners = Arrays.copyOf(listeners, listeners.length + 1); + } else { + listeners = new SimulationListener[1]; + } + + listeners[listeners.length - 1] = new CancelListener(); + + try { + simulation.simulate(listeners); + } catch (Throwable e) { + throwable = e; + return null; + } + return simulation.getSimulatedData(); + } + + + /** + * Return additional listeners to use during the simulation. The default + * implementation returns an empty array. + * + * @return additional listeners to use, or null. + */ + protected SimulationListener[] getExtraListeners() { + return new SimulationListener[0]; + } + + + /** + * Called after a simulation is successfully simulated. This method is not + * called if the simulation ends in an exception. + */ + protected abstract void simulationDone(); + + /** + * Called if the simulation is interrupted due to an exception. + * + * @param t the Throwable that caused the interruption + */ + protected abstract void simulationInterrupted(Throwable t); + + + + /** + * Marks this simulation as done and calls the progress update. + */ + @Override + protected final void done() { + if (throwable == null) + simulationDone(); + else + simulationInterrupted(throwable); + } + + + + /** + * A simulation listener that throws a {@link SimulationCancelledException} if + * this SwingWorker has been cancelled. The conditions is checked every time a step + * is taken. + * + * @author Sampo Niskanen + */ + private class CancelListener extends AbstractSimulationListener { + + @Override + public void postStep(SimulationStatus status) throws SimulationCancelledException { + + if (isCancelled()) { + throw new SimulationCancelledException("The simulation was interrupted."); + } + + } + } +} diff --git a/core/src/net/sf/openrocket/gui/main/Splash.java b/core/src/net/sf/openrocket/gui/main/Splash.java new file mode 100644 index 00000000..85f3d529 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/Splash.java @@ -0,0 +1,93 @@ +package net.sf.openrocket.gui.main; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.SplashScreen; +import java.awt.font.GlyphVector; +import java.awt.geom.Rectangle2D; + +import net.sf.openrocket.util.BuildProperties; + +/** + * Helper methods for manipulating the Java runtime splash screen. + *

+ * Notes: + * SplashScreen.update() takes randomly between 4 and 500 ms to complete, + * even after it has been called ~100 times (and therefore pre-compiled). + * Therefore it cannot be relied upon to perform for example color fades. + * Care should be taken to call update() only once or twice per second. + * + * @author Sampo Niskanen + */ +public class Splash { + + // The right edge of the text base line for the version string + private static final int VERSION_POSITION_X = 617; + private static final int VERSION_POSITION_Y = 135; + private static final Font VERSION_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 9); + private static final Color VERSION_COLOR = Color.WHITE; + + + /** + * Initialize the splash screen with additional information. This method should + * be called as soon as reasonably possible during program startup. + * + * @return true if the splash screen could be successfully initialized + */ + public static boolean init() { + // Get the splash screen + SplashScreen s = getSplash(); + if (s == null) + return false; + + // Create graphics context and set antialiasing on + Graphics2D g2 = s.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + // Draw the version number + drawVersionNumber(g2); + + // Update the splash screen + s.update(); + return true; + } + + + + private static void drawVersionNumber(Graphics2D g2) { + String text = "Version " + BuildProperties.getVersion(); + GlyphVector gv = VERSION_FONT.createGlyphVector(g2.getFontRenderContext(), text); + + Rectangle2D rect = gv.getVisualBounds(); + double width = rect.getWidth(); + + g2.setColor(VERSION_COLOR); + g2.drawGlyphVector(gv, (float) (VERSION_POSITION_X - width), (float) VERSION_POSITION_Y); + + } + + + /** + * Return the current splash screen or null if not available. + * This method catches the possible exceptions and returns null if they occur. + * + * @return the current splash screen, or null. + */ + private static SplashScreen getSplash() { + try { + return SplashScreen.getSplashScreen(); + } catch (RuntimeException e) { + return null; + } + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/main/SwingExceptionHandler.java b/core/src/net/sf/openrocket/gui/main/SwingExceptionHandler.java new file mode 100644 index 00000000..dbaf437c --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/SwingExceptionHandler.java @@ -0,0 +1,425 @@ +package net.sf.openrocket.gui.main; + +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; + +import net.sf.openrocket.gui.dialogs.BugReportDialog; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.ExceptionHandler; + + +public class SwingExceptionHandler implements Thread.UncaughtExceptionHandler, ExceptionHandler { + + private static final LogHelper log = Application.getLogger(); + + private static final int MEMORY_RESERVE = 512 * 1024; + + /** + * A memory reserve of 0.5 MB of memory, that can be freed when showing the dialog. + *

+ * This field is package-private so that the JRE cannot optimize its use away. + */ + volatile byte[] memoryReserve = null; + + private volatile boolean handling = false; + + + + + @Override + public void uncaughtException(final Thread thread, final Throwable throwable) { + + // Free memory reserve if out of memory + if (isOutOfMemoryError(throwable)) { + memoryReserve = null; + handling = false; + log.error("Out of memory error detected", throwable); + } + + if (isNonFatalJREBug(throwable)) { + log.warn("Ignoring non-fatal JRE bug", throwable); + return; + } + + log.error("Handling uncaught exception on thread=" + thread, throwable); + throwable.printStackTrace(); + + if (handling) { + log.warn("Exception is currently being handled, ignoring"); + return; + } + + try { + handling = true; + + // Show on the EDT + if (SwingUtilities.isEventDispatchThread()) { + log.info("Exception handler running on EDT, showing dialog"); + showDialog(thread, throwable); + } else { + log.info("Exception handler not on EDT, invoking dialog on EDT"); + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + showDialog(thread, throwable); + } + }); + } + + } catch (Throwable ex) { + + // Make sure the handler does not throw any exceptions + try { + log.error("Caught exception while handling exception", ex); + System.err.println("Exception in exception handler, dumping exception:"); + ex.printStackTrace(); + } catch (Exception ignore) { + } + + } finally { + // Mark handling as completed + handling = false; + } + + } + + + /** + * Handle an error condition programmatically without throwing an exception. + * This can be used in cases where recovery of the error is desirable. + *

+ * This method is guaranteed never to throw an exception, and can thus be safely + * used in finally blocks. + * + * @param message the error message. + */ + @Override + public void handleErrorCondition(String message) { + log.error(1, message, new TraceException()); + handleErrorCondition(new InternalException(message)); + } + + + /** + * Handle an error condition programmatically without throwing an exception. + * This can be used in cases where recovery of the error is desirable. + *

+ * This method is guaranteed never to throw an exception, and can thus be safely + * used in finally blocks. + * + * @param message the error message. + * @param exception the exception that occurred. + */ + @Override + public void handleErrorCondition(String message, Throwable exception) { + log.error(1, message, exception); + handleErrorCondition(new InternalException(message, exception)); + } + + + /** + * Handle an error condition programmatically without throwing an exception. + * This can be used in cases where recovery of the error is desirable. + *

+ * This method is guaranteed never to throw an exception, and can thus be safely + * used in finally blocks. + * + * @param exception the exception that occurred. + */ + @Override + public void handleErrorCondition(final Throwable exception) { + try { + if (!(exception instanceof InternalException)) { + log.error(1, "Error occurred", exception); + } + final Thread thread = Thread.currentThread(); + + if (SwingUtilities.isEventDispatchThread()) { + log.info("Running in EDT, showing dialog"); + this.showDialog(thread, exception); + } else { + log.info("Not in EDT, invoking dialog later"); + final SwingExceptionHandler instance = this; + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + instance.showDialog(thread, exception); + } + }); + } + } catch (Exception e) { + log.error("Exception occurred in error handler", e); + } + } + + + /** + * The actual handling routine. + * + * @param t the thread that caused the exception, or null. + * @param e the exception. + */ + private void showDialog(Thread t, Throwable e) { + + // Out of memory + if (isOutOfMemoryError(e)) { + log.info("Showing out-of-memory dialog"); + JOptionPane.showMessageDialog(null, + new Object[] { + "OpenRocket is out of available memory!", + "You should immediately close unnecessary design windows,", + "save any unsaved designs and restart OpenRocket!" + }, "Out of memory", JOptionPane.ERROR_MESSAGE); + return; + } + + // Create the message + String msg = e.getClass().getSimpleName() + ": " + e.getMessage(); + if (msg.length() > 90) { + msg = msg.substring(0, 80) + "..."; + } + + // Unknown Error + if (!(e instanceof Exception) && !(e instanceof LinkageError)) { + log.info("Showing Error dialog"); + JOptionPane.showMessageDialog(null, + new Object[] { + "An unknown Java error occurred:", + msg, + "You should immediately close unnecessary design windows,
" + + "save any unsaved designs and restart OpenRocket!" + }, "Unknown Java error", JOptionPane.ERROR_MESSAGE); + return; + } + + + // Normal exception, show question dialog + log.info("Showing Exception dialog"); + int selection = JOptionPane.showOptionDialog(null, new Object[] { + "OpenRocket encountered an uncaught exception. This typically signifies " + + "a bug in the software.", + "        " + msg + "", + " ", + "Please take a moment to report this bug to the developers.", + "This can be done automatically if you have an Internet connection." + }, "Uncaught exception", JOptionPane.DEFAULT_OPTION, + JOptionPane.ERROR_MESSAGE, null, + new Object[] { "View bug report", "Close" }, "View bug report"); + + if (selection != 0) { + // User cancelled + log.user("User chose not to fill bug report"); + return; + } + + // Show bug report dialog + log.user("User requested sending bug report"); + BugReportDialog.showExceptionDialog(null, t, e); + } + + + + /** + * Registers the uncaught exception handler. This should be used to ensure that + * all necessary registrations are performed. + */ + public void registerExceptionHandler() { + + Thread.setDefaultUncaughtExceptionHandler(this); + + // Handler for modal dialogs of Sun's Java implementation + // See bug ID 4499199. + System.setProperty("sun.awt.exception.handler", AwtHandler.class.getName()); + + reserveMemory(); + + } + + + /** + * Reserve the buffer memory that is freed in case an OutOfMemoryError occurs. + */ + private void reserveMemory() { + memoryReserve = new byte[MEMORY_RESERVE]; + for (int i = 0; i < MEMORY_RESERVE; i++) { + memoryReserve[i] = (byte) i; + } + } + + + + /** + * Return whether this throwable was caused by an OutOfMemoryError + * condition. An exception is deemed to be caused by OutOfMemoryError + * if the throwable or any of its causes is of the type OutOfMemoryError. + *

+ * This method is required because Apple's JRE implementation sometimes + * masks OutOfMemoryErrors within RuntimeExceptions. Idiots. + * + * @param t the throwable to examine. + * @return whether this is an out-of-memory condition. + */ + private boolean isOutOfMemoryError(Throwable t) { + while (t != null) { + if (t instanceof OutOfMemoryError) + return true; + t = t.getCause(); + } + return false; + } + + + + /** + * Handler used in modal dialogs by Sun Java implementation. + */ + public static class AwtHandler { + public void handle(Throwable t) { + Application.getExceptionHandler().uncaughtException(Thread.currentThread(), t); + } + } + + + /** + * Detect various non-fatal Sun JRE bugs. + * + * @param t the throwable + * @return whether this exception should be ignored + */ + private boolean isNonFatalJREBug(Throwable t) { + + // NOTE: Calling method logs the entire throwable, so log only message here + + + /* + * Detect and ignore bug 6826104 in Sun JRE. + */ + if (t instanceof NullPointerException) { + StackTraceElement[] trace = t.getStackTrace(); + + if (trace.length > 3 && + trace[0].getClassName().equals("sun.awt.X11.XWindowPeer") && + trace[0].getMethodName().equals("restoreTransientFor") && + + trace[1].getClassName().equals("sun.awt.X11.XWindowPeer") && + trace[1].getMethodName().equals("removeFromTransientFors") && + + trace[2].getClassName().equals("sun.awt.X11.XWindowPeer") && + trace[2].getMethodName().equals("setModalBlocked")) { + log.warn("Ignoring Sun JRE bug (6826104): http://bugs.sun.com/view_bug.do?bug_id=6826104" + t); + return true; + } + + } + + + /* + * Detect and ignore bug 6828938 in Sun JRE 1.6.0_14 - 1.6.0_16. + */ + if (t instanceof ArrayIndexOutOfBoundsException) { + final String buggyClass = "sun.font.FontDesignMetrics"; + StackTraceElement[] elements = t.getStackTrace(); + if (elements.length >= 3 && + (buggyClass.equals(elements[0].getClassName()) || + buggyClass.equals(elements[1].getClassName()) || + buggyClass.equals(elements[2].getClassName()))) { + log.warn("Ignoring Sun JRE bug 6828938: " + + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6828938): " + t); + return true; + } + } + + /* + * Detect and ignore bug 6561072 in Sun JRE 1.6.0_? + */ + if (t instanceof NullPointerException) { + StackTraceElement[] trace = t.getStackTrace(); + + if (trace.length > 3 && + trace[0].getClassName().equals("javax.swing.JComponent") && + trace[0].getMethodName().equals("repaint") && + + trace[1].getClassName().equals("sun.swing.FilePane$2") && + trace[1].getMethodName().equals("repaintListSelection") && + + trace[2].getClassName().equals("sun.swing.FilePane$2") && + trace[2].getMethodName().equals("repaintSelection")) { + log.warn("Ignoring Sun JRE bug 6561072 " + + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6561072): " + t); + return true; + } + } + + + /* + * Detect and ignore bug 6933331 in Sun JRE 1.6.0_18 and others + */ + if (t instanceof IllegalStateException) { + StackTraceElement[] trace = t.getStackTrace(); + + if (trace.length > 1 && + trace[0].getClassName().equals("sun.awt.windows.WComponentPeer") && + trace[0].getMethodName().equals("getBackBuffer")) { + log.warn("Ignoring Sun JRE bug 6933331 " + + "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6933331): " + t); + return true; + } + } + + /* + * Detect and ignore bug in Sun JRE 1.6.0_19 + */ + if (t instanceof NullPointerException) { + StackTraceElement[] trace = t.getStackTrace(); + + if (trace.length > 3 && + trace[0].getClassName().equals("sun.awt.shell.Win32ShellFolder2") && + trace[0].getMethodName().equals("pidlsEqual") && + + trace[1].getClassName().equals("sun.awt.shell.Win32ShellFolder2") && + trace[1].getMethodName().equals("equals") && + + trace[2].getClassName().equals("sun.awt.shell.Win32ShellFolderManager2") && + trace[2].getMethodName().equals("isFileSystemRoot")) { + log.warn("Ignoring Sun JRE bug " + + "(see http://forums.sun.com/thread.jspa?threadID=5435324): " + t); + return true; + } + } + + /* + * Detect Sun JRE bug in D3D + */ + if (t instanceof ClassCastException) { + if (t.getMessage().equals("sun.awt.Win32GraphicsConfig cannot be cast to sun.java2d.d3d.D3DGraphicsConfig")) { + log.warn("Ignoring Sun JRE bug " + + "(see http://forums.sun.com/thread.jspa?threadID=5440525): " + t); + return true; + } + } + + return false; + } + + + @SuppressWarnings("unused") + private static class InternalException extends Exception { + public InternalException() { + super(); + } + + public InternalException(String message, Throwable cause) { + super(message, cause); + } + + public InternalException(String message) { + super(message); + } + + public InternalException(Throwable cause) { + super(cause); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/main/UndoRedoAction.java b/core/src/net/sf/openrocket/gui/main/UndoRedoAction.java new file mode 100644 index 00000000..08c31c7e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/UndoRedoAction.java @@ -0,0 +1,104 @@ +package net.sf.openrocket.gui.main; + +import java.awt.event.ActionEvent; + +import javax.swing.AbstractAction; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.UndoRedoListener; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; + +/** + * Inner class to implement undo/redo actions. + */ +public class UndoRedoAction extends AbstractAction implements UndoRedoListener { + + // Use Factory mechanism because we want to register the new instance as an + // UndoRedoListener. + public static UndoRedoAction newUndoAction( OpenRocketDocument document ) { + UndoRedoAction undo = new UndoRedoAction( UNDO, document ); + document.addUndoRedoListener(undo); + return undo; + } + + public static UndoRedoAction newRedoAction( OpenRocketDocument document ) { + UndoRedoAction redo = new UndoRedoAction( REDO, document ); + document.addUndoRedoListener(redo); + return redo; + } + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private static final int UNDO = 1; + private static final int REDO = 2; + + private final int type; + + private final OpenRocketDocument document; + + // Sole constructor + private UndoRedoAction(int type, OpenRocketDocument document) { + this.document = document; + if (type != UNDO && type != REDO) { + throw new IllegalArgumentException("Unknown type = " + type); + } + this.type = type; + setAllValues(); + } + + + // Actual action to make + @Override + public void actionPerformed(ActionEvent e) { + switch (type) { + case UNDO: + log.user("Performing undo, event=" + e); + document.undo(); + break; + + case REDO: + log.user("Performing redo, event=" + e); + document.redo(); + break; + } + } + + + // Set all the values correctly (name and enabled/disabled status) + public void setAllValues() { + String name, desc; + boolean actionEnabled; + + switch (type) { + case UNDO: + //// Undo + name = trans.get("OpenRocketDocument.Undo"); + desc = document.getUndoDescription(); + actionEnabled = document.isUndoAvailable(); + this.putValue(SMALL_ICON, Icons.EDIT_UNDO); + break; + + case REDO: + ////Redo + name = trans.get("OpenRocketDocument.Redo"); + desc = document.getRedoDescription(); + actionEnabled = document.isRedoAvailable(); + this.putValue(SMALL_ICON, Icons.EDIT_REDO); + break; + + default: + throw new BugException("illegal type=" + type); + } + + if (desc != null) + name = name + " (" + desc + ")"; + + putValue(NAME, name); + setEnabled(actionEnabled); + } +} diff --git a/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java new file mode 100644 index 00000000..f78bfd00 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java @@ -0,0 +1,32 @@ +package net.sf.openrocket.gui.main.componenttree; + +import javax.swing.DropMode; +import javax.swing.ToolTipManager; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.components.BasicTree; + + +public class ComponentTree extends BasicTree { + + public ComponentTree(OpenRocketDocument document) { + super(); + this.setModel(new ComponentTreeModel(document.getRocket(), this)); + + this.setCellRenderer(new ComponentTreeRenderer()); + + this.setDragEnabled(true); + this.setDropMode(DropMode.INSERT); + this.setTransferHandler(new ComponentTreeTransferHandler(document)); + + // Expand whole tree by default + expandTree(); + + // Enable tooltips for this component + ToolTipManager.sharedInstance().registerComponent(this); + + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java new file mode 100644 index 00000000..fe141f61 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java @@ -0,0 +1,226 @@ +package net.sf.openrocket.gui.main.componenttree; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.JTree; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; +import net.sf.openrocket.rocketcomponent.ComponentChangeListener; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.BugException; + + +/** + * A TreeModel that implements viewing of the rocket tree structure. + * + * @author Sampo Niskanen + */ + +public class ComponentTreeModel implements TreeModel, ComponentChangeListener { + ArrayList listeners = new ArrayList(); + + private final RocketComponent root; + private final JTree tree; + + public ComponentTreeModel(RocketComponent root, JTree tree) { + this.root = root; + this.tree = tree; + root.addComponentChangeListener(this); + } + + + @Override + public Object getChild(Object parent, int index) { + RocketComponent component = (RocketComponent) parent; + + try { + return component.getChild(index); + } catch (IndexOutOfBoundsException e) { + return null; + } + } + + + @Override + public int getChildCount(Object parent) { + RocketComponent c = (RocketComponent) parent; + + return c.getChildCount(); + } + + + @Override + public int getIndexOfChild(Object parent, Object child) { + if (parent == null || child == null) + return -1; + + RocketComponent p = (RocketComponent) parent; + RocketComponent c = (RocketComponent) child; + + return p.getChildPosition(c); + } + + @Override + public Object getRoot() { + return root; + } + + @Override + public boolean isLeaf(Object node) { + return !((RocketComponent) node).allowsChildren(); + } + + @Override + public void addTreeModelListener(TreeModelListener l) { + listeners.add(l); + } + + @Override + public void removeTreeModelListener(TreeModelListener l) { + listeners.remove(l); + } + + private void fireTreeNodeChanged(RocketComponent node) { + TreeModelEvent e = new TreeModelEvent(this, makeTreePath(node), null, null); + Object[] l = listeners.toArray(); + for (int i = 0; i < l.length; i++) + ((TreeModelListener) l[i]).treeNodesChanged(e); + } + + + private void fireTreeStructureChanged(RocketComponent source) { + Object[] path = { root }; + + + // Get currently expanded path IDs + Enumeration enumer = tree.getExpandedDescendants(new TreePath(path)); + ArrayList expanded = new ArrayList(); + if (enumer != null) { + while (enumer.hasMoreElements()) { + TreePath p = enumer.nextElement(); + expanded.add(((RocketComponent) p.getLastPathComponent()).getID()); + } + } + + // Send structure change event + TreeModelEvent e = new TreeModelEvent(this, path); + Object[] l = listeners.toArray(); + for (int i = 0; i < l.length; i++) + ((TreeModelListener) l[i]).treeStructureChanged(e); + + // Re-expand the paths + for (String id : expanded) { + RocketComponent c = root.findComponent(id); + if (c == null) + continue; + tree.expandPath(makeTreePath(c)); + } + if (source != null) { + TreePath p = makeTreePath(source); + tree.makeVisible(p); + tree.expandPath(p); + } + } + + @Override + public void valueForPathChanged(TreePath path, Object newValue) { + System.err.println("ERROR: valueForPathChanged called?!"); + } + + + @Override + public void componentChanged(ComponentChangeEvent e) { + if (e.isTreeChange() || e.isUndoChange()) { + // Tree must be fully updated also in case of an undo change + fireTreeStructureChanged(e.getSource()); + if (e.isTreeChange() && e.isUndoChange()) { + // If the undo has changed the tree structure, some elements may be hidden + // unnecessarily + // TODO: LOW: Could this be performed better? + expandAll(); + } + } else if (e.isOtherChange()) { + fireTreeNodeChanged(e.getSource()); + } + } + + public void expandAll() { + Iterator iterator = root.iterator(false); + while (iterator.hasNext()) { + tree.makeVisible(makeTreePath(iterator.next())); + } + } + + + /** + * Return the rocket component that a TreePath object is referring to. + * + * @param path the TreePath + * @return the RocketComponent the path is referring to. + * @throws NullPointerException if path is null + * @throws BugException if the path does not refer to a RocketComponent + */ + public static RocketComponent componentFromPath(TreePath path) { + Object last = path.getLastPathComponent(); + if (!(last instanceof RocketComponent)) { + throw new BugException("Tree path does not refer to a RocketComponent: " + path.toString()); + } + return (RocketComponent) last; + } + + + /** + * Return a TreePath corresponding to the specified rocket component. + * + * @param component the rocket component + * @return a TreePath corresponding to this RocketComponent + */ + public static TreePath makeTreePath(RocketComponent component) { + if (component == null) { + throw new NullPointerException(); + } + + RocketComponent c = component; + + List list = new LinkedList(); + + while (c != null) { + list.add(0, c); + c = c.getParent(); + } + + return new TreePath(list.toArray()); + } + + + /** + * Return a string describing the path, using component normal names. + * + * @param treePath the tree path + * @return a string representation + */ + public static String pathToString(TreePath treePath) { + StringBuffer sb = new StringBuffer(); + sb.append("["); + for (Object o : treePath.getPath()) { + if (sb.length() > 1) { + sb.append("; "); + } + if (o instanceof RocketComponent) { + sb.append(((RocketComponent) o).getComponentName()); + } else { + sb.append(o.toString()); + } + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java new file mode 100644 index 00000000..a7e8386e --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java @@ -0,0 +1,45 @@ +package net.sf.openrocket.gui.main.componenttree; + + + +import java.awt.Component; + +import javax.swing.JTree; +import javax.swing.tree.DefaultTreeCellRenderer; + +import net.sf.openrocket.gui.main.ComponentIcons; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.TextUtil; + +public class ComponentTreeRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent( + JTree tree, + Object value, + boolean sel, + boolean expanded, + boolean leaf, + int row, + boolean hasFocus) { + + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + + // Set icon + setIcon(ComponentIcons.getSmallIcon(value.getClass())); + + // Set tooltip + RocketComponent c = (RocketComponent) value; + String comment = c.getComment().trim(); + if (comment.length() > 0) { + comment = TextUtil.htmlEncode(comment); + comment = "" + comment.replace("\n", "
"); + this.setToolTipText(comment); + } else { + this.setToolTipText(null); + } + + return this; + } + +} diff --git a/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java new file mode 100644 index 00000000..4855112b --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java @@ -0,0 +1,362 @@ +package net.sf.openrocket.gui.main.componenttree; + +import java.awt.Point; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; +import java.util.Arrays; + +import javax.swing.JComponent; +import javax.swing.JTree; +import javax.swing.SwingUtilities; +import javax.swing.TransferHandler; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; + +/** + * A TransferHandler that handles dragging components from and to a ComponentTree. + * Supports both moving and copying (only copying when dragging to a different rocket). + * + * @author Sampo Niskanen + */ +public class ComponentTreeTransferHandler extends TransferHandler { + + private static final LogHelper log = Application.getLogger(); + + private final OpenRocketDocument document; + + /** + * Sole constructor. + * + * @param document the document this handler will drop to, used for undo actions. + */ + public ComponentTreeTransferHandler(OpenRocketDocument document) { + this.document = document; + } + + @Override + public int getSourceActions(JComponent comp) { + return COPY_OR_MOVE; + } + + @Override + public Transferable createTransferable(JComponent component) { + if (!(component instanceof JTree)) { + throw new BugException("TransferHandler called with component " + component); + } + + JTree tree = (JTree) component; + TreePath path = tree.getSelectionPath(); + if (path == null) { + return null; + } + + RocketComponent c = ComponentTreeModel.componentFromPath(path); + if (c instanceof Rocket) { + log.info("Attempting to create transferable from Rocket"); + return null; + } + + log.info("Creating transferable from component " + c.getComponentName()); + return new RocketComponentTransferable(c); + } + + + + + @Override + public void exportDone(JComponent comp, Transferable trans, int action) { + // Removal from the old place is implemented already in import, so do nothing + } + + + + @Override + public boolean canImport(TransferHandler.TransferSupport support) { + SourceTarget data = getSourceAndTarget(support); + + if (data == null) { + return false; + } + + boolean allowed = data.destParent.isCompatible(data.child); + log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed); + + // If drag-dropping to another rocket always copy + if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) { + support.setDropAction(COPY); + } + + return allowed; + } + + + + @Override + public boolean importData(TransferHandler.TransferSupport support) { + + // We currently only support drop, not paste + if (!support.isDrop()) { + log.warn("Import action is not a drop action"); + return false; + } + + // Sun JRE silently ignores any RuntimeExceptions in importData, yeech! + try { + + SourceTarget data = getSourceAndTarget(support); + + // Check what action to perform + int action = support.getDropAction(); + if (data.srcParent.getRoot() != data.destParent.getRoot()) { + // If drag-dropping to another rocket always copy + log.info("Performing DnD between different rockets, forcing copy action"); + action = TransferHandler.COPY; + } + + + // Check whether move action would be a no-op + if ((action == MOVE) && (data.srcParent == data.destParent) && + (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) { + log.user("Dropped component at the same place as previously: " + data); + return false; + } + + + switch (action) { + case MOVE: + log.user("Performing DnD move operation: " + data); + + // If parents are the same, check whether removing the child changes the insert position + int index = data.destIndex; + if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) { + index--; + } + + // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one + try { + document.startUndo("Move component"); + try { + data.srcParent.getRocket().freeze(); + data.srcParent.removeChild(data.srcIndex); + data.destParent.addChild(data.child, index); + } finally { + data.srcParent.getRocket().thaw(); + } + } finally { + document.stopUndo(); + } + return true; + + case COPY: + log.user("Performing DnD copy operation: " + data); + RocketComponent copy = data.child.copy(); + try { + document.startUndo("Copy component"); + data.destParent.addChild(copy, data.destIndex); + } finally { + document.stopUndo(); + } + return true; + + default: + log.warn("Unknown transfer action " + action); + return false; + } + + } catch (final RuntimeException e) { + // Open error dialog later if an exception has occurred + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + Application.getExceptionHandler().handleErrorCondition(e); + } + }); + return false; + } + } + + + + /** + * Fetch the source and target for the DnD action. This method does not perform + * checks on whether this action is allowed based on component positioning rules. + * + * @param support the transfer support + * @return the source and targer, or null if invalid. + */ + private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) { + // We currently only support drop, not paste + if (!support.isDrop()) { + log.warn("Import action is not a drop action"); + return null; + } + + // we only import RocketComponentTransferable + if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) { + log.debug("Attempting to import data with data flavors " + + Arrays.toString(support.getTransferable().getTransferDataFlavors())); + return null; + } + + // Fetch the drop location and convert it to work around bug 6560955 + JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation(); + if (dl.getPath() == null) { + log.debug("No drop path location available"); + return null; + } + MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl); + + + // Fetch the transferred component (child component) + Transferable transferable = support.getTransferable(); + RocketComponent child; + + try { + child = (RocketComponent) transferable.getTransferData( + RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR); + } catch (IOException e) { + throw new BugException(e); + } catch (UnsupportedFlavorException e) { + throw new BugException(e); + } + + + // Get the source component & index + RocketComponent srcParent = child.getParent(); + if (srcParent == null) { + log.debug("Attempting to drag root component"); + return null; + } + int srcIndex = srcParent.getChildPosition(child); + + + // Get destination component & index + RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path); + int destIndex = location.index; + if (destIndex < 0) { + destIndex = 0; + } + + return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child); + } + + private class SourceTarget { + private final RocketComponent srcParent; + private final int srcIndex; + private final RocketComponent destParent; + private final int destIndex; + private final RocketComponent child; + + public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex, + RocketComponent child) { + this.srcParent = srcParent; + this.srcIndex = srcIndex; + this.destParent = destParent; + this.destIndex = destIndex; + this.child = child; + } + + @Override + public String toString() { + return "[" + + "srcParent=" + srcParent.getComponentName() + + ", srcIndex=" + srcIndex + + ", destParent=" + destParent.getComponentName() + + ", destIndex=" + destIndex + + ", child=" + child.getComponentName() + + "]"; + } + + } + + + + /** + * Convert the JTree drop location in order to work around bug 6560955 + * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955). + *

+ * This method analyzes whether the user is dropping on top of the last component + * of a subtree or the next item in the tree. The case to fix must fulfill the following + * requirements: + *

    + *
  • The node before the current insertion node is not a leaf node + *
  • The drop point is on top of the last node of that node + *
+ *

+ * This does not fix the visual clue provided to the user, but fixes the actual drop location. + * + * @param tree the JTree in question + * @param location the original drop location + * @return the updated drop location + */ + private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) { + + final TreePath originalPath = location.getPath(); + final int originalIndex = location.getChildIndex(); + + if (originalPath == null || originalIndex <= 0) { + return new MyDropLocation(location); + } + + // Check whether previous node is a leaf node + TreeModel model = tree.getModel(); + Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1); + if (model.isLeaf(previousNode)) { + return new MyDropLocation(location); + } + + // Find node on top of which the drop occurred + Point point = location.getDropPoint(); + TreePath dropPath = tree.getPathForLocation(point.x, point.y); + if (dropPath == null) { + return new MyDropLocation(location); + } + + // Check whether previousNode is in the ancestry of the actual drop location + boolean inAncestry = false; + for (Object o : dropPath.getPath()) { + if (o == previousNode) { + inAncestry = true; + break; + } + } + if (!inAncestry) { + return new MyDropLocation(location); + } + + // The bug has occurred - insert after the actual drop location + TreePath correctInsertPath = dropPath.getParentPath(); + int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(), + dropPath.getLastPathComponent()) + 1; + + log.verbose("Working around Sun JRE bug 6560955: " + + "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex + + " into path=" + ComponentTreeModel.pathToString(correctInsertPath) + + " index=" + correctInsertIndex); + + return new MyDropLocation(correctInsertPath, correctInsertIndex); + } + + private class MyDropLocation { + private final TreePath path; + private final int index; + + public MyDropLocation(JTree.DropLocation location) { + this(location.getPath(), location.getChildIndex()); + } + + public MyDropLocation(TreePath path, int index) { + this.path = path; + this.index = index; + } + + } +} diff --git a/core/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java b/core/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java new file mode 100644 index 00000000..e9379dae --- /dev/null +++ b/core/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java @@ -0,0 +1,47 @@ +package net.sf.openrocket.gui.main.componenttree; + +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.io.IOException; + +import net.sf.openrocket.rocketcomponent.RocketComponent; + +/** + * A transferable that provides a reference to a (JVM-local) RocketComponent object. + * + * @author Sampo Niskanen + */ +public class RocketComponentTransferable implements Transferable { + + public static final DataFlavor ROCKET_COMPONENT_DATA_FLAVOR = new DataFlavor( + DataFlavor.javaJVMLocalObjectMimeType + "; class=" + RocketComponent.class.getCanonicalName(), + "OpenRocket component"); + + + private final RocketComponent component; + + public RocketComponentTransferable(RocketComponent component) { + this.component = component; + } + + + @Override + public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { + if (!isDataFlavorSupported(flavor)) { + throw new UnsupportedFlavorException(flavor); + } + return component; + } + + @Override + public DataFlavor[] getTransferDataFlavors() { + return new DataFlavor[] { ROCKET_COMPONENT_DATA_FLAVOR }; + } + + @Override + public boolean isDataFlavorSupported(DataFlavor flavor) { + return flavor.equals(ROCKET_COMPONENT_DATA_FLAVOR); + } + +} diff --git a/core/src/net/sf/openrocket/gui/plot/Axis.java b/core/src/net/sf/openrocket/gui/plot/Axis.java new file mode 100644 index 00000000..3f97ad36 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/plot/Axis.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.gui.plot; + +import net.sf.openrocket.util.BugException; + +public class Axis implements Cloneable { + + private double minValue = Double.NaN; + private double maxValue = Double.NaN; + + + + public void addBound(double value) { + + if (value < minValue || Double.isNaN(minValue)) { + minValue = value; + } + if (value > maxValue || Double.isNaN(maxValue)) { + maxValue = value; + } + + } + + + public double getMinValue() { + return minValue; + } + + public double getMaxValue() { + return maxValue; + } + + public double getRangeLength() { + return maxValue - minValue; + } + + public void reset() { + minValue = Double.NaN; + maxValue = Double.NaN; + } + + + + @Override + public Axis clone() { + try { + + return (Axis) super.clone(); + + } catch (CloneNotSupportedException e) { + throw new BugException("BUG! Could not clone()."); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java b/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java new file mode 100644 index 00000000..bd58782f --- /dev/null +++ b/core/src/net/sf/openrocket/gui/plot/PlotConfiguration.java @@ -0,0 +1,754 @@ +package net.sf.openrocket.gui.plot; + +import java.util.EnumSet; +import java.util.List; +import java.util.Set; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Pair; + + +public class PlotConfiguration implements Cloneable { + + private static final Translator trans = Application.getTranslator(); + + public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS; + static { + ArrayList configs = new ArrayList(); + PlotConfiguration config; + + //// Vertical motion vs. time + config = new PlotConfiguration(trans.get("PlotConfiguration.Verticalmotion")); + config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); + config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z); + config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_Z); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + //// Total motion vs. time + config = new PlotConfiguration(trans.get("PlotConfiguration.Totalmotion")); + config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); + config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL); + config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_TOTAL); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + //// Flight side profile + config = new PlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X); + config.addPlotDataType(FlightDataType.TYPE_ALTITUDE); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + //// Stability vs. time + config = new PlotConfiguration("Stability vs. time"); + config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0); + config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1); + config.addPlotDataType(FlightDataType.TYPE_CG_LOCATION, 1); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + //// Drag coefficients vs. Mach number + config = new PlotConfiguration("Drag coefficients vs. Mach number", + FlightDataType.TYPE_MACH_NUMBER); + config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0); + config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0); + config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0); + config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0); + configs.add(config); + + //// Roll characteristics + config = new PlotConfiguration("Roll characteristics"); + config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0); + config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1); + config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1); + config.addPlotDataType(FlightDataType.TYPE_ROLL_DAMPING_COEFF, 1); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.LAUNCHROD, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + //// Angle of attack and orientation vs. time + config = new PlotConfiguration("Angle of attack and orientation vs. time"); + config.addPlotDataType(FlightDataType.TYPE_AOA, 0); + config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI); + config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_THETA); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + //// Simulation time step and computation time + config = new PlotConfiguration("Simulation time step and computation time"); + config.addPlotDataType(FlightDataType.TYPE_TIME_STEP); + config.addPlotDataType(FlightDataType.TYPE_COMPUTATION_TIME); + config.setEvent(FlightEvent.Type.IGNITION, true); + config.setEvent(FlightEvent.Type.BURNOUT, true); + config.setEvent(FlightEvent.Type.APOGEE, true); + config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); + config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); + config.setEvent(FlightEvent.Type.GROUND_HIT, true); + configs.add(config); + + DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]); + } + + + + /** Bonus given for the first type being on the first axis */ + private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0; + + /** + * Bonus given if the first axis includes zero (to prefer first axis having zero over + * the others) + */ + private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0; + + /** Bonus given for a common zero point on left and right axes. */ + private static final double BONUS_COMMON_ZERO = 40.0; + + /** Bonus given for only using a single axis. */ + private static final double BONUS_ONLY_ONE_AXIS = 50.0; + + + private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range + + + + /** The data types to be plotted. */ + private ArrayList plotDataTypes = new ArrayList(); + + private ArrayList plotDataUnits = new ArrayList(); + + /** The corresponding Axis on which they will be plotted, or null to auto-select. */ + private ArrayList plotDataAxes = new ArrayList(); + + private EnumSet events = EnumSet.noneOf(FlightEvent.Type.class); + + /** The domain (x) axis. */ + private FlightDataType domainAxisType = null; + private Unit domainAxisUnit = null; + + + /** All available axes. */ + private final int axesCount; + private ArrayList allAxes = new ArrayList(); + + + + private String name = null; + + + + public PlotConfiguration() { + this(null, FlightDataType.TYPE_TIME); + } + + public PlotConfiguration(String name) { + this(name, FlightDataType.TYPE_TIME); + } + + public PlotConfiguration(String name, FlightDataType domainType) { + this.name = name; + // Two axes + allAxes.add(new Axis()); + allAxes.add(new Axis()); + axesCount = 2; + + setDomainAxisType(domainType); + } + + + + + + public FlightDataType getDomainAxisType() { + return domainAxisType; + } + + public void setDomainAxisType(FlightDataType type) { + boolean setUnit; + + if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup()) + setUnit = false; + else + setUnit = true; + + domainAxisType = type; + if (setUnit) + domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit(); + } + + public Unit getDomainAxisUnit() { + return domainAxisUnit; + } + + public void setDomainAxisUnit(Unit u) { + if (!domainAxisType.getUnitGroup().contains(u)) { + throw new IllegalArgumentException("Setting unit " + u + " to type " + domainAxisType); + } + domainAxisUnit = u; + } + + + + public void addPlotDataType(FlightDataType type) { + plotDataTypes.add(type); + plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); + plotDataAxes.add(-1); + } + + public void addPlotDataType(FlightDataType type, int axis) { + if (axis >= axesCount) { + throw new IllegalArgumentException("Axis index too large"); + } + plotDataTypes.add(type); + plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); + plotDataAxes.add(axis); + } + + + + + public void setPlotDataType(int index, FlightDataType type) { + FlightDataType origType = plotDataTypes.get(index); + plotDataTypes.set(index, type); + + if (origType.getUnitGroup() != type.getUnitGroup()) { + plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit()); + } + } + + public void setPlotDataUnit(int index, Unit unit) { + if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) { + throw new IllegalArgumentException("Attempting to set unit " + unit + " to group " + + plotDataTypes.get(index).getUnitGroup()); + } + plotDataUnits.set(index, unit); + } + + public void setPlotDataAxis(int index, int axis) { + if (axis >= axesCount) { + throw new IllegalArgumentException("Axis index too large"); + } + plotDataAxes.set(index, axis); + } + + + public void setPlotDataType(int index, FlightDataType type, Unit unit, int axis) { + if (axis >= axesCount) { + throw new IllegalArgumentException("Axis index too large"); + } + plotDataTypes.set(index, type); + plotDataUnits.set(index, unit); + plotDataAxes.set(index, axis); + } + + public void removePlotDataType(int index) { + plotDataTypes.remove(index); + plotDataUnits.remove(index); + plotDataAxes.remove(index); + } + + + + public FlightDataType getType(int index) { + return plotDataTypes.get(index); + } + + public Unit getUnit(int index) { + return plotDataUnits.get(index); + } + + public int getAxis(int index) { + return plotDataAxes.get(index); + } + + public int getTypeCount() { + return plotDataTypes.size(); + } + + + /// Events + + public Set getActiveEvents() { + return events.clone(); + } + + public void setEvent(FlightEvent.Type type, boolean active) { + if (active) { + events.add(type); + } else { + events.remove(type); + } + } + + public boolean isEventActive(FlightEvent.Type type) { + return events.contains(type); + } + + + + + + public List getAllAxes() { + List list = new ArrayList(); + list.addAll(allAxes); + return list; + } + + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Returns the name of this PlotConfiguration. + */ + @Override + public String toString() { + return name; + } + + + + /** + * Find the best combination of the auto-selectable axes. + * + * @return a new PlotConfiguration with the best fitting auto-selected axes and + * axes ranges selected. + */ + public PlotConfiguration fillAutoAxes(FlightDataBranch data) { + PlotConfiguration config = recursiveFillAutoAxes(data).getU(); + System.out.println("BEST FOUND, fitting"); + config.fitAxes(data); + return config; + } + + + + + /** + * Recursively search for the best combination of the auto-selectable axes. + * This is a brute-force search method. + * + * @return a new PlotConfiguration with the best fitting auto-selected axes and + * axes ranges selected, and the goodness value + */ + private Pair recursiveFillAutoAxes(FlightDataBranch data) { + + // Create copy to fill in + PlotConfiguration copy = this.clone(); + + int autoindex; + for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) { + if (plotDataAxes.get(autoindex) < 0) + break; + } + + + if (autoindex >= plotDataAxes.size()) { + // All axes have been assigned, just return since we are already the best + return new Pair(copy, copy.getGoodnessValue(data)); + } + + + // Set the auto-selected index one at a time and choose the best one + PlotConfiguration best = null; + double bestValue = Double.NEGATIVE_INFINITY; + for (int i = 0; i < axesCount; i++) { + copy.plotDataAxes.set(autoindex, i); + Pair result = copy.recursiveFillAutoAxes(data); + if (result.getV() > bestValue) { + best = result.getU(); + bestValue = result.getV(); + } + } + + return new Pair(best, bestValue); + } + + + + + + /** + * Fit the axes to hold the provided data. All of the plotDataAxis elements must + * be non-negative. + *

+ * NOTE: This method assumes that only two axes are used. + */ + protected void fitAxes(FlightDataBranch data) { + + // Reset axes + for (Axis a : allAxes) { + a.reset(); + } + + // Add full range to the axes + int length = plotDataTypes.size(); + for (int i = 0; i < length; i++) { + FlightDataType type = plotDataTypes.get(i); + Unit unit = plotDataUnits.get(i); + int index = plotDataAxes.get(i); + if (index < 0) { + throw new IllegalStateException("fitAxes called with auto-selected axis"); + } + Axis axis = allAxes.get(index); + + double min = unit.toUnit(data.getMinimum(type)); + double max = unit.toUnit(data.getMaximum(type)); + + axis.addBound(min); + axis.addBound(max); + } + + // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close + for (Axis a : allAxes) { + if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) { + a.addBound(a.getMinValue() - 1); + a.addBound(a.getMaxValue() + 1); + } + + double addition = a.getRangeLength() * 0.03; + a.addBound(a.getMinValue() - addition); + a.addBound(a.getMaxValue() + addition); + + double dist; + dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue())); + if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) { + a.addBound(0); + } + } + + + // Check whether to use a common zero + Axis left = allAxes.get(0); + Axis right = allAxes.get(1); + + if (left.getMinValue() > 0 || left.getMaxValue() < 0 || + right.getMinValue() > 0 || right.getMaxValue() < 0 || + Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue())) + return; + + + + //// Compute common zero + // TODO: MEDIUM: This algorithm may require tweaking + + double min1 = left.getMinValue(); + double max1 = left.getMaxValue(); + double min2 = right.getMinValue(); + double max2 = right.getMaxValue(); + + // Calculate and round scaling factor + double scale = Math.max(left.getRangeLength(), right.getRangeLength()) / + Math.min(left.getRangeLength(), right.getRangeLength()); + + System.out.println("Scale: " + scale); + + scale = roundScale(scale); + if (right.getRangeLength() > left.getRangeLength()) { + scale = 1 / scale; + } + System.out.println("Rounded scale: " + scale); + + // Scale right axis, enlarge axes if necessary and scale back + min2 *= scale; + max2 *= scale; + min1 = Math.min(min1, min2); + min2 = min1; + max1 = Math.max(max1, max2); + max2 = max1; + min2 /= scale; + max2 /= scale; + + + + // Scale to unit length + // double scale1 = left.getRangeLength(); + // double scale2 = right.getRangeLength(); + // + // double min1 = left.getMinValue() / scale1; + // double max1 = left.getMaxValue() / scale1; + // double min2 = right.getMinValue() / scale2; + // double max2 = right.getMaxValue() / scale2; + // + // // Combine unit ranges + // min1 = MathUtil.min(min1, min2); + // min2 = min1; + // max1 = MathUtil.max(max1, max2); + // max2 = max1; + // + // // Scale up + // min1 *= scale1; + // max1 *= scale1; + // min2 *= scale2; + // max2 *= scale2; + // + // // Compute common scale + // double range1 = max1-min1; + // double range2 = max2-min2; + // + // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2); + // double roundScale = roundScale(scale); + // + // if (range2 < range1) { + // if (roundScale < scale) { + // min2 = min1 / roundScale; + // max2 = max1 / roundScale; + // } else { + // min1 = min2 * roundScale; + // max1 = max2 * roundScale; + // } + // } else { + // if (roundScale > scale) { + // min2 = min1 * roundScale; + // max2 = max1 * roundScale; + // } else { + // min1 = min2 / roundScale; + // max1 = max2 / roundScale; + // } + // } + + // Apply scale + left.addBound(min1); + left.addBound(max1); + right.addBound(min2); + right.addBound(max2); + + } + + + + private double roundScale(double scale) { + double mul = 1; + while (scale >= 10) { + scale /= 10; + mul *= 10; + } + while (scale < 1) { + scale *= 10; + mul /= 10; + } + + // 1 2 4 5 10 + + if (scale > 7.5) { + scale = 10; + } else if (scale > 4.5) { + scale = 5; + } else if (scale > 3) { + scale = 4; + } else if (scale > 1.5) { + scale = 2; + } else { + scale = 1; + } + return scale * mul; + } + + + + private double roundScaleUp(double scale) { + double mul = 1; + while (scale >= 10) { + scale /= 10; + mul *= 10; + } + while (scale < 1) { + scale *= 10; + mul /= 10; + } + + if (scale > 5) { + scale = 10; + } else if (scale > 4) { + scale = 5; + } else if (scale > 2) { + scale = 4; + } else if (scale > 1) { + scale = 2; + } else { + scale = 1; + } + return scale * mul; + } + + + private double roundScaleDown(double scale) { + double mul = 1; + while (scale >= 10) { + scale /= 10; + mul *= 10; + } + while (scale < 1) { + scale *= 10; + mul /= 10; + } + + if (scale > 5) { + scale = 5; + } else if (scale > 4) { + scale = 4; + } else if (scale > 2) { + scale = 2; + } else { + scale = 1; + } + return scale * mul; + } + + + + /** + * Fits the axis ranges to the data and returns the "goodness value" of this + * selection of axes. All plotDataAxis elements must be non-null. + *

+ * NOTE: This method assumes that all data can fit into the axes ranges and + * that only two axes are used. + * + * @return a "goodness value", the larger the better. + */ + protected double getGoodnessValue(FlightDataBranch data) { + double goodness = 0; + int length = plotDataTypes.size(); + + // Fit the axes ranges to the data + fitAxes(data); + + /* + * Calculate goodness of ranges. 100 points is given if the values fill the + * entire range, 0 if they fill none of it. + */ + for (int i = 0; i < length; i++) { + FlightDataType type = plotDataTypes.get(i); + Unit unit = plotDataUnits.get(i); + int index = plotDataAxes.get(i); + if (index < 0) { + throw new IllegalStateException("getGoodnessValue called with auto-selected axis"); + } + Axis axis = allAxes.get(index); + + double min = unit.toUnit(data.getMinimum(type)); + double max = unit.toUnit(data.getMaximum(type)); + if (Double.isNaN(min) || Double.isNaN(max)) + continue; + if (MathUtil.equals(min, max)) + continue; + + double d = (max - min) / axis.getRangeLength(); + d = MathUtil.safeSqrt(d); // Prioritize small ranges + goodness += d * 100.0; + } + + + /* + * Add extra points for specific things. + */ + + // A little for the first type being on the first axis + if (plotDataAxes.get(0) == 0) + goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS; + + // A little bonus if the first axis contains zero + Axis left = allAxes.get(0); + if (left.getMinValue() <= 0 && left.getMaxValue() >= 0) + goodness += BONUS_FIRST_AXIS_HAS_ZERO; + + // A boost if a common zero was used in the ranging + Axis right = allAxes.get(1); + if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 && + right.getMinValue() <= 0 && right.getMaxValue() >= 0) + goodness += BONUS_COMMON_ZERO; + + // A boost if only one axis is used + if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue())) + goodness += BONUS_ONLY_ONE_AXIS; + + return goodness; + } + + + + /** + * Reset the units of this configuration to the default units. Returns this + * PlotConfiguration. + * + * @return this PlotConfiguration. + */ + public PlotConfiguration resetUnits() { + for (int i = 0; i < plotDataTypes.size(); i++) { + plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit()); + } + return this; + } + + + + + @Override + public PlotConfiguration clone() { + try { + + PlotConfiguration copy = (PlotConfiguration) super.clone(); + + // Shallow-clone all immutable lists + copy.plotDataTypes = this.plotDataTypes.clone(); + copy.plotDataAxes = this.plotDataAxes.clone(); + copy.plotDataUnits = this.plotDataUnits.clone(); + copy.events = this.events.clone(); + + // Deep-clone all Axis since they are mutable + copy.allAxes = new ArrayList(); + for (Axis a : this.allAxes) { + copy.allAxes.add(a.clone()); + } + + return copy; + + + } catch (CloneNotSupportedException e) { + throw new BugException("BUG! Could not clone()."); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java b/core/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java new file mode 100644 index 00000000..d26ea7c2 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java @@ -0,0 +1,583 @@ +package net.sf.openrocket.gui.plot; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.geom.Line2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.components.StyledLabel; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.startup.Preferences; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.annotations.XYImageAnnotation; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.ValueAxis; +import org.jfree.chart.plot.Marker; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.plot.ValueMarker; +import org.jfree.chart.plot.XYPlot; +import org.jfree.chart.renderer.xy.StandardXYItemRenderer; +import org.jfree.chart.title.TextTitle; +import org.jfree.data.Range; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +import org.jfree.text.TextUtilities; +import org.jfree.ui.LengthAdjustmentType; +import org.jfree.ui.RectangleAnchor; +import org.jfree.ui.TextAnchor; + +/** + * Dialog that shows a plot of a simulation results based on user options. + * + * @author Sampo Niskanen + */ +public class SimulationPlotDialog extends JDialog { + + private static final float PLOT_STROKE_WIDTH = 1.5f; + private static final Translator trans = Application.getTranslator(); + + private static final Color DEFAULT_EVENT_COLOR = new Color(0, 0, 0); + private static final Map EVENT_COLORS = + new HashMap(); + static { + EVENT_COLORS.put(FlightEvent.Type.LAUNCH, new Color(255, 0, 0)); + EVENT_COLORS.put(FlightEvent.Type.LIFTOFF, new Color(0, 80, 196)); + EVENT_COLORS.put(FlightEvent.Type.LAUNCHROD, new Color(0, 100, 80)); + EVENT_COLORS.put(FlightEvent.Type.IGNITION, new Color(230, 130, 15)); + EVENT_COLORS.put(FlightEvent.Type.BURNOUT, new Color(80, 55, 40)); + EVENT_COLORS.put(FlightEvent.Type.EJECTION_CHARGE, new Color(80, 55, 40)); + EVENT_COLORS.put(FlightEvent.Type.STAGE_SEPARATION, new Color(80, 55, 40)); + EVENT_COLORS.put(FlightEvent.Type.APOGEE, new Color(15, 120, 15)); + EVENT_COLORS.put(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, new Color(0, 0, 128)); + EVENT_COLORS.put(FlightEvent.Type.GROUND_HIT, new Color(0, 0, 0)); + EVENT_COLORS.put(FlightEvent.Type.SIMULATION_END, new Color(128, 0, 0)); + } + + private static final Map EVENT_IMAGES = + new HashMap(); + static { + loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png"); + loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png"); + loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png"); + loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png"); + loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png"); + loadImage(FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png"); + loadImage(FlightEvent.Type.STAGE_SEPARATION, + "pix/eventicons/event-stage-separation.png"); + loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png"); + loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, + "pix/eventicons/event-recovery-device-deployment.png"); + loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png"); + loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png"); + } + + private static void loadImage(FlightEvent.Type type, String file) { + InputStream is; + + is = ClassLoader.getSystemResourceAsStream(file); + if (is == null) { + System.out.println("ERROR: File " + file + " not found!"); + return; + } + + try { + Image image = ImageIO.read(is); + EVENT_IMAGES.put(type, image); + } catch (IOException ignore) { + ignore.printStackTrace(); + } + } + + + + + private final List renderers = + new ArrayList(); + + private SimulationPlotDialog(Window parent, Simulation simulation, PlotConfiguration config) { + //// Flight data plot + super(parent, trans.get("PlotDialog.title.Flightdataplot")); + this.setModalityType(ModalityType.DOCUMENT_MODAL); + + final boolean initialShowPoints = Application.getPreferences().getBoolean(Preferences.PLOT_SHOW_POINTS, false); + + + // Fill the auto-selections + FlightDataBranch branch = simulation.getSimulatedData().getBranch(0); + PlotConfiguration filled = config.fillAutoAxes(branch); + List axes = filled.getAllAxes(); + + + // Create the data series for both axes + XYSeriesCollection[] data = new XYSeriesCollection[2]; + data[0] = new XYSeriesCollection(); + data[1] = new XYSeriesCollection(); + + + // Get the domain axis type + final FlightDataType domainType = filled.getDomainAxisType(); + final Unit domainUnit = filled.getDomainAxisUnit(); + if (domainType == null) { + throw new IllegalArgumentException("Domain axis type not specified."); + } + List x = branch.get(domainType); + + + // Get plot length (ignore trailing NaN's) + int typeCount = filled.getTypeCount(); + int dataLength = 0; + for (int i = 0; i < typeCount; i++) { + FlightDataType type = filled.getType(i); + List y = branch.get(type); + + for (int j = dataLength; j < y.size(); j++) { + if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j))) + dataLength = j; + } + } + dataLength = Math.min(dataLength, x.size()); + + + // Create the XYSeries objects from the flight data and store into the collections + String[] axisLabel = new String[2]; + for (int i = 0; i < typeCount; i++) { + // Get info + FlightDataType type = filled.getType(i); + Unit unit = filled.getUnit(i); + int axis = filled.getAxis(i); + String name = getLabel(type, unit); + + // Store data in provided units + List y = branch.get(type); + XYSeries series = new XYSeries(name, false, true); + for (int j = 0; j < dataLength; j++) { + series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j))); + } + data[axis].addSeries(series); + + // Update axis label + if (axisLabel[axis] == null) + axisLabel[axis] = type.getName(); + else + axisLabel[axis] += "; " + type.getName(); + } + + + // Create the chart using the factory to get all default settings + JFreeChart chart = ChartFactory.createXYLineChart( + //// Simulated flight + trans.get("PlotDialog.Chart.Simulatedflight"), + null, + null, + null, + PlotOrientation.VERTICAL, + true, + true, + false + ); + + chart.addSubtitle(new TextTitle(config.getName())); + + // Add the data and formatting to the plot + XYPlot plot = chart.getXYPlot(); + int axisno = 0; + for (int i = 0; i < 2; i++) { + // Check whether axis has any data + if (data[i].getSeriesCount() > 0) { + // Create and set axis + double min = axes.get(i).getMinValue(); + double max = axes.get(i).getMaxValue(); + NumberAxis axis = new PresetNumberAxis(min, max); + axis.setLabel(axisLabel[i]); + // axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue()); + plot.setRangeAxis(axisno, axis); + + // Add data and map to the axis + plot.setDataset(axisno, data[i]); + ModifiedXYItemRenderer r = new ModifiedXYItemRenderer(); + r.setBaseShapesVisible(initialShowPoints); + r.setBaseShapesFilled(true); + for (int j = 0; j < data[i].getSeriesCount(); j++) { + r.setSeriesStroke(j, new BasicStroke(PLOT_STROKE_WIDTH)); + } + renderers.add(r); + plot.setRenderer(axisno, r); + plot.mapDatasetToRangeAxis(axisno, axisno); + axisno++; + } + } + + plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit)); + plot.addDomainMarker(new ValueMarker(0)); + plot.addRangeMarker(new ValueMarker(0)); + + + + // Create list of events to show (combine event too close to each other) + ArrayList timeList = new ArrayList(); + ArrayList eventList = new ArrayList(); + ArrayList colorList = new ArrayList(); + ArrayList imageList = new ArrayList(); + + HashSet typeSet = new HashSet(); + + double prevTime = -100; + String text = null; + Color color = null; + Image image = null; + + List events = branch.getEvents(); + for (int i = 0; i < events.size(); i++) { + FlightEvent event = events.get(i); + double t = event.getTime(); + FlightEvent.Type type = event.getType(); + + if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) { + if (Math.abs(t - prevTime) <= 0.01) { + + if (!typeSet.contains(type)) { + text = text + ", " + type.toString(); + color = getEventColor(type); + image = EVENT_IMAGES.get(type); + typeSet.add(type); + } + + } else { + + if (text != null) { + timeList.add(prevTime); + eventList.add(text); + colorList.add(color); + imageList.add(image); + } + prevTime = t; + text = type.toString(); + color = getEventColor(type); + image = EVENT_IMAGES.get(type); + typeSet.clear(); + typeSet.add(type); + + } + } + } + if (text != null) { + timeList.add(prevTime); + eventList.add(text); + colorList.add(color); + imageList.add(image); + } + + + // Create the event markers + + if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) { + + // Domain time is plotted as vertical markers + for (int i = 0; i < eventList.size(); i++) { + double t = timeList.get(i); + String event = eventList.get(i); + color = colorList.get(i); + + ValueMarker m = new ValueMarker(t); + m.setLabel(event); + m.setPaint(color); + m.setLabelPaint(color); + m.setAlpha(0.7f); + plot.addDomainMarker(m); + } + + } else { + + // Other domains are plotted as image annotations + List time = branch.get(FlightDataType.TYPE_TIME); + List domain = branch.get(config.getDomainAxisType()); + + for (int i = 0; i < eventList.size(); i++) { + final double t = timeList.get(i); + String event = eventList.get(i); + image = imageList.get(i); + + if (image == null) + continue; + + // Calculate index and interpolation position a + final double a; + int tindex = Collections.binarySearch(time, t); + if (tindex < 0) { + tindex = -tindex - 1; + } + if (tindex >= time.size()) { + // index greater than largest value in time list + tindex = time.size() - 1; + a = 0; + } else if (tindex <= 0) { + // index smaller than smallest value in time list + tindex = 0; + a = 0; + } else { + tindex--; + double t1 = time.get(tindex); + double t2 = time.get(tindex + 1); + + if ((t1 > t) || (t2 < t)) { + throw new BugException("t1=" + t1 + " t2=" + t2 + " t=" + t); + } + + if (MathUtil.equals(t1, t2)) { + a = 0; + } else { + a = 1 - (t - t1) / (t2 - t1); + } + } + + double xcoord; + if (a == 0) { + xcoord = domain.get(tindex); + } else { + xcoord = a * domain.get(tindex) + (1 - a) * domain.get(tindex + 1); + } + + for (int index = 0; index < config.getTypeCount(); index++) { + FlightDataType type = config.getType(index); + List range = branch.get(type); + + // Image annotations are not supported on the right-side axis + // TODO: LOW: Can this be achieved by JFreeChart? + if (filled.getAxis(index) != SimulationPlotPanel.LEFT) { + continue; + } + + double ycoord; + if (a == 0) { + ycoord = range.get(tindex); + } else { + ycoord = a * range.get(tindex) + (1 - a) * range.get(tindex + 1); + } + + // Convert units + xcoord = config.getDomainAxisUnit().toUnit(xcoord); + ycoord = config.getUnit(index).toUnit(ycoord); + + XYImageAnnotation annotation = + new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); + annotation.setToolTipText(event); + plot.addAnnotation(annotation); + } + } + } + + + // Create the dialog + + JPanel panel = new JPanel(new MigLayout("fill")); + this.add(panel); + + ChartPanel chartPanel = new ChartPanel(chart, + false, // properties + true, // save + false, // print + true, // zoom + true); // tooltips + chartPanel.setMouseWheelEnabled(true); + chartPanel.setEnforceFileExtensions(true); + chartPanel.setInitialDelay(500); + + chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); + + panel.add(chartPanel, "grow, wrap 20lp"); + + //// Show data points + final JCheckBox check = new JCheckBox(trans.get("PlotDialog.CheckBox.Showdatapoints")); + check.setSelected(initialShowPoints); + check.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + boolean show = check.isSelected(); + Application.getPreferences().putBoolean(Preferences.PLOT_SHOW_POINTS, show); + for (ModifiedXYItemRenderer r : renderers) { + r.setBaseShapesVisible(show); + } + } + }); + panel.add(check, "split, left"); + + + JLabel label = new StyledLabel(trans.get("PlotDialog.lbl.Chart"), -2); + panel.add(label, "gapleft para"); + + + panel.add(new JPanel(), "growx"); + + //// Close button + JButton button = new JButton(trans.get("dlg.but.close")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + SimulationPlotDialog.this.dispose(); + } + }); + panel.add(button, "right"); + + this.setLocationByPlatform(true); + this.pack(); + + GUIUtil.setDisposableDialogOptions(this, button); + GUIUtil.rememberWindowSize(this); + } + + private String getLabel(FlightDataType type, Unit unit) { + String name = type.getName(); + if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) && + !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0) + name += " (" + unit.getUnit() + ")"; + return name; + } + + + + private class PresetNumberAxis extends NumberAxis { + private final double min; + private final double max; + + public PresetNumberAxis(double min, double max) { + this.min = min; + this.max = max; + autoAdjustRange(); + } + + @Override + protected void autoAdjustRange() { + this.setRange(min, max); + } + } + + + /** + * Static method that shows a plot with the specified parameters. + * + * @param parent the parent window, which will be blocked. + * @param simulation the simulation to plot. + * @param config the configuration of the plot. + */ + public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) { + new SimulationPlotDialog(parent, simulation, config).setVisible(true); + } + + + + private static Color getEventColor(FlightEvent.Type type) { + Color c = EVENT_COLORS.get(type); + if (c != null) + return c; + return DEFAULT_EVENT_COLOR; + } + + + + + + /** + * A modification to the standard renderer that renders the domain marker + * labels vertically instead of horizontally. + */ + private static class ModifiedXYItemRenderer extends StandardXYItemRenderer { + + @Override + public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis, + Marker marker, Rectangle2D dataArea) { + + if (!(marker instanceof ValueMarker)) { + // Use parent for all others + super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea); + return; + } + + /* + * Draw the normal marker, but with rotated text. + * Copied from the overridden method. + */ + ValueMarker vm = (ValueMarker) marker; + double value = vm.getValue(); + Range range = domainAxis.getRange(); + if (!range.contains(value)) { + return; + } + + double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge()); + + PlotOrientation orientation = plot.getOrientation(); + Line2D line = null; + if (orientation == PlotOrientation.HORIZONTAL) { + line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v); + } else { + line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY()); + } + + final Composite originalComposite = g2.getComposite(); + g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker + .getAlpha())); + g2.setPaint(marker.getPaint()); + g2.setStroke(marker.getStroke()); + g2.draw(line); + + String label = marker.getLabel(); + RectangleAnchor anchor = marker.getLabelAnchor(); + if (label != null) { + Font labelFont = marker.getLabelFont(); + g2.setFont(labelFont); + g2.setPaint(marker.getLabelPaint()); + Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2, + orientation, dataArea, line.getBounds2D(), marker + .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor); + + // Changed: + TextAnchor textAnchor = TextAnchor.TOP_RIGHT; + TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX() + 2, + (float) coordinates.getY(), textAnchor, + -Math.PI / 2, textAnchor); + } + g2.setComposite(originalComposite); + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java b/core/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java new file mode 100644 index 00000000..06544f52 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java @@ -0,0 +1,532 @@ +package net.sf.openrocket.gui.plot; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; +import java.util.Arrays; +import java.util.EnumSet; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.SwingUtilities; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.components.DescriptionArea; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.util.Utils; + +/** + * Panel that displays the simulation plot options to the user. + * + * @author Sampo Niskanen + */ +public class SimulationPlotPanel extends JPanel { + private static final Translator trans = Application.getTranslator(); + + // TODO: LOW: Should these be somewhere else? + public static final int AUTO = -1; + public static final int LEFT = 0; + public static final int RIGHT = 1; + + //// Auto + public static final String AUTO_NAME = trans.get("simplotpanel.AUTO_NAME"); + //// Left + public static final String LEFT_NAME = trans.get("simplotpanel.LEFT_NAME"); + //// Right + public static final String RIGHT_NAME = trans.get("simplotpanel.RIGHT_NAME"); + + //// Custom + private static final String CUSTOM = trans.get("simplotpanel.CUSTOM"); + + /** The "Custom" configuration - not to be used for anything other than the title. */ + private static final PlotConfiguration CUSTOM_CONFIGURATION; + static { + CUSTOM_CONFIGURATION = new PlotConfiguration(CUSTOM); + } + + /** The array of presets for the combo box. */ + private static final PlotConfiguration[] PRESET_ARRAY; + static { + PRESET_ARRAY = Arrays.copyOf(PlotConfiguration.DEFAULT_CONFIGURATIONS, + PlotConfiguration.DEFAULT_CONFIGURATIONS.length + 1); + PRESET_ARRAY[PRESET_ARRAY.length - 1] = CUSTOM_CONFIGURATION; + } + + + + /** The current default configuration, set each time a plot is made. */ + private static PlotConfiguration defaultConfiguration = + PlotConfiguration.DEFAULT_CONFIGURATIONS[0].resetUnits(); + + + private final Simulation simulation; + private final FlightDataType[] types; + private PlotConfiguration configuration; + + + private JComboBox configurationSelector; + + private JComboBox domainTypeSelector; + private UnitSelector domainUnitSelector; + + private JPanel typeSelectorPanel; + private FlightEventTableModel eventTableModel; + + + private int modifying = 0; + + + public SimulationPlotPanel(final Simulation simulation) { + super(new MigLayout("fill")); + + this.simulation = simulation; + if (simulation.getSimulatedData() == null || + simulation.getSimulatedData().getBranchCount() == 0) { + throw new IllegalArgumentException("Simulation contains no data."); + } + FlightDataBranch branch = simulation.getSimulatedData().getBranch(0); + types = branch.getTypes(); + + setConfiguration(defaultConfiguration); + + //// Configuration selector + + // Setup the combo box + configurationSelector = new JComboBox(PRESET_ARRAY); + for (PlotConfiguration config : PRESET_ARRAY) { + if (config.getName().equals(configuration.getName())) { + configurationSelector.setSelectedItem(config); + } + } + + configurationSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + PlotConfiguration conf = (PlotConfiguration) configurationSelector.getSelectedItem(); + if (conf == CUSTOM_CONFIGURATION) + return; + modifying++; + setConfiguration(conf.clone().resetUnits()); + updatePlots(); + modifying--; + } + }); + //// Preset plot configurations: + this.add(new JLabel(trans.get("simplotpanel.lbl.Presetplotconf")), "spanx, split"); + this.add(configurationSelector, "growx, wrap 20lp"); + + + + //// X axis + + //// X axis type: + this.add(new JLabel(trans.get("simplotpanel.lbl.Xaxistype")), "spanx, split"); + domainTypeSelector = new JComboBox(types); + domainTypeSelector.setSelectedItem(configuration.getDomainAxisType()); + domainTypeSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + FlightDataType type = (FlightDataType) domainTypeSelector.getSelectedItem(); + configuration.setDomainAxisType(type); + domainUnitSelector.setUnitGroup(type.getUnitGroup()); + domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit()); + setToCustom(); + } + }); + this.add(domainTypeSelector, "gapright para"); + + //// Unit: + this.add(new JLabel(trans.get("simplotpanel.lbl.Unit"))); + domainUnitSelector = new UnitSelector(configuration.getDomainAxisType().getUnitGroup()); + domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit()); + domainUnitSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + configuration.setDomainAxisUnit(domainUnitSelector.getSelectedUnit()); + } + }); + this.add(domainUnitSelector, "width 40lp, gapright para"); + + //// The data will be plotted in time order even if the X axis type is not time. + DescriptionArea desc = new DescriptionArea(trans.get("simplotpanel.Desc"), 2, -2f); + desc.setViewportBorder(BorderFactory.createEmptyBorder()); + this.add(desc, "width 1px, growx 1, wrap unrel"); + + + + //// Y axis selector panel + //// Y axis types: + this.add(new JLabel(trans.get("simplotpanel.lbl.Yaxistypes"))); + //// Flight events: + this.add(new JLabel(trans.get("simplotpanel.lbl.Flightevents")), "wrap rel"); + + typeSelectorPanel = new JPanel(new MigLayout("gapy rel")); + JScrollPane scroll = new JScrollPane(typeSelectorPanel); + this.add(scroll, "spany 2, height 10px, wmin 400lp, grow 100, gapright para"); + + + //// Flight events + eventTableModel = new FlightEventTableModel(); + JTable table = new JTable(eventTableModel); + table.setTableHeader(null); + table.setShowVerticalLines(false); + table.setRowSelectionAllowed(false); + table.setColumnSelectionAllowed(false); + + TableColumnModel columnModel = table.getColumnModel(); + TableColumn col0 = columnModel.getColumn(0); + int w = table.getRowHeight() + 2; + col0.setMinWidth(w); + col0.setPreferredWidth(w); + col0.setMaxWidth(w); + table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); + this.add(new JScrollPane(table), "height 10px, width 200lp, grow 1, wrap rel"); + + + //// All + None buttons + JButton button = new JButton(trans.get("simplotpanel.but.All")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + for (FlightEvent.Type t : FlightEvent.Type.values()) + configuration.setEvent(t, true); + eventTableModel.fireTableDataChanged(); + } + }); + this.add(button, "split 2, gapleft para, gapright para, growx, sizegroup buttons"); + + //// None + button = new JButton(trans.get("simplotpanel.but.None")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + for (FlightEvent.Type t : FlightEvent.Type.values()) + configuration.setEvent(t, false); + eventTableModel.fireTableDataChanged(); + } + }); + this.add(button, "gapleft para, gapright para, growx, sizegroup buttons, wrap para"); + + + + //// New Y axis plot type + button = new JButton(trans.get("simplotpanel.but.NewYaxisplottype")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (configuration.getTypeCount() >= 15) { + JOptionPane.showMessageDialog(SimulationPlotPanel.this, + //// A maximum of 15 plots is allowed. + //// Cannot add plot + trans.get("simplotpanel.OptionPane.lbl1"), + trans.get("simplotpanel.OptionPane.lbl2"), + JOptionPane.ERROR_MESSAGE); + return; + } + + // Select new type smartly + FlightDataType type = null; + for (FlightDataType t : + simulation.getSimulatedData().getBranch(0).getTypes()) { + + boolean used = false; + if (configuration.getDomainAxisType().equals(t)) { + used = true; + } else { + for (int i = 0; i < configuration.getTypeCount(); i++) { + if (configuration.getType(i).equals(t)) { + used = true; + break; + } + } + } + + if (!used) { + type = t; + break; + } + } + if (type == null) { + type = simulation.getSimulatedData().getBranch(0).getTypes()[0]; + } + + // Add new type + configuration.addPlotDataType(type); + setToCustom(); + updatePlots(); + } + }); + this.add(button, "spanx, split"); + + + this.add(new JPanel(), "growx"); + + //// Plot flight + button = new JButton(trans.get("simplotpanel.but.Plotflight")); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (configuration.getTypeCount() == 0) { + JOptionPane.showMessageDialog(SimulationPlotPanel.this, + trans.get("error.noPlotSelected"), + trans.get("error.noPlotSelected.title"), + JOptionPane.ERROR_MESSAGE); + return; + } + defaultConfiguration = configuration.clone(); + SimulationPlotDialog.showPlot(SwingUtilities.getWindowAncestor(SimulationPlotPanel.this), + simulation, configuration); + } + }); + this.add(button, "right"); + + + updatePlots(); + } + + + private void setConfiguration(PlotConfiguration conf) { + + boolean modified = false; + + configuration = conf.clone(); + if (!Utils.contains(types, configuration.getDomainAxisType())) { + configuration.setDomainAxisType(types[0]); + modified = true; + } + + for (int i = 0; i < configuration.getTypeCount(); i++) { + if (!Utils.contains(types, configuration.getType(i))) { + configuration.removePlotDataType(i); + i--; + modified = true; + } + } + + if (modified) { + configuration.setName(CUSTOM); + } + + } + + + private void setToCustom() { + modifying++; + configuration.setName(CUSTOM); + configurationSelector.setSelectedItem(CUSTOM_CONFIGURATION); + modifying--; + } + + + private void updatePlots() { + domainTypeSelector.setSelectedItem(configuration.getDomainAxisType()); + domainUnitSelector.setUnitGroup(configuration.getDomainAxisType().getUnitGroup()); + domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit()); + + typeSelectorPanel.removeAll(); + for (int i = 0; i < configuration.getTypeCount(); i++) { + FlightDataType type = configuration.getType(i); + Unit unit = configuration.getUnit(i); + int axis = configuration.getAxis(i); + + typeSelectorPanel.add(new PlotTypeSelector(i, type, unit, axis), "wrap"); + } + + typeSelectorPanel.repaint(); + + eventTableModel.fireTableDataChanged(); + } + + + + + /** + * A JPanel which configures a single plot of a PlotConfiguration. + */ + private class PlotTypeSelector extends JPanel { + private final String[] POSITIONS = { AUTO_NAME, LEFT_NAME, RIGHT_NAME }; + + private final int index; + private JComboBox typeSelector; + private UnitSelector unitSelector; + private JComboBox axisSelector; + + + public PlotTypeSelector(int plotIndex, FlightDataType type, Unit unit, int position) { + super(new MigLayout("ins 0")); + + this.index = plotIndex; + + typeSelector = new JComboBox(types); + typeSelector.setSelectedItem(type); + typeSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + FlightDataType type = (FlightDataType) typeSelector.getSelectedItem(); + configuration.setPlotDataType(index, type); + unitSelector.setUnitGroup(type.getUnitGroup()); + unitSelector.setSelectedUnit(configuration.getUnit(index)); + setToCustom(); + } + }); + this.add(typeSelector, "gapright para"); + + //// Unit: + this.add(new JLabel(trans.get("simplotpanel.lbl.Unit"))); + unitSelector = new UnitSelector(type.getUnitGroup()); + if (unit != null) + unitSelector.setSelectedUnit(unit); + unitSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + Unit unit = unitSelector.getSelectedUnit(); + configuration.setPlotDataUnit(index, unit); + } + }); + this.add(unitSelector, "width 40lp, gapright para"); + + //// Axis: + this.add(new JLabel(trans.get("simplotpanel.lbl.Axis"))); + axisSelector = new JComboBox(POSITIONS); + if (position == LEFT) + axisSelector.setSelectedIndex(1); + else if (position == RIGHT) + axisSelector.setSelectedIndex(2); + else + axisSelector.setSelectedIndex(0); + axisSelector.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (modifying > 0) + return; + int axis = axisSelector.getSelectedIndex() - 1; + configuration.setPlotDataAxis(index, axis); + } + }); + this.add(axisSelector); + + + JButton button = new JButton(Icons.DELETE); + //// Remove this plot + button.setToolTipText(trans.get("simplotpanel.but.ttip.Removethisplot")); + button.setBorderPainted(false); + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + configuration.removePlotDataType(index); + setToCustom(); + updatePlots(); + } + }); + this.add(button, "gapright 0"); + } + } + + + + private class FlightEventTableModel extends AbstractTableModel { + private final FlightEvent.Type[] eventTypes; + + public FlightEventTableModel() { + EnumSet set = EnumSet.noneOf(FlightEvent.Type.class); + for (int i = 0; i < simulation.getSimulatedData().getBranchCount(); i++) { + for (FlightEvent e : simulation.getSimulatedData().getBranch(i).getEvents()) { + set.add(e.getType()); + } + } + set.remove(FlightEvent.Type.ALTITUDE); + int count = set.size(); + + eventTypes = new FlightEvent.Type[count]; + int pos = 0; + for (FlightEvent.Type t : FlightEvent.Type.values()) { + if (set.contains(t)) { + eventTypes[pos] = t; + pos++; + } + } + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public int getRowCount() { + return eventTypes.length; + } + + @Override + public Class getColumnClass(int column) { + switch (column) { + case 0: + return Boolean.class; + + case 1: + return String.class; + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + @Override + public Object getValueAt(int row, int column) { + switch (column) { + case 0: + return new Boolean(configuration.isEventActive(eventTypes[row])); + + case 1: + return eventTypes[row].toString(); + + default: + throw new IndexOutOfBoundsException("column=" + column); + } + } + + @Override + public boolean isCellEditable(int row, int column) { + return column == 0; + } + + @Override + public void setValueAt(Object value, int row, int column) { + if (column != 0 || !(value instanceof Boolean)) { + throw new IllegalArgumentException("column=" + column + ", value=" + value); + } + + configuration.setEvent(eventTypes[row], (Boolean) value); + this.fireTableCellUpdated(row, column); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/print/AbstractPrintableTransition.java b/core/src/net/sf/openrocket/gui/print/AbstractPrintableTransition.java new file mode 100644 index 00000000..8e6042de --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/AbstractPrintableTransition.java @@ -0,0 +1,86 @@ +package net.sf.openrocket.gui.print; + +import net.sf.openrocket.rocketcomponent.Transition; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public abstract class AbstractPrintableTransition extends JPanel { + /** + * The stroke of the transition arc. + */ + private final static BasicStroke thinStroke = new BasicStroke(1.0f); + + /** + * The X margin. + */ + protected int marginX = (int) PrintUnit.INCHES.toPoints(0.25f); + + /** + * The Y margin. + */ + protected int marginY = (int) PrintUnit.INCHES.toPoints(0.25f); + + /** + * Constructor. Initialize this printable with the component to be printed. + * + * @param isDoubleBuffered a boolean, true for double-buffering + * @param transition the component to be printed + */ + public AbstractPrintableTransition(boolean isDoubleBuffered, Transition transition) { + super(isDoubleBuffered); + setBackground(Color.white); + init(transition); + } + + /** + * Compute the basic values of each arc of the transition/shroud. This is adapted from + * The Properties of + * Model Rocket Body Tube Transitions, by J.R. Brohm + * + * @param component the transition component + */ + protected abstract void init(Transition component); + + /** + * Draw the component onto the graphics context. + * + * @param g2 the graphics context + */ + protected abstract void draw(Graphics2D g2); + + /** + * Returns a generated image of the transition. May then be used wherever AWT images can be used, or converted to + * another image/picture format and used accordingly. + * + * @return an awt image of the fin set + */ + public Image createImage() { + int width = getWidth() + marginX; + int height = getHeight() + marginY; + // Create a buffered image in which to draw + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // Create a graphics contents on the buffered image + Graphics2D g2d = bufferedImage.createGraphics(); + // Draw graphics + g2d.setBackground(Color.white); + g2d.clearRect(0, 0, width, height); + paintComponent(g2d); + // Graphics context no longer needed so dispose it + g2d.dispose(); + return bufferedImage; + } + + @Override + public void paintComponent(Graphics g) { + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + g2.setColor(Color.BLACK); + g2.setStroke(thinStroke); + + draw(g2); + } +} diff --git a/core/src/net/sf/openrocket/gui/print/ConceptPrintDialog.java b/core/src/net/sf/openrocket/gui/print/ConceptPrintDialog.java new file mode 100644 index 00000000..9a07efa0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/ConceptPrintDialog.java @@ -0,0 +1,70 @@ +package net.sf.openrocket.gui.print; + +import java.awt.Window; +import java.lang.reflect.InvocationTargetException; + +import javax.swing.JButton; +import javax.swing.JCheckBox; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.util.GUIUtil; + +public class ConceptPrintDialog extends JDialog { + + public ConceptPrintDialog() { + super((Window) null, "Print"); + + JPanel panel = new JPanel(new MigLayout("fill")); + + JList list = new JList(new Object[] { + "Model name", + "Parts detail", + "Fin templates", + "Design report" + }); + panel.add(new JScrollPane(list), "spanx, growx, wrap"); + + JCheckBox checkbox = new JCheckBox("Show by stage"); + panel.add(checkbox, ""); + + JButton button = new JButton("Settings"); + panel.add(button, "right, wrap para"); + + JLabel label = new JLabel("Printer: LaserJet 6L
Paper size: A4 Portrait"); + panel.add(label); + + button = new JButton("Change"); + panel.add(button, "right, wrap 20lp"); + + panel.add(new JButton("Save as PDF"), "split, spanx, right"); + panel.add(new JButton("Preview"), "right"); + panel.add(new JButton("Print"), "right"); + panel.add(new JButton("Close"), "right"); + + + this.add(panel); + + } + + + + public static void main(String[] args) throws InterruptedException, InvocationTargetException { + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + GUIUtil.setBestLAF(); + JDialog dialog = new ConceptPrintDialog(); + GUIUtil.setDisposableDialogOptions(dialog, null); + dialog.setSize(450, 350); + dialog.setVisible(true); + } + }); + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/DesignReport.java b/core/src/net/sf/openrocket/gui/print/DesignReport.java new file mode 100644 index 00000000..886e225a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/DesignReport.java @@ -0,0 +1,520 @@ +/* + * DesignReport.java + */ +package net.sf.openrocket.gui.print; + +import java.awt.Graphics2D; +import java.io.IOException; +import java.text.DecimalFormat; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.figureelements.FigureElement; +import net.sf.openrocket.gui.figureelements.RocketInfo; +import net.sf.openrocket.gui.scalefigure.RocketPanel; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.Coordinate; + +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Element; +import com.itextpdf.text.Paragraph; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.BaseFont; +import com.itextpdf.text.pdf.DefaultFontMapper; +import com.itextpdf.text.pdf.PdfContentByte; +import com.itextpdf.text.pdf.PdfPCell; +import com.itextpdf.text.pdf.PdfPTable; +import com.itextpdf.text.pdf.PdfWriter; + +/** + *

+ * #  Title # Section describing the rocket in general without motors
+ * # Section describing the rocket in general without motors
+ * 

+ * design name + * empty mass & CG + * CP position + * CP position at 5 degree AOA (or similar) + * number of stages + * parachute/streamer sizes + * max. diameter (caliber) + * velocity at exit of rail/rod + * minimum safe velocity reached in x inches/cm + *

+ * # Section for each motor configuration + *

+ * a summary of the motors, e.g. 3xC6-0; B4-6 + * a list of the motors including the manufacturer, designation (maybe also info like burn time, grams of propellant, + * total impulse) + * total grams of propellant + * total impulse + * takeoff weight + * CG and CP position, stability margin + * predicted flight altitude, max. velocity and max. acceleration + * predicted velocity at chute deployment + * predicted descent rate + * Thrust to Weight Ratio of each stage + *

+ *

+ */ +public class DesignReport { + + /** + * The logger. + */ + private static final LogHelper log = Application.getLogger(); + + /** + * The OR Document. + */ + private OpenRocketDocument rocketDocument; + + /** + * A panel used for rendering of the design diagram. + */ + final RocketPanel panel; + + /** + * The iText document. + */ + protected Document document; + + /** The displayed strings. */ + private static final String STAGES = "Stages: "; + private static final String MASS_WITH_MOTORS = "Mass (with motors): "; + private static final String MASS_WITH_MOTOR = "Mass (with motor): "; + private static final String MASS_EMPTY = "Mass (Empty): "; + private static final String STABILITY = "Stability: "; + private static final String CG = "CG: "; + private static final String CP = "CP: "; + private static final String MOTOR = "Motor"; + private static final String AVG_THRUST = "Avg Thrust"; + private static final String BURN_TIME = "Burn Time"; + private static final String MAX_THRUST = "Max Thrust"; + private static final String TOTAL_IMPULSE = "Total Impulse"; + private static final String THRUST_TO_WT = "Thrust to Wt"; + private static final String PROPELLANT_WT = "Propellant Wt"; + private static final String SIZE = "Size"; + private static final String ALTITUDE = "Altitude"; + private static final String FLIGHT_TIME = "Flight Time"; + private static final String TIME_TO_APOGEE = "Time to Apogee"; + private static final String VELOCITY_OFF_PAD = "Velocity off Pad"; + private static final String MAX_VELOCITY = "Max Velocity"; + private static final String LANDING_VELOCITY = "Landing Velocity"; + private static final String ROCKET_DESIGN = "Rocket Design"; + private static final double GRAVITY_CONSTANT = 9.80665d; + + /** + * Constructor. + * + * @param theRocDoc the OR document + * @param theIDoc the iText document + */ + public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc) { + document = theIDoc; + rocketDocument = theRocDoc; + panel = new RocketPanel(rocketDocument); + } + + /** + * Main entry point. Prints the rocket drawing and design data. + * + * @param writer a direct byte writer + */ + public void writeToDocument(PdfWriter writer) { + if (writer == null) { + return; + } + com.itextpdf.text.Rectangle pageSize = document.getPageSize(); + int pageImageableWidth = (int) pageSize.getWidth() - (int) pageSize.getBorderWidth() * 2; + int pageImageableHeight = (int) pageSize.getHeight() / 2 - (int) pageSize.getBorderWidthTop(); + + PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, ROCKET_DESIGN); + + Rocket rocket = rocketDocument.getRocket(); + final Configuration configuration = rocket.getDefaultConfiguration().clone(); + configuration.setAllStages(); + PdfContentByte canvas = writer.getDirectContent(); + + final PrintFigure figure = new PrintFigure(configuration); + + FigureElement cp = panel.getExtraCP(); + FigureElement cg = panel.getExtraCG(); + RocketInfo text = panel.getExtraText(); + + double scale = paintRocketDiagram(pageImageableWidth, pageImageableHeight, canvas, figure, cp, cg); + + canvas.beginText(); + try { + canvas.setFontAndSize(BaseFont.createFont(PrintUtilities.NORMAL.getFamilyname(), BaseFont.CP1252, + BaseFont.EMBEDDED), PrintUtilities.NORMAL_FONT_SIZE); + } catch (DocumentException e) { + log.error("Could not set font.", e); + } catch (IOException e) { + log.error("Could not create font.", e); + } + int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS + .toPoints(1))); + final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts); + canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight); + canvas.moveTextWithLeading(0, -16); + + float initialY = canvas.getYTLM(); + + canvas.showText(rocketDocument.getRocket().getName()); + + canvas.newlineShowText(STAGES); + canvas.showText("" + rocket.getStageCount()); + + + if (configuration.hasMotors()) { + if (configuration.getStageCount() > 1) { + canvas.newlineShowText(MASS_WITH_MOTORS); + } else { + canvas.newlineShowText(MASS_WITH_MOTOR); + } + } else { + canvas.newlineShowText(MASS_EMPTY); + } + canvas.showText(text.getMass(UnitGroup.UNITS_MASS.getDefaultUnit())); + + canvas.newlineShowText(STABILITY); + canvas.showText(text.getStability()); + + canvas.newlineShowText(CG); + canvas.showText(text.getCg()); + + canvas.newlineShowText(CP); + canvas.showText(text.getCp()); + canvas.endText(); + + try { + //Move the internal pointer of the document below that of what was just written using the direct byte buffer. + Paragraph paragraph = new Paragraph(); + float finalY = canvas.getYTLM(); + int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight)); + + paragraph.setSpacingAfter(heightOfDiagramAndText); + document.add(paragraph); + + String[] motorIds = rocket.getMotorConfigurationIDs(); + + for (int j = 0; j < motorIds.length; j++) { + String motorId = motorIds[j]; + if (motorId != null) { + PdfPTable parent = new PdfPTable(2); + parent.setWidthPercentage(100); + parent.setHorizontalAlignment(Element.ALIGN_LEFT); + parent.setSpacingBefore(0); + parent.setWidths(new int[] { 1, 3 }); + int leading = 0; + //The first motor config is always null. Skip it and the top-most motor, then set the leading. + if (j > 1) { + leading = 25; + } + addFlightData(rocket, motorId, parent, leading); + addMotorData(rocket, motorId, parent); + document.add(parent); + } + } + } catch (DocumentException e) { + log.error("Could not modify document.", e); + } + } + + + /** + * Paint a diagram of the rocket into the PDF document. + * + * @param thePageImageableWidth the number of points in the width of the page available for drawing + * @param thePageImageableHeight the number of points in the height of the page available for drawing + * @param theCanvas the direct byte writer + * @param theFigure the print figure + * @param theCp the center of pressure figure element + * @param theCg the center of gravity figure element + * + * @return the scale of the diagram + */ + private double paintRocketDiagram(final int thePageImageableWidth, final int thePageImageableHeight, + final PdfContentByte theCanvas, final PrintFigure theFigure, + final FigureElement theCp, final FigureElement theCg) { + theFigure.clearAbsoluteExtra(); + theFigure.clearRelativeExtra(); + theFigure.addRelativeExtra(theCp); + theFigure.addRelativeExtra(theCg); + theFigure.updateFigure(); + + double scale = + (thePageImageableWidth * 2.2) / theFigure.getFigureWidth(); + + theFigure.setScale(scale); + /* + * page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion + */ + theFigure.setSize(thePageImageableWidth, thePageImageableHeight); + theFigure.updateFigure(); + + + final DefaultFontMapper mapper = new DefaultFontMapper(); + Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper); + g2d.translate(20, 120); + + g2d.scale(0.4d, 0.4d); + theFigure.paint(g2d); + g2d.dispose(); + return scale; + } + + /** + * Add the motor data for a motor configuration to the table. + * + * @param rocket the rocket + * @param motorId the motor ID to output + * @param parent the parent to which the motor data will be added + */ + private void addMotorData(Rocket rocket, String motorId, final PdfPTable parent) { + + PdfPTable motorTable = new PdfPTable(8); + motorTable.setWidthPercentage(68); + motorTable.setHorizontalAlignment(Element.ALIGN_LEFT); + + final PdfPCell motorCell = ITextHelper.createCell(MOTOR, PdfPCell.BOTTOM); + final int mPad = 10; + motorCell.setPaddingLeft(mPad); + motorTable.addCell(motorCell); + motorTable.addCell(ITextHelper.createCell(AVG_THRUST, PdfPCell.BOTTOM)); + motorTable.addCell(ITextHelper.createCell(BURN_TIME, PdfPCell.BOTTOM)); + motorTable.addCell(ITextHelper.createCell(MAX_THRUST, PdfPCell.BOTTOM)); + motorTable.addCell(ITextHelper.createCell(TOTAL_IMPULSE, PdfPCell.BOTTOM)); + motorTable.addCell(ITextHelper.createCell(THRUST_TO_WT, PdfPCell.BOTTOM)); + motorTable.addCell(ITextHelper.createCell(PROPELLANT_WT, PdfPCell.BOTTOM)); + motorTable.addCell(ITextHelper.createCell(SIZE, PdfPCell.BOTTOM)); + + DecimalFormat ttwFormat = new DecimalFormat("0.00"); + + MassCalculator massCalc = new BasicMassCalculator(); + + Configuration config = new Configuration(rocket); + config.setMotorConfigurationID(motorId); + + int totalMotorCount = 0; + double totalPropMass = 0; + double totalImpulse = 0; + double totalTTW = 0; + + int stage = 0; + double stageMass = 0; + + boolean topBorder = false; + for (RocketComponent c : rocket) { + + if (c instanceof Stage) { + config.setToStage(stage); + stage++; + stageMass = massCalc.getCG(config, MassCalcType.LAUNCH_MASS).weight; + // Calculate total thrust-to-weight from only lowest stage motors + totalTTW = 0; + topBorder = true; + } + + if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) { + MotorMount mount = (MotorMount) c; + + if (mount.isMotorMount() && mount.getMotor(motorId) != null) { + Motor motor = mount.getMotor(motorId); + int motorCount = c.toAbsolute(Coordinate.NUL).length; + + + int border = Rectangle.NO_BORDER; + if (topBorder) { + border = Rectangle.TOP; + topBorder = false; + } + + String name = motor.getDesignation(); + if (motorCount > 1) { + name += " (" + Chars.TIMES + motorCount + ")"; + } + + final PdfPCell motorVCell = ITextHelper.createCell(name, border); + motorVCell.setPaddingLeft(mPad); + motorTable.addCell(motorVCell); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getAverageThrustEstimate()), border)); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(motor.getBurnTimeEstimate()), border)); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getMaxThrustEstimate()), border)); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(motor.getTotalImpulseEstimate()), border)); + + double ttw = motor.getAverageThrustEstimate() / (stageMass * GRAVITY_CONSTANT); + motorTable.addCell(ITextHelper.createCell( + ttwFormat.format(ttw) + ":1", border)); + + double propMass = (motor.getLaunchCG().weight - motor.getEmptyCG().weight); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(propMass), border)); + + final Unit motorUnit = UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit(); + motorTable.addCell(ITextHelper.createCell(motorUnit.toString(motor.getDiameter()) + + "/" + + motorUnit.toString(motor.getLength()) + " " + + motorUnit.toString(), border)); + + // Sum up total count + totalMotorCount += motorCount; + totalPropMass += propMass * motorCount; + totalImpulse += motor.getTotalImpulseEstimate() * motorCount; + totalTTW += ttw * motorCount; + } + } + } + + if (totalMotorCount > 1) { + int border = Rectangle.TOP; + final PdfPCell motorVCell = ITextHelper.createCell("Total:", border); + motorVCell.setPaddingLeft(mPad); + motorTable.addCell(motorVCell); + motorTable.addCell(ITextHelper.createCell("", border)); + motorTable.addCell(ITextHelper.createCell("", border)); + motorTable.addCell(ITextHelper.createCell("", border)); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(totalImpulse), border)); + motorTable.addCell(ITextHelper.createCell( + ttwFormat.format(totalTTW) + ":1", border)); + motorTable.addCell(ITextHelper.createCell( + UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(totalPropMass), border)); + motorTable.addCell(ITextHelper.createCell("", border)); + + } + + PdfPCell c = new PdfPCell(motorTable); + c.setBorder(PdfPCell.LEFT); + c.setBorderWidthTop(0f); + parent.addCell(c); + } + + + /** + * Add the motor data for a motor configuration to the table. + * + * @param theRocket the rocket + * @param motorId a motor configuration id + * @param parent the parent to which the motor data will be added + * @param leading the number of points for the leading + */ + private void addFlightData(final Rocket theRocket, final String motorId, final PdfPTable parent, int leading) { + + // Perform flight simulation + Rocket duplicate = theRocket.copyWithOriginalID(); + FlightData flight = null; + try { + Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate); + simulation.getOptions().setMotorConfigurationID(motorId); + simulation.simulate(); + flight = simulation.getSimulatedData(); + } catch (SimulationException e1) { + // Ignore + } + + // Output the flight data + if (flight != null) { + try { + final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit(); + final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit(); + final Unit flightTimeUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit(); + + PdfPTable labelTable = new PdfPTable(2); + labelTable.setWidths(new int[] { 3, 2 }); + final Paragraph chunk = ITextHelper.createParagraph(stripBrackets( + theRocket.getMotorConfigurationNameOrDescription(motorId)), PrintUtilities.BOLD); + chunk.setLeading(leading); + chunk.setSpacingAfter(3f); + + document.add(chunk); + + final PdfPCell cell = ITextHelper.createCell(ALTITUDE, 2, 2); + cell.setUseBorderPadding(false); + cell.setBorderWidthTop(0f); + labelTable.addCell(cell); + labelTable.addCell(ITextHelper.createCell(distanceUnit.toStringUnit(flight.getMaxAltitude()), 2, 2)); + + labelTable.addCell(ITextHelper.createCell(FLIGHT_TIME, 2, 2)); + labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getFlightTime()), 2, 2)); + + labelTable.addCell(ITextHelper.createCell(TIME_TO_APOGEE, 2, 2)); + labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getTimeToApogee()), 2, 2)); + + labelTable.addCell(ITextHelper.createCell(VELOCITY_OFF_PAD, 2, 2)); + labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getLaunchRodVelocity()), 2, 2)); + + labelTable.addCell(ITextHelper.createCell(MAX_VELOCITY, 2, 2)); + labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getMaxVelocity()), 2, 2)); + + labelTable.addCell(ITextHelper.createCell(LANDING_VELOCITY, 2, 2)); + labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getGroundHitVelocity()), 2, 2)); + + //Add the table to the parent; have to wrap it in a cell + PdfPCell c = new PdfPCell(labelTable); + c.setBorder(PdfPCell.RIGHT); + c.setBorderWidthTop(0); + c.setTop(0); + parent.addCell(c); + } catch (DocumentException e) { + log.error("Could not add flight data to document.", e); + } + } + } + + /** + * Strip [] brackets from a string. + * + * @param target the original string + * + * @return target with [] removed + */ + private String stripBrackets(String target) { + return stripLeftBracket(stripRightBracket(target)); + } + + /** + * Strip [ from a string. + * + * @param target the original string + * + * @return target with [ removed + */ + private String stripLeftBracket(String target) { + return target.replace("[", ""); + } + + /** + * Strip ] from a string. + * + * @param target the original string + * + * @return target with ] removed + */ + private String stripRightBracket(String target) { + return target.replace("]", ""); + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/FinMarkingGuide.java b/core/src/net/sf/openrocket/gui/print/FinMarkingGuide.java new file mode 100644 index 00000000..f0be174d --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/FinMarkingGuide.java @@ -0,0 +1,456 @@ +package net.sf.openrocket.gui.print; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.GeneralPath; +import java.awt.geom.Path2D; +import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * This is the core Swing representation of a fin marking guide. It can handle multiple fin sets on the same or + * different body tubes. One marking guide will be created for any body tube that has a finset. If a tube has + * multiple finsets, then they are combined onto one marking guide. It also includes launch lug marking line(s) if lugs + * are present. If (and only if) a launch lug exists, then the word 'Front' is affixed to the leading edge of the + * guide to give orientation. + *

+ */ +public class FinMarkingGuide extends JPanel { + + /** + * The stroke of normal lines. + */ + private final static BasicStroke thinStroke = new BasicStroke(1.0f); + + /** + * The size of the arrow in points. + */ + private static final int ARROW_SIZE = 10; + + /** + * The default guide width in inches. + */ + public final static double DEFAULT_GUIDE_WIDTH = 3d; + + /** + * 2 PI radians (represents a circle). + */ + public final static double TWO_PI = 2 * Math.PI; + + /** + * The I18N translator. + */ + private static final Translator trans = Application.getTranslator(); + + /** + * The margin. + */ + private static final int MARGIN = (int) PrintUnit.INCHES.toPoints(0.25f); + + /** + * The height (circumference) of the biggest body tube with a finset. + */ + private int maxHeight = 0; + + /** + * A map of body tubes, to a list of components that contains finsets and launch lugs. + */ + private Map> markingGuideItems; + + /** + * Constructor. + * + * @param rocket the rocket instance + */ + public FinMarkingGuide(Rocket rocket) { + super(false); + setBackground(Color.white); + markingGuideItems = init(rocket); + //Max of 2 drawing guides horizontally per page. + setSize((int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH) * 2 + 3 * MARGIN, maxHeight); + } + + /** + * Initialize the marking guide class by iterating over a rocket and finding all finsets. + * + * @param component the root rocket component - this is iterated to find all finset and launch lugs + * @return a map of body tubes to lists of finsets and launch lugs. + */ + private Map> init(Rocket component) { + Iterator iter = component.iterator(false); + Map> results = new LinkedHashMap>(); + BodyTube current = null; + int totalHeight = 0; + int iterationHeight = 0; + int count = 0; + + while (iter.hasNext()) { + RocketComponent next = iter.next(); + if (next instanceof BodyTube) { + current = (BodyTube) next; + } else if (next instanceof FinSet || next instanceof LaunchLug) { + java.util.List list = results.get(current); + if (list == null && current != null) { + list = new ArrayList(); + results.put(current, list); + + double radius = current.getOuterRadius(); + int circumferenceInPoints = (int) PrintUnit.METERS.toPoints(radius * TWO_PI); + + // Find the biggest body tube circumference. + if (iterationHeight < (circumferenceInPoints + MARGIN)) { + iterationHeight = circumferenceInPoints + MARGIN; + } + //At most, two marking guides horizontally. After that, move down and back to the left margin. + count++; + if (count % 2 == 0) { + totalHeight += iterationHeight; + iterationHeight = 0; + } + } + if (list != null) { + list.add((ExternalComponent) next); + } + } + } + maxHeight = totalHeight + iterationHeight; + return results; + } + + /** + * Returns a generated image of the fin marking guide. May then be used wherever AWT images can be used, or + * converted to another image/picture format and used accordingly. + * + * @return an awt image of the fin marking guide + */ + public Image createImage() { + int width = getWidth() + 25; + int height = getHeight() + 25; + // Create a buffered image in which to draw + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // Create a graphics context on the buffered image + Graphics2D g2d = bufferedImage.createGraphics(); + // Draw graphics + g2d.setBackground(Color.white); + g2d.clearRect(0, 0, width, height); + paintComponent(g2d); + // Graphics context no longer needed so dispose it + g2d.dispose(); + return bufferedImage; + } + + /** + *

+     *   ---------------------- Page Edge --------------------------------------------------------
+     *   |                                        ^
+     *   |                                        |
+     *   |
+     *   |                                        y
+     *   |
+     *   |                                        |
+     *   P                                        v
+     *   a      ---                 +----------------------------+  ------------
+     *   g<------^-- x ------------>+                            +       ^
+     *   e       |                  +                            +       |
+     *           |                  +                            +     baseYOffset
+     *   E       |                  +                            +       v
+     *   d       |                  +<----------Fin------------->+ -------------
+     *   g       |                  +                            +
+     *   e       |                  +                            +
+     *   |       |                  +                            +
+     *   |       |                  +                            +
+     *   |       |                  +                            +   baseSpacing
+     *   |       |                  +                            +
+     *   |       |                  +                            +
+     *   |       |                  +                            +
+     *   |       |                  +                            +
+     *   |       |                  +<----------Fin------------->+  --------------
+     *   |       |                  +                            +
+     *   | circumferenceInPoints    +                            +
+     *   |       |                  +                            +
+     *   |       |                  +                            +
+     *   |       |                  +                            +    baseSpacing
+     *   |       |                  +<------Launch Lug --------->+           -----
+     *   |       |                  +                            +                 \
+     *   |       |                  +                            +                 + yLLOffset
+     *   |       |                  +                            +                 /
+     *   |       |                  +<----------Fin------------->+  --------------
+     *   |       |                  +                            +       ^
+     *   |       |                  +                            +       |
+     *   |       |                  +                            +    baseYOffset
+     *   |       v                  +                            +       v
+     *   |      ---                 +----------------------------+  --------------
+     *
+     *                              |<-------- width ----------->|
+     *
+     * yLLOffset is computed from the difference between the base rotation of the fin and the radial direction of the lug.
+     *
+     * Note: There is a current limitation that a tube with multiple launch lugs may not render the lug lines correctly.
+     * 
+ * + * @param g the Graphics context + */ + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + paintFinMarkingGuide(g2); + } + + private void paintFinMarkingGuide(Graphics2D g2) { + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + g2.setColor(Color.BLACK); + g2.setStroke(thinStroke); + int x = MARGIN; + int y = MARGIN; + + int width = (int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH); + + int column = 0; + for (BodyTube next : markingGuideItems.keySet()) { + double circumferenceInPoints = PrintUnit.METERS.toPoints(next.getOuterRadius() * TWO_PI); + List componentList = markingGuideItems.get(next); + //Don't draw the lug if there are no fins. + if (hasFins(componentList)) { + + drawMarkingGuide(g2, x, y, (int) (circumferenceInPoints), width); + + //Sort so that fins always precede lugs + sort(componentList); + + boolean hasMultipleComponents = componentList.size() > 1; + + double baseSpacing = 0d; + double baseYOrigin = 0; + double finRadial = 0d; + int yFirstFin = y; + int yLastFin = y; + boolean firstFinSet = true; + + //fin1: 42 fin2: 25 + for (ExternalComponent externalComponent : componentList) { + if (externalComponent instanceof FinSet) { + FinSet fins = (FinSet) externalComponent; + int finCount = fins.getFinCount(); + int offset = 0; + baseSpacing = (circumferenceInPoints / finCount); + double baseRotation = fins.getBaseRotation(); + if (!firstFinSet) { + //Adjust the rotation to a positive number. + while (baseRotation < 0) { + baseRotation += TWO_PI / finCount; + } + offset = computeYOffset(y, circumferenceInPoints, baseSpacing, baseYOrigin, finRadial, baseRotation); + } else { + //baseYOrigin is the distance from the top of the marking guide to the first fin of the first fin set. + //This measurement is used to base all subsequent finsets and lugs off of. + baseYOrigin = baseSpacing / 2; + offset = (int) (baseYOrigin) + y; + firstFinSet = false; + } + yFirstFin = y; + yLastFin = y; + finRadial = baseRotation; + + //Draw the fin marking lines. + for (int fin = 0; fin < finCount; fin++) { + if (fin > 0) { + offset += baseSpacing; + yLastFin = offset; + } else { + yFirstFin = offset; + } + drawDoubleArrowLine(g2, x, offset, x + width, offset); + // if (hasMultipleComponents) { + g2.drawString(externalComponent.getName(), x + (width / 3), offset - 2); + // } + } + } else if (externalComponent instanceof LaunchLug) { + LaunchLug lug = (LaunchLug) externalComponent; + double yLLOffset = (lug.getRadialDirection() - finRadial) / TWO_PI; + //The placement of the lug line must respect the boundary of the outer marking guide. In order + //to do that, place it above or below either the top or bottom fin line, based on the difference + //between their rotational directions. + if (yLLOffset < 0) { + yLLOffset = yLLOffset * circumferenceInPoints + yLastFin; + } else { + yLLOffset = yLLOffset * circumferenceInPoints + yFirstFin; + } + drawDoubleArrowLine(g2, x, (int) yLLOffset, x + width, (int) yLLOffset); + g2.drawString(lug.getName(), x + (width / 3), (int) yLLOffset - 2); + + } + } + //Only if the tube has a lug or multiple finsets does the orientation of the marking guide matter. So print 'Front'. + if (hasMultipleComponents) { + drawFrontIndication(g2, x, y, (int) baseSpacing, (int) circumferenceInPoints, width); + } + + //At most, two marking guides horizontally. After that, move down and back to the left margin. + column++; + if (column % 2 == 0) { + x = MARGIN; + y += circumferenceInPoints + MARGIN; + } else { + x += MARGIN + width; + } + } + } + } + + /** + * Compute the y offset for the next fin line. + * + * @param y the top margin + * @param circumferenceInPoints the circumference (height) of the guide + * @param baseSpacing the circumference / fin count + * @param baseYOrigin the offset from the top of the guide to the first fin of the first fin set drawn + * @param prevBaseRotation the rotation of the previous finset + * @param baseRotation the rotation of the current finset + * @return number of points from the top of the marking guide to the line to be drawn + */ + private int computeYOffset(int y, double circumferenceInPoints, double baseSpacing, double baseYOrigin, double prevBaseRotation, double baseRotation) { + int offset; + double finRadialDifference = (baseRotation - prevBaseRotation) / TWO_PI; + //If the fin line would be off the top of the marking guide, then readjust. + if (baseYOrigin + finRadialDifference * circumferenceInPoints < 0) { + offset = (int) (baseYOrigin + baseSpacing + finRadialDifference * circumferenceInPoints) + y; + } else if (baseYOrigin - finRadialDifference * circumferenceInPoints > 0) { + offset = (int) (finRadialDifference * circumferenceInPoints + baseYOrigin) + y; + } else { + offset = (int) (finRadialDifference * circumferenceInPoints - baseYOrigin) + y; + } + return offset; + } + + /** + * Determines if the list contains a FinSet. + * + * @param list a list of ExternalComponent + * @return true if the list contains at least one FinSet + */ + private boolean hasFins(List list) { + for (ExternalComponent externalComponent : list) { + if (externalComponent instanceof FinSet) { + return true; + } + } + return false; + } + + /** + * Sort a list of ExternalComponent in-place. Forces FinSets to precede Launch Lugs. + * + * @param componentList a list of ExternalComponent + */ + private void sort(List componentList) { + Collections.sort(componentList, new Comparator() { + @Override + public int compare(ExternalComponent o1, ExternalComponent o2) { + if (o1 instanceof FinSet) { + return -1; + } + if (o2 instanceof FinSet) { + return 1; + } + return 0; + } + }); + } + + /** + * Draw the marking guide outline. + * + * @param g2 the graphics context + * @param x the starting x coordinate + * @param y the starting y coordinate + * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube circumference + * @param width the width of the marking guide in print units; somewhat arbitrary + */ + private void drawMarkingGuide(Graphics2D g2, int x, int y, int length, int width) { + Path2D outline = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4); + outline.moveTo(x, y); + outline.lineTo(width + x, y); + outline.lineTo(width + x, length + y); + outline.lineTo(x, length + y); + outline.closePath(); + g2.draw(outline); + + //Draw tick marks for alignment, 1/4 of the width in from either edge + int fromEdge = (width) / 4; + final int tickLength = 8; + //Upper left + g2.drawLine(x + fromEdge, y, x + fromEdge, y + tickLength); + //Upper right + g2.drawLine(x + width - fromEdge, y, x + width - fromEdge, y + tickLength); + //Lower left + g2.drawLine(x + fromEdge, y + length - tickLength, x + fromEdge, y + length); + //Lower right + g2.drawLine(x + width - fromEdge, y + length - tickLength, x + width - fromEdge, y + length); + } + + /** + * Draw a vertical string indicating the front of the rocket. This is necessary when a launch lug exists to + * give proper orientation of the guide (assuming that the lug is asymmetrically positioned with respect to a fin). + * + * @param g2 the graphics context + * @param x the starting x coordinate + * @param y the starting y coordinate + * @param spacing the space between fin lines + * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube circumference + * @param width the width of the marking guide in print units; somewhat arbitrary + */ + private void drawFrontIndication(Graphics2D g2, int x, int y, int spacing, int length, int width) { + //The magic numbers here are fairly arbitrary. They're chosen in a manner that best positions 'Front' to be + //readable, without going to complex string layout prediction logic. + int rotateX = x + width - 16; + int rotateY = y + (int) (spacing * 1.5) + 20; + if (rotateY > y + length + 14) { + rotateY = y + length / 2 - 10; + } + g2.translate(rotateX, rotateY); + g2.rotate(Math.PI / 2); + g2.drawString(trans.get("FinMarkingGuide.lbl.Front"), 0, 0); + g2.rotate(-Math.PI / 2); + g2.translate(-rotateX, -rotateY); + } + + /** + * Draw a horizontal line with arrows on both endpoints. Depicts a fin alignment. + * + * @param g2 the graphics context + * @param x1 the starting x coordinate + * @param y1 the starting y coordinate + * @param x2 the ending x coordinate + * @param y2 the ending y coordinate + */ + void drawDoubleArrowLine(Graphics2D g2, int x1, int y1, int x2, int y2) { + int len = x2 - x1; + + g2.drawLine(x1, y1, x1 + len, y2); + g2.fillPolygon(new int[]{x1 + len, x1 + len - ARROW_SIZE, x1 + len - ARROW_SIZE, x1 + len}, + new int[]{y2, y2 - ARROW_SIZE / 2, y2 + ARROW_SIZE / 2, y2}, 4); + + g2.fillPolygon(new int[]{x1, x1 + ARROW_SIZE, x1 + ARROW_SIZE, x1}, + new int[]{y1, y1 - ARROW_SIZE / 2, y1 + ARROW_SIZE / 2, y1}, 4); + } + + +} diff --git a/core/src/net/sf/openrocket/gui/print/ITextHelper.java b/core/src/net/sf/openrocket/gui/print/ITextHelper.java new file mode 100644 index 00000000..0b2b6f95 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/ITextHelper.java @@ -0,0 +1,246 @@ +/* + * ITextHelper.java + */ +package net.sf.openrocket.gui.print; + +import com.itextpdf.text.Chunk; +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Font; +import com.itextpdf.text.Paragraph; +import com.itextpdf.text.Phrase; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.PdfContentByte; +import com.itextpdf.text.pdf.PdfPCell; +import com.itextpdf.text.pdf.PdfPTable; +import com.itextpdf.text.pdf.PdfWriter; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * A bunch of helper methods for creating iText components. + */ +public final class ITextHelper { + + /** + * Create a cell for an iText table. + * + * @return a cell with bottom border + */ + public static PdfPCell createCell () { + return createCell(Rectangle.BOTTOM); + } + + /** + * Create a cell for an iText table with the given border location. + * + * @param border the border location + * + * @return a cell with given border + */ + public static PdfPCell createCell (int border) { + PdfPCell result = new PdfPCell(); + result.setBorder(border); + + return result; + } + + /** + * Create a cell whose contents are a table. No border. + * + * @param table the table to insert into the cell + * + * @return the cell containing a table + */ + public static PdfPCell createCell (PdfPTable table) { + PdfPCell result = new PdfPCell(); + result.setBorder(PdfPCell.NO_BORDER); + result.addElement(table); + + return result; + } + + /** + * Create a cell whose contents are the given string. No border. Standard PrintUtilities.NORMAL font. + * + * @param v the text of the cell. + * + * @return the cell containing the text + */ + public static PdfPCell createCell (String v) { + return createCell(v, Rectangle.NO_BORDER, PrintUtilities.NORMAL); + } + + /** + * Create a cell whose contents are the given string , rendered with the given font. No border. + * + * @param v the text of the cell + * @param font the font + * + * @return the cell containing the text + */ + public static PdfPCell createCell (String v, Font font) { + return createCell(v, Rectangle.NO_BORDER, font); + } + + /** + * Create a cell whose contents are the given string with specified left and right padding (spacing). + * + * @param v the text of the cell + * @param leftPad the number of points to precede the text + * @param rightPad the number of points to follow the text + * + * @return the cell containing the text + */ + public static PdfPCell createCell (String v, int leftPad, int rightPad) { + PdfPCell c = createCell(v, Rectangle.NO_BORDER, PrintUtilities.NORMAL); + c.setPaddingLeft(leftPad); + c.setPaddingRight(rightPad); + return c; + } + + /** + * Create a cell whose contents are the given string with the given border. Uses NORMAL font. + * + * @param v the text of the cell + * @param border the border type + * + * @return the cell containing the text + */ + public static PdfPCell createCell (String v, int border) { + return createCell(v, border, PrintUtilities.NORMAL); + } + + /** + * Complete create cell - fully qualified. Create a cell whose contents are the given string with the given border + * and font. + * + * @param v the text of the cell + * @param border the border type + * @param font the font + * + * @return the cell containing the text + */ + public static PdfPCell createCell (String v, int border, Font font) { + PdfPCell result = new PdfPCell(); + result.setBorder(border); + Chunk c = new Chunk(); + c.setFont(font); + c.append(v); + result.addElement(c); + return result; + } + + /** + * Create a phrase with the given text and font. + * + * @param text the text + * @param font the font + * + * @return an iText phrase + */ + public static Phrase createPhrase (String text, Font font) { + Phrase p = new Phrase(); + final Chunk chunk = new Chunk(text); + chunk.setFont(font); + p.add(chunk); + return p; + } + + /** + * Create a phrase with the given text. + * + * @param text the text + * + * @return an iText phrase + */ + public static Phrase createPhrase (String text) { + return createPhrase(text, PrintUtilities.NORMAL); + } + + /** + * Create a paragraph with the given text and font. + * + * @param text the text + * @param font the font + * + * @return an iText paragraph + */ + public static Paragraph createParagraph (String text, Font font) { + Paragraph p = new Paragraph(); + final Chunk chunk = new Chunk(text); + chunk.setFont(font); + p.add(chunk); + return p; + } + + /** + * Create a paragraph with the given text and using NORMAL font. + * + * @param text the text + * + * @return an iText paragraph + */ + public static Paragraph createParagraph (String text) { + return createParagraph(text, PrintUtilities.NORMAL); + } + + /** + * Break a large image up into page-size pieces and output each page in order to an iText document. The image is + * overlayed with an matrix of pages running from left to right until the right side of the image is reached. Then + * the next 'row' of pages is output from left to right, and so on. + * + * @param pageSize a rectangle that defines the bounds of the page size + * @param doc the iText document + * @param writer the underlying content writer + * @param image the source image + * + * @throws DocumentException thrown if the document could not be written + */ + public static void renderImageAcrossPages (Rectangle pageSize, Document doc, PdfWriter writer, java.awt.Image image) + throws DocumentException { + final int margin = (int)Math.min(doc.topMargin(), PrintUnit.POINTS_PER_INCH * 0.3f); + float wPage = pageSize.getWidth() - 2 * margin; + float hPage = pageSize.getHeight() - 2 * margin; + + float wImage = image.getWidth(null); + float hImage = image.getHeight(null); + java.awt.Rectangle crop = new java.awt.Rectangle(0, 0, (int) Math.min(wPage, wImage), (int) Math.min(hPage, + hImage)); + PdfContentByte content = writer.getDirectContent(); + + int ymargin = 0; + + while (true) { + BufferedImage subImage = ((BufferedImage) image).getSubimage((int) crop.getX(), (int) crop.getY(), + (int) crop.getWidth(), (int) crop.getHeight()); + + Graphics2D g2 = content.createGraphics(pageSize.getWidth(), pageSize.getHeight()); + g2.drawImage(subImage, margin, ymargin, null); + g2.dispose(); + + // After the first page, the y-margin needs to be set. + ymargin = margin; + + final int newX = (int) (crop.getWidth() + crop.getX()); + if (newX < wImage) { + double adjust = Math.min(wImage - newX, wPage); + crop = new java.awt.Rectangle(newX, (int) crop.getY(), (int) adjust, + (int) crop.getHeight()); + } + else { + final int newY = (int) (crop.getHeight() + crop.getY()); + if (newY < hImage) { + double adjust = Math.min(hImage - newY, hPage); + crop = new java.awt.Rectangle(0, newY, (int) Math.min(wPage, wImage), (int) adjust); + } + else { + break; + } + } + doc.newPage(); + } + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java b/core/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java new file mode 100644 index 00000000..456495f3 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java @@ -0,0 +1,121 @@ +/* + * OpenRocketPrintable.java + */ +package net.sf.openrocket.gui.print; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +/** + * This enumeration identifies the various types of information that may be printed. + */ + +public enum OpenRocketPrintable { + // Design Report + DESIGN_REPORT("OpenRocketPrintable.DesignReport", false, 1), + // Parts detail + PARTS_DETAIL("OpenRocketPrintable.Partsdetail", true, 2), + // Nose Cone Templates + NOSE_CONE_TEMPLATE("OpenRocketPrintable.Noseconetemplates", false, 3), + // Transition Templates + TRANSITION_TEMPLATE("OpenRocketPrintable.Transitiontemplates", false, 4), + // Finset shape + FIN_TEMPLATE("OpenRocketPrintable.Fintemplates", true, 5), + // Fin marking guide. + FIN_MARKING_GUIDE("OpenRocketPrintable.Finmarkingguide", false, 6); + + + private static final Translator trans = Application.getTranslator(); + + /** + * The description - will be displayed in the JTree. + */ + private String description; + + /** + * Flag that indicates if the enum value is different depending upon stage. + */ + private boolean stageSpecific; + + /** + * The order of the item as it appears in the printed document. + */ + private int order; + + /** + * Constructor. + * + * @param s the displayable description + * @param staged indicates if the printable is stage dependent + * @param idx the relative print order + */ + OpenRocketPrintable(String s, boolean staged, int idx) { + description = s; + stageSpecific = staged; + order = idx; + } + + /** + * Get the description of this printable. + * + * @return a displayable string + */ + public String getDescription() { + return trans.get(description); + } + + /** + * Answers if this enum value has different meaning depending upon the stage. + * + * @return true if the printable is stage dependent + */ + public boolean isStageSpecific() { + return stageSpecific; + } + + /** + * Answer the print order. This is relative to other enum values. No two enum values will have the same print + * order value. + * + * @return a 0 based order (0 being first, or highest) + */ + public int getPrintOrder() { + return order; + } + + /** + * Look up an enum value based on the description. + * + * @param target the description + * + * @return an instance of this enum class or null if not found + */ + public static OpenRocketPrintable findByDescription(String target) { + OpenRocketPrintable[] values = values(); + for (OpenRocketPrintable value : values) { + if (value.getDescription().equalsIgnoreCase(target)) { + return value; + } + } + return null; + } + + /** + * Get a list of ordered enum values that do not have stage affinity. + * + * @return a list of OpenRocketPrintable + */ + public static List getUnstaged() { + List unstaged = new ArrayList(); + OpenRocketPrintable[] values = values(); + for (OpenRocketPrintable value : values) { + if (!value.isStageSpecific()) { + unstaged.add(value); + } + } + return unstaged; + } +} diff --git a/core/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java b/core/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java new file mode 100644 index 00000000..90cf4610 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java @@ -0,0 +1,76 @@ +/* + * PDFPrintStreamDoc.java + */ +package net.sf.openrocket.gui.print; + +import javax.print.Doc; +import javax.print.DocFlavor; +import javax.print.attribute.AttributeSetUtilities; +import javax.print.attribute.DocAttributeSet; +import java.io.*; + +/** + * This class implements a javax Doc specifically for PDF printing. All reports in OpenRocket are PDF (iText) based. + */ +public class PDFPrintStreamDoc implements Doc { + + /** The source stream of the PDF document. */ + private InputStream stream; + + /** The document's attributes. */ + private DocAttributeSet attributeSet; + + /** + * Constructor. + * + * @param ostream an output stream representing the pdf doc + * @param attributes the attributes of the document + */ + public PDFPrintStreamDoc (ByteArrayOutputStream ostream, DocAttributeSet attributes) { + stream = new ByteArrayInputStream(ostream.toByteArray()); + if (attributes != null) { + attributeSet = AttributeSetUtilities.unmodifiableView(attributes); + } + } + + /** + * Flavor is PDF. + * + * @return PDF flavor + */ + @Override + public DocFlavor getDocFlavor () { + return DocFlavor.INPUT_STREAM.PDF; + } + + @Override + public DocAttributeSet getAttributes () { + return attributeSet; + } + + /* Since the data is to be supplied as an InputStream delegate to + * getStreamForBytes(). + */ + @Override + public Object getPrintData () throws IOException { + return getStreamForBytes(); + } + + /** + * Intentionally null since the flavor is PDF. + * + * @return null + */ + @Override + public Reader getReaderForText () { + return null; + } + + /* Return the print data as an InputStream. + * Always return the same instance. + */ + @Override + public InputStream getStreamForBytes () throws IOException { + return stream; + } +} diff --git a/core/src/net/sf/openrocket/gui/print/PaperOrientation.java b/core/src/net/sf/openrocket/gui/print/PaperOrientation.java new file mode 100644 index 00000000..0e973e0f --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PaperOrientation.java @@ -0,0 +1,42 @@ +package net.sf.openrocket.gui.print; + +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.RectangleReadOnly; + +public enum PaperOrientation { + + PORTRAIT("Portrait") { + @Override + public Rectangle orient(Rectangle rect) { + return new RectangleReadOnly(rect); + } + }, + LANDSCAPE("Landscape") { + @Override + public Rectangle orient(Rectangle rect) { + return new RectangleReadOnly(new Rectangle(rect).rotate()); + } + }; + + + private final String name; + + private PaperOrientation(String name) { + this.name = name; + } + + /** + * Change the orientation of a portrait paper to the orientation represented by this + * orientation. + * + * @param rect the original paper size rectangle + * @return the oriented paper size rectangle + */ + public abstract Rectangle orient(Rectangle rect); + + + @Override + public String toString() { + return name; + } +} diff --git a/core/src/net/sf/openrocket/gui/print/PaperSize.java b/core/src/net/sf/openrocket/gui/print/PaperSize.java new file mode 100644 index 00000000..d3d07410 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PaperSize.java @@ -0,0 +1,193 @@ +package net.sf.openrocket.gui.print; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.Locale; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +import com.itextpdf.text.PageSize; +import com.itextpdf.text.Rectangle; + +public enum PaperSize { + A3("A3", PageSize.A3), + A4("A4", PageSize.A4), + A5("A5", PageSize.A5), + LETTER("Letter", PageSize.LETTER), + LEGAL("Legal", PageSize.LEGAL); + + private final String name; + private final Rectangle size; + + private PaperSize(String name, Rectangle size) { + this.name = name; + this.size = size; + } + + public Rectangle getSize() { + return size; + } + + @Override + public String toString() { + return name; + } + + + + ////////////////////////// + + private static final LogHelper log = Application.getLogger(); + private static PaperSize defaultSize = null; + + /** + * Return the default paper size for the current system. + * @return the default paper size + */ + public static PaperSize getDefault() { + if (defaultSize == null) { + + // Test environment variable "PAPERSIZE" (Unix) + defaultSize = getDefaultFromEnvironmentVariable(); + if (defaultSize != null) { + log.info("Selecting default paper size from PAPERSIZE environment variable: " + defaultSize); + return defaultSize; + } + + // Test /etc/papersize (Unix) + defaultSize = getDefaultFromEtcPapersize(); + if (defaultSize != null) { + log.info("Selecting default paper size from /etc/papersize: " + defaultSize); + return defaultSize; + } + + // Test user.country + defaultSize = getDefaultForCountry(System.getProperty("user.country")); + if (defaultSize != null) { + log.info("Selecting default paper size based on user.country: " + defaultSize); + return defaultSize; + } + + // Test locale country + defaultSize = getDefaultForCountry(Locale.getDefault().getCountry()); + if (defaultSize != null) { + log.info("Selecting default paper size based on locale country: " + defaultSize); + return defaultSize; + } + + // Fallback to A4 + defaultSize = A4; + log.info("Selecting default paper size fallback: " + defaultSize); + } + + return defaultSize; + } + + + /** + * Attempt to read the default paper size from the "PAPERSIZE" environment variable. + * + * @return the default paper size if successful, or null if unable to read/parse file. + */ + private static PaperSize getDefaultFromEnvironmentVariable() { + String str = System.getenv("PAPERSIZE"); + return getSizeFromString(str); + } + + /** + * Attempt to read the default paper size from the file defined by the environment variable + * PAPERCONF or from /etc/papersize. + * + * @return the default paper size if successful, or null if unable to read/parse file. + */ + private static PaperSize getDefaultFromEtcPapersize() { + + // Find file to read + String file = System.getenv("PAPERCONF"); + if (file == null) { + file = "/etc/papersize"; + } + + // Attempt to read the file + BufferedReader in = null; + try { + + String str; + in = new BufferedReader(new FileReader(file)); + while ((str = in.readLine()) != null) { + if (str.matches("^\\s*(#.*|$)")) { + continue; + } + break; + } + + return getSizeFromString(str); + + } catch (IOException e) { + + // Could not read file + return null; + + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + } + } + } + } + + + /** + * Get a paper size based on a string. The string is trimmed and case-insensitively + * compared to the base names of the paper sizes. + * + * @param size the size string (may be null) + * @return the corresponding paper size, or null if unknown + */ + static PaperSize getSizeFromString(String size) { + if (size == null) { + return null; + } + + size = size.trim(); + for (PaperSize p : PaperSize.values()) { + if (p.name.equalsIgnoreCase(size)) { + return p; + } + } + return null; + } + + + /** + * Get default paper size for a specific country. This method falls back to A4 for + * any country not known to use Letter. + * + * @param country the 2-char country code (may be null) + * @return the paper size, or null if country is not a country code + */ + static PaperSize getDefaultForCountry(String country) { + /* + * List is based on info from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/territory_language_information.html + * OpenOffice.org agrees with this: http://wiki.services.openoffice.org/wiki/DefaultPaperSize#Summary + */ + final String[] letterCountries = { "BZ", "CA", "CL", "CO", "CR", "SV", "GT", "MX", "NI", "PA", "PH", "PR", "US", "VE" }; + + if (country == null || !country.matches("^[a-zA-Z][a-zA-Z]$")) { + return null; + } + + country = country.toUpperCase(); + for (String c : letterCountries) { + if (c.equals(country)) { + return LETTER; + } + } + return A4; + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintController.java b/core/src/net/sf/openrocket/gui/print/PrintController.java new file mode 100644 index 00000000..69e4df05 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintController.java @@ -0,0 +1,129 @@ +/* + * PrintController.java + * + */ +package net.sf.openrocket.gui.print; + +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.ExceptionConverter; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.PdfBoolean; +import com.itextpdf.text.pdf.PdfName; +import com.itextpdf.text.pdf.PdfWriter; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.gui.print.visitor.FinMarkingGuideStrategy; +import net.sf.openrocket.gui.print.visitor.FinSetPrintStrategy; +import net.sf.openrocket.gui.print.visitor.PartsDetailVisitorStrategy; +import net.sf.openrocket.gui.print.visitor.TransitionStrategy; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.Iterator; +import java.util.Set; + +/** + * This is the main active object for printing. It performs all actions necessary to create and populate the print + * file. + */ +public class PrintController { + + /** + * Print the selected components to a PDF document. + * + * @param doc the OR document + * @param toBePrinted the user chosen items to print + * @param outputFile the file being written to + * @param settings the print settings + */ + public void print(OpenRocketDocument doc, Iterator toBePrinted, OutputStream outputFile, + PrintSettings settings) { + + Document idoc = new Document(getSize(settings)); + PdfWriter writer = null; + try { + writer = PdfWriter.getInstance(idoc, outputFile); + writer.setStrictImageSequence(true); + + writer.addViewerPreference(PdfName.PRINTSCALING, PdfName.NONE); + writer.addViewerPreference(PdfName.PICKTRAYBYPDFSIZE, PdfBoolean.PDFTRUE); + try { + idoc.open(); + Thread.sleep(1000); + } catch (InterruptedException e) { + } + while (toBePrinted.hasNext()) { + PrintableContext printableContext = toBePrinted.next(); + + Set stages = printableContext.getStageNumber(); + + switch (printableContext.getPrintable()) { + case DESIGN_REPORT: + DesignReport dp = new DesignReport(doc, idoc); + dp.writeToDocument(writer); + idoc.newPage(); + break; + case FIN_TEMPLATE: + final FinSetPrintStrategy finWriter = new FinSetPrintStrategy(idoc, + writer, + stages); + finWriter.writeToDocument(doc.getRocket()); + break; + case PARTS_DETAIL: + final PartsDetailVisitorStrategy detailVisitor = new PartsDetailVisitorStrategy(idoc, + writer, + stages); + detailVisitor.writeToDocument(doc.getRocket()); + detailVisitor.close(); + idoc.newPage(); + break; + case TRANSITION_TEMPLATE: + final TransitionStrategy tranWriter = new TransitionStrategy(idoc, writer, stages); + tranWriter.writeToDocument(doc.getRocket(), false); + idoc.newPage(); + break; + + case NOSE_CONE_TEMPLATE: + final TransitionStrategy coneWriter = new TransitionStrategy(idoc, writer, stages); + coneWriter.writeToDocument(doc.getRocket(), true); + idoc.newPage(); + break; + + case FIN_MARKING_GUIDE: + final FinMarkingGuideStrategy fmg = new FinMarkingGuideStrategy(idoc, writer); + fmg.writeToDocument(doc.getRocket()); + idoc.newPage(); + break; + } + } + //Stupid iText throws a really nasty exception if there is no data when close is called. + if (writer.getCurrentDocumentSize() <= 140) { + writer.setPageEmpty(false); + } + writer.close(); + idoc.close(); + } catch (DocumentException e) { + } catch (ExceptionConverter ec) { + } finally { + if (outputFile != null) { + try { + outputFile.close(); + } catch (IOException e) { + } + } + } + } + + /** + * Get the correct paper size from the print settings. + * + * @param settings the print settings + * @return the paper size + */ + private Rectangle getSize(PrintSettings settings) { + PaperSize size = settings.getPaperSize(); + PaperOrientation orientation = settings.getPaperOrientation(); + return orientation.orient(size.getSize()); + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintFigure.java b/core/src/net/sf/openrocket/gui/print/PrintFigure.java new file mode 100644 index 00000000..3f4655c6 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintFigure.java @@ -0,0 +1,34 @@ +/* + * PrintFigure.java + */ +package net.sf.openrocket.gui.print; + +import net.sf.openrocket.gui.scalefigure.RocketFigure; +import net.sf.openrocket.rocketcomponent.Configuration; + +/** + * A figure used to override the scale factor in RocketFigure. This allows pinpoint scaling to allow a diagram + * to fit in the width of the chosen page size. + */ +public class PrintFigure extends RocketFigure { + + /** + * Constructor. + * + * @param configuration the configuration + */ + public PrintFigure(final Configuration configuration) { + super(configuration); + } + + @Override + protected double computeTy(int heightPx) { + super.computeTy(heightPx); + return 0; + } + + public void setScale(final double theScale) { + this.scale = theScale; //dpi/0.0254*scaling; + updateFigure(); + } +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintSettings.java b/core/src/net/sf/openrocket/gui/print/PrintSettings.java new file mode 100644 index 00000000..6537f400 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintSettings.java @@ -0,0 +1,89 @@ +package net.sf.openrocket.gui.print; + +import java.awt.Color; + +import net.sf.openrocket.util.AbstractChangeSource; + +/** + * A class containing all printing settings. + */ +public class PrintSettings extends AbstractChangeSource { + + private Color templateFillColor = Color.LIGHT_GRAY; + private Color templateBorderColor = Color.DARK_GRAY; + + private PaperSize paperSize = PaperSize.getDefault(); + private PaperOrientation paperOrientation = PaperOrientation.PORTRAIT; + + + public Color getTemplateFillColor() { + return templateFillColor; + } + + public void setTemplateFillColor(Color templateFillColor) { + // Implicitly tests against setting null + if (templateFillColor.equals(this.templateFillColor)) { + return; + } + this.templateFillColor = templateFillColor; + fireChangeEvent(); + } + + public Color getTemplateBorderColor() { + return templateBorderColor; + } + + public void setTemplateBorderColor(Color templateBorderColor) { + // Implicitly tests against setting null + if (templateBorderColor.equals(this.templateBorderColor)) { + return; + } + this.templateBorderColor = templateBorderColor; + fireChangeEvent(); + } + + public PaperSize getPaperSize() { + return paperSize; + } + + public void setPaperSize(PaperSize paperSize) { + if (paperSize.equals(this.paperSize)) { + return; + } + this.paperSize = paperSize; + fireChangeEvent(); + } + + public PaperOrientation getPaperOrientation() { + return paperOrientation; + } + + public void setPaperOrientation(PaperOrientation orientation) { + if (orientation.equals(paperOrientation)) { + return; + } + this.paperOrientation = orientation; + fireChangeEvent(); + } + + + + /** + * Load settings from the specified print settings. + * @param settings the settings to load + */ + public void loadFrom(PrintSettings settings) { + this.templateFillColor = settings.templateFillColor; + this.templateBorderColor = settings.templateBorderColor; + this.paperSize = settings.paperSize; + this.paperOrientation = settings.paperOrientation; + fireChangeEvent(); + } + + + @Override + public String toString() { + return "PrintSettings [templateFillColor=" + templateFillColor + ", templateBorderColor=" + templateBorderColor + ", paperSize=" + paperSize + ", paperOrientation=" + paperOrientation + "]"; + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java b/core/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java new file mode 100644 index 00000000..53a710c8 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java @@ -0,0 +1,50 @@ +/* + * PrintSimulationWorker.java + */ +package net.sf.openrocket.gui.print; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.main.SimulationWorker; +import net.sf.openrocket.simulation.FlightData; + +/** + * A SimulationWorker that simulates the rocket flight in the background and sets the results to the extra text when + * finished. The worker can be cancelled if necessary. + */ +public class PrintSimulationWorker { + + public static FlightData doit(Simulation sim) { + return new InnerPrintSimulationWorker(sim).doit(); + } + + static class InnerPrintSimulationWorker extends SimulationWorker { + + public InnerPrintSimulationWorker(Simulation sim) { + super(sim); + } + + public FlightData doit() { + return doInBackground(); + } + + @Override + protected void simulationDone() { + // Do nothing if cancelled + if (isCancelled()) { + return; + } + + simulation.getSimulatedData(); + } + + + /** + * Called if the simulation is interrupted due to an exception. + * + * @param t the Throwable that caused the interruption + */ + @Override + protected void simulationInterrupted(final Throwable t) { + } + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/PrintUnit.java b/core/src/net/sf/openrocket/gui/print/PrintUnit.java new file mode 100644 index 00000000..c603f2f9 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintUnit.java @@ -0,0 +1,158 @@ +/* + * PrintUnit.java + */ +package net.sf.openrocket.gui.print; + +/** + * Utilities for print units. + */ +public enum PrintUnit { + INCHES { + public double toInches(double d) { return d; } + public double toMillis(double d) { return d/INCHES_PER_MM; } + public double toCentis(double d) { return d/(INCHES_PER_MM*TEN); } + public double toMeters(double d) { return d/(INCHES_PER_MM*TEN*TEN*TEN); } + public long toPoints(double d) { return (long)(d * POINTS_PER_INCH); } + public double convert(double d, PrintUnit u) { return u.toInches(d); } + }, + MILLIMETERS { + public double toInches(double d) { return d * INCHES_PER_MM; } + public double toMillis(double d) { return d; } + public double toCentis(double d) { return d/TEN; } + public double toMeters(double d) { return d/(TEN*TEN*TEN); } + public long toPoints(double d) { return INCHES.toPoints(toInches(d)); } + public double convert(double d, PrintUnit u) { return u.toMillis(d); } + }, + CENTIMETERS { + public double toInches(double d) { return d * INCHES_PER_MM * TEN; } + public double toMillis(double d) { return d * TEN; } + public double toCentis(double d) { return d; } + public double toMeters(double d) { return d/(TEN*TEN); } + public long toPoints(double d) { return INCHES.toPoints(toInches(d)); } + public double convert(double d, PrintUnit u) { return u.toCentis(d); } + }, + METERS { + public double toInches(double d) { return d * INCHES_PER_MM * TEN * TEN * TEN; } + public double toMillis(double d) { return d * TEN * TEN * TEN; } + public double toCentis(double d) { return d * TEN * TEN; } + public double toMeters(double d) { return d; } + public long toPoints(double d) { return INCHES.toPoints(toInches(d)); } + public double convert(double d, PrintUnit u) { return u.toMeters(d); } + }, + POINTS { + public double toInches(double d) { return d/POINTS_PER_INCH; } + public double toMillis(double d) { return d/(POINTS_PER_INCH * INCHES_PER_MM); } + public double toCentis(double d) { return toMillis(d)/TEN; } + public double toMeters(double d) { return toMillis(d)/(TEN*TEN*TEN); } + public long toPoints(double d) { return (long)d; } + public double convert(double d, PrintUnit u) { return u.toPoints(d); } + }; + + // Handy constants for conversion methods + public static final double INCHES_PER_MM = 0.0393700787d; + public static final double MM_PER_INCH = 1.0d/INCHES_PER_MM; + public static final long TEN = 10; + /** + * PPI is Postscript Point and is a standard of 72. Java2D also uses this internally as a pixel-per-inch, so pixels + * and points are for the most part interchangeable (unless the defaults are changed), which makes translating + * between the screen and a print job easier. + * + * Not to be confused with Dots-Per-Inch, which is printer and print mode dependent. + */ + public static final int POINTS_PER_INCH = 72; + + // To maintain full signature compatibility with 1.5, and to improve the + // clarity of the generated javadoc (see 6287639: Abstract methods in + // enum classes should not be listed as abstract), method convert + // etc. are not declared abstract but otherwise act as abstract methods. + + /** + * Convert the given length in the given unit to this + * unit. Conversions from finer to coarser granularities + * truncate, so may lose precision. + * + *

For example, to convert 10 inches to point, use: + * PrintUnit.POINTS.convert(10L, PrintUnit.INCHES) + * + * @param sourceLength the length in the given sourceUnit + * @param sourceUnit the unit of the sourceDuration argument + * + * @return the converted length in this unit, + * or Long.MIN_VALUE if conversion would negatively + * overflow, or Long.MAX_VALUE if it would positively overflow. + */ + public double convert(double sourceLength, PrintUnit sourceUnit) { + throw new AbstractMethodError(); + } + + /** + * Equivalent to INCHES.convert(length, this). + * + * @param length the length + * + * @return the converted length, + * or Long.MIN_VALUE if conversion would negatively + * overflow, or Long.MAX_VALUE if it would positively overflow. + * @see #convert + */ + public double toInches(double length) { + throw new AbstractMethodError(); + } + + /** + * Equivalent to MILLIMETERS.convert(length, this). + * + * @param length the length + * + * @return the converted length, + * or Long.MIN_VALUE if conversion would negatively + * overflow, or Long.MAX_VALUE if it would positively overflow. + * @see #convert + */ + public double toMillis(double length) { + throw new AbstractMethodError(); + } + + /** + * Equivalent to CENTIMETERS.convert(length, this). + * + * @param length the length + * + * @return the converted length, + * or Long.MIN_VALUE if conversion would negatively + * overflow, or Long.MAX_VALUE if it would positively overflow. + * @see #convert + */ + public double toCentis(double length) { + throw new AbstractMethodError(); + } + + /** + * Equivalent to METERS.convert(length, this). + * + * @param length the length + * + * @return the converted length, + * or Long.MIN_VALUE if conversion would negatively + * overflow, or Long.MAX_VALUE if it would positively overflow. + * @see #convert + */ + public double toMeters(double length) { + throw new AbstractMethodError(); + } + + /** + * Equivalent to POINTS.convert(length, this). + * + * @param length the length + * + * @return the converted length, + * or Long.MIN_VALUE if conversion would negatively + * overflow, or Long.MAX_VALUE if it would positively overflow. + * @see #convert + */ + public long toPoints(double length) { + throw new AbstractMethodError(); + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/PrintUtilities.java b/core/src/net/sf/openrocket/gui/print/PrintUtilities.java new file mode 100644 index 00000000..7f096efc --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintUtilities.java @@ -0,0 +1,104 @@ +/* + * PrintUtilities.java + */ +package net.sf.openrocket.gui.print; + + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.print.PageFormat; +import java.awt.print.Printable; + +import javax.swing.RepaintManager; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +import com.itextpdf.text.Chunk; +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Font; +import com.itextpdf.text.Paragraph; + +/** + * Utilities methods and fonts used for printing. + */ +public class PrintUtilities implements Printable { + + /** + * The logger. + */ + private static final LogHelper log = Application.getLogger(); + + public static final int NORMAL_FONT_SIZE = Font.DEFAULTSIZE - 3; + public static final int SMALL_FONT_SIZE = NORMAL_FONT_SIZE - 3; + + public static final Font BOLD = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE, Font.BOLD); + public static final Font BIG_BOLD = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE + 3, Font.BOLD); + public static final Font BOLD_UNDERLINED = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE, + Font.BOLD | Font.UNDERLINE); + public static final Font NORMAL = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE); + public static final Font SMALL = new Font(Font.FontFamily.HELVETICA, SMALL_FONT_SIZE); + + + private Component componentToBePrinted; + + public PrintUtilities(Component componentToBePrinted) { + this.componentToBePrinted = componentToBePrinted; + } + + @Override + public int print(Graphics g, PageFormat pageFormat, int pageIndex) { + if (pageIndex > 0) { + return (NO_SUCH_PAGE); + } else { + Graphics2D g2d = (Graphics2D) g; + translateToJavaOrigin(g2d, pageFormat); + disableDoubleBuffering(componentToBePrinted); + componentToBePrinted.paint(g2d); + enableDoubleBuffering(componentToBePrinted); + return (PAGE_EXISTS); + } + } + + public static void disableDoubleBuffering(Component c) { + RepaintManager currentManager = RepaintManager.currentManager(c); + currentManager.setDoubleBufferingEnabled(false); + } + + public static void enableDoubleBuffering(Component c) { + RepaintManager currentManager = RepaintManager.currentManager(c); + currentManager.setDoubleBufferingEnabled(true); + } + + + /** + * Translate the page format coordinates onto the graphics object using Java's origin (top left). + * + * @param g2d the graphics object + * @param pageFormat the print page format + */ + public static void translateToJavaOrigin(Graphics2D g2d, PageFormat pageFormat) { + g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY()); + } + + /** + * Add text as a new paragraph in a given font to the document. + * + * @param document the document + * @param font the font + * @param title the title + */ + public static void addText(Document document, com.itextpdf.text.Font font, String title) { + Chunk sectionHeader = new Chunk(title); + sectionHeader.setFont(font); + try { + Paragraph p = new Paragraph(); + p.add(sectionHeader); + document.add(p); + } catch (DocumentException e) { + log.error("Could not add paragraph.", e); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintableContext.java b/core/src/net/sf/openrocket/gui/print/PrintableContext.java new file mode 100644 index 00000000..4a5ddcac --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintableContext.java @@ -0,0 +1,121 @@ +/* + * PrintableContext.java + * + */ +package net.sf.openrocket.gui.print; + +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Instances of this class are meant to keep track of what the user has selected to be printed. + */ +public class PrintableContext implements Comparable, Iterable { + + /** + * The stage number. May be null for printables that have no stage meaning. + */ + private Set stageNumber; + + /** + * The type of thing to be printed. + */ + private OpenRocketPrintable printable; + + /** + * Sort of a reverse map that tracks each type of printable item and the stages for which that item is to be printed. + */ + private final Map> previous = new TreeMap>(); + + /** + * Constructor. + */ + public PrintableContext() { + } + + /** + * Constructor. + * + * @param theStageNumber the stage number of the printable; may be null if not applicable + * @param thePrintable the type of the thing to be printed + * + * @throws IllegalArgumentException thrown if thePrintable.isStageSpecific + */ + private PrintableContext(final Set theStageNumber, final OpenRocketPrintable thePrintable) + throws IllegalArgumentException { + if (thePrintable.isStageSpecific() && theStageNumber == null) { + throw new IllegalArgumentException("A stage number must be provided when a printable is stage specific."); + } + stageNumber = theStageNumber; + printable = thePrintable; + } + + /** + * Add a type of printable to a stage (number). + * + * @param theStageNumber the stage number + * @param thePrintable the printable to associate with the stage + */ + public void add(final Integer theStageNumber, final OpenRocketPrintable thePrintable) { + Set stages = previous.get(thePrintable); + if (stages == null) { + stages = new TreeSet(); + previous.put(thePrintable, stages); + } + if (theStageNumber != null) { + stages.add(theStageNumber); + } + } + + /** PrintableContext iterator. */ + @Override + public Iterator iterator() { + return new Iterator() { + + Iterator keyIter = previous.keySet().iterator(); + + @Override + public boolean hasNext() { + return keyIter.hasNext(); + } + + @Override + public PrintableContext next() { + final OpenRocketPrintable key = keyIter.next(); + return new PrintableContext(previous.get(key), key); + } + + @Override + public void remove() { + } + }; + + } + + /** + * Get the stage number, if it's applicable to the printable. + * + * @return the stage number + */ + public Set getStageNumber() { + return stageNumber; + } + + /** + * Get the printable. + * + * @return the printable + */ + public OpenRocketPrintable getPrintable() { + return printable; + } + + @Override + public int compareTo(final PrintableContext other) { + return this.printable.getPrintOrder() - other.printable.getPrintOrder(); + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintableFinSet.java b/core/src/net/sf/openrocket/gui/print/PrintableFinSet.java new file mode 100644 index 00000000..cf07cb13 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintableFinSet.java @@ -0,0 +1,199 @@ +/* + * PrintableFinSet.java + */ +package net.sf.openrocket.gui.print; + +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.util.Coordinate; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.GeneralPath; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; + +/** + * This class allows for a FinSet to be printable. It does so by decorating an existing finset (which will not be + * modified) and rendering it within a JPanel. The JPanel is not actually visualized on a display, but instead renders + * it to a print device. + */ +public class PrintableFinSet extends JPanel implements Printable { + + /** + * The object that represents the shape (outline) of the fin. This gets drawn onto the Swing component. + */ + protected GeneralPath polygon = null; + + /** + * The X margin. + */ + private final int marginX = (int)(PrintUnit.POINTS_PER_INCH * 0.3f); + /** + * The Y margin. + */ + private final int marginY = (int)(PrintUnit.POINTS_PER_INCH * 0.3f); + /** + * The minimum X coordinate. + */ + private int minX = 0; + /** + * The minimum Y coordinate. + */ + private int minY = 0; + + /** + * Constructor. + * + * @param fs the finset to print + */ + public PrintableFinSet (FinSet fs) { + this(fs.getFinPointsWithTab()); + } + + /** + * Construct a fin set from a set of points. + * + * @param points an array of points. + */ + public PrintableFinSet (Coordinate[] points) { + super(false); + init(points); + setBackground(Color.white); + } + + /** + * Initialize the fin set polygon and set the size of the component. + * + * @param points an array of points. + */ + private void init (Coordinate[] points) { + + polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, points.length); + polygon.moveTo(0, 0); + + int maxX = 0; + int maxY = 0; + + for (Coordinate point : points) { + final long x = PrintUnit.METERS.toPoints(point.x); + final long y = PrintUnit.METERS.toPoints(point.y); + minX = (int) Math.min(x, minX); + minY = (int) Math.min(y, minY); + maxX = (int) Math.max(x, maxX); + maxY = (int) Math.max(y, maxY); + polygon.lineTo(x, y); + } + polygon.closePath(); + + setSize(maxX - minX, maxY - minY); + } + + /** + * Get the X-axis margin value. + * + * @return margin, in points + */ + protected double getMarginX () { + return marginX; + } + + /** + * Get the Y-axis margin value. + * + * @return margin, in points + */ + protected double getMarginY () { + return marginY; + } + + /** + * From the java.awt.print.Printable interface. + *

+ * Prints the page at the specified index into the specified {@link java.awt.Graphics} context in the specified + * format. A PrinterJob calls the Printable interface to request that a page be rendered + * into the context specified by graphics. The format of the page to be drawn is specified by + * pageFormat. The zero based index of the requested page is specified by pageIndex. If + * the requested page does not exist then this method returns NO_SUCH_PAGE; otherwise PAGE_EXISTS is returned. The + * Graphics class or subclass implements the {@link java.awt.print.PrinterGraphics} interface to + * provide additional information. If the Printable object aborts the print job then it throws a + * {@link java.awt.print.PrinterException}. + *

+ * Note: This is not currently used in OpenRocket. It's only here for reference. + * + * @param graphics the context into which the page is drawn + * @param pageFormat the size and orientation of the page being drawn + * @param pageIndex the zero based index of the page to be drawn + * + * @return PAGE_EXISTS if the page is rendered successfully or NO_SUCH_PAGE if pageIndex specifies a + * non-existent page. + * + * @throws java.awt.print.PrinterException + * thrown when the print job is terminated. + */ + @Override + public int print (final Graphics graphics, final PageFormat pageFormat, final int pageIndex) + throws PrinterException { + + Graphics2D g2d = (Graphics2D) graphics; + PrintUtilities.translateToJavaOrigin(g2d, pageFormat); + PrintUtilities.disableDoubleBuffering(this); + paint(g2d); + PrintUtilities.enableDoubleBuffering(this); + return Printable.PAGE_EXISTS; + } + + /** + * Returns a generated image of the fin set. May then be used wherever AWT images can be used, or converted to + * another image/picture format and used accordingly. + * + * @return an awt image of the fin set + */ + public Image createImage () { + int width = getWidth() + marginX; + int height = getHeight() + marginY; + // Create a buffered image in which to draw + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + // Create a graphics contents on the buffered image + Graphics2D g2d = bufferedImage.createGraphics(); + // Draw graphics + g2d.setBackground(Color.white); + g2d.clearRect(0, 0, width, height); + paintComponent(g2d); + // Graphics context no longer needed so dispose it + g2d.dispose(); + return bufferedImage; + } + + /** + * Render the fin set onto the graphics context. This is done by creating a GeneralPath component that follows the + * outline of the fin set coordinates to create a polygon, which is then drawn onto the graphics context. + * Through-the-wall fin tabs are supported if they are present. + * + * @param g the Java2D graphics context + */ + @Override + public void paintComponent (Graphics g) { + super.paintComponent(g); + Graphics2D g2d = (Graphics2D) g; + + int x = 0; + int y = 0; + + // The minimum X/Y can be negative (primarily only Y due to fin tabs; rarely (never) X, but protect both anyway). + if (minX < marginX) { + x = marginX + Math.abs(minX); + } + if (minY < marginY) { + y = marginY + Math.abs(minY); + } + // Reset the origin. + g2d.translate(x, y); + g2d.setPaint(TemplateProperties.getFillColor()); + g2d.fill(polygon); + g2d.setPaint(TemplateProperties.getLineColor()); + g2d.draw(polygon); + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintableNoseCone.java b/core/src/net/sf/openrocket/gui/print/PrintableNoseCone.java new file mode 100644 index 00000000..78afe6f0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintableNoseCone.java @@ -0,0 +1,59 @@ +package net.sf.openrocket.gui.print; + +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.Shape; + +import net.sf.openrocket.gui.rocketfigure.TransitionShapes; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.util.Transformation; + +public class PrintableNoseCone extends AbstractPrintableTransition { + + /** + * If the component to be drawn is a nose cone, save a reference to it. + */ + private NoseCone target; + + /** + * Construct a printable nose cone. + * + * @param noseCone the component to print + */ + public PrintableNoseCone(Transition noseCone) { + super(false, noseCone); + } + + @Override + protected void init(Transition component) { + + target = (NoseCone) component; + double radius = target.getForeRadius(); + if (radius < target.getAftRadius()) { + radius = target.getAftRadius(); + } + setSize((int) PrintUnit.METERS.toPoints(2 * radius) + marginX, + (int) PrintUnit.METERS.toPoints(target.getLength() + target.getAftShoulderLength()) + marginY); + } + + /** + * Draw a nose cone. + * + * @param g2 the graphics context + */ + @Override + protected void draw(Graphics2D g2) { + Shape[] shapes = TransitionShapes.getShapesSide(target, Transformation.rotate_x(0d), PrintUnit.METERS.toPoints(1)); + + if (shapes != null && shapes.length > 0) { + Rectangle r = shapes[0].getBounds(); + g2.translate(marginX + r.getHeight() / 2, marginY); + g2.rotate(Math.PI / 2); + for (Shape shape : shapes) { + g2.draw(shape); + } + g2.rotate(-Math.PI / 2); + } + } +} diff --git a/core/src/net/sf/openrocket/gui/print/PrintableTransition.java b/core/src/net/sf/openrocket/gui/print/PrintableTransition.java new file mode 100644 index 00000000..a70703da --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/PrintableTransition.java @@ -0,0 +1,207 @@ +package net.sf.openrocket.gui.print; + +import java.awt.BasicStroke; +import java.awt.Graphics2D; +import java.awt.geom.Arc2D; +import java.awt.geom.GeneralPath; +import java.awt.geom.Line2D; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; + +import net.sf.openrocket.rocketcomponent.Transition; + +/** + * This class allows for a Transition to be printable. It does so by decorating an existing transition (which will not be + * modified) and rendering it within a JPanel. The JPanel is not actually visualized on a display, but instead renders + * it to a print device. + *

+ * Note: Currently nose cones are only supported by drawing the 2D projection of the profile. A more useful approach + * may be to draw a myriahedral projection that can be cut out and bent to form the shape. + */ +public class PrintableTransition extends AbstractPrintableTransition { + + /** + * Dashed array value. + */ + private final static float dash1[] = { 4.0f }; + /** + * The dashed stroke for glue tab. + */ + private final static BasicStroke dashed = new BasicStroke(1.0f, + BasicStroke.CAP_BUTT, + BasicStroke.JOIN_MITER, + 10.0f, dash1, 0.0f); + + /** + * The layout is an outer arc, an inner arc, and two lines one either endpoints that connect the arcs. + * Most of the math involves transposing geometric cartesian coordinates to the Java AWT coordinate system. + */ + private Path2D gp; + + /** + * The glue tab. + */ + private Path2D glueTab1; + + /** + * The alignment marks. + */ + private Line2D tick1, tick2; + + /** + * The x coordinates for the two ticks drawn at theta degrees. + */ + private int tick3X, tick4X; + + /** + * The angle, in degrees. + */ + private float theta; + + /** + * The x,y coordinates for where the virtual circle center is located. + */ + private int circleCenterX, circleCenterY; + + /** + * Constructor. + * + * @param transition the transition to print + */ + public PrintableTransition(Transition transition) { + super(false, transition); + } + + @Override + protected void init(Transition component) { + + double r1 = component.getAftRadius(); + double r2 = component.getForeRadius(); + + //Regardless of orientation, we have the convention of R1 as the smaller radius. Flip if different. + if (r1 > r2) { + r1 = r2; + r2 = component.getAftRadius(); + } + double len = component.getLength(); + double v = r2 - r1; + double tmp = Math.sqrt(v * v + len * len); + double factor = tmp / v; + + theta = (float) (360d * v / tmp); + + int r1InPoints = (int) PrintUnit.METERS.toPoints(r1 * factor); + int r2InPoints = (int) PrintUnit.METERS.toPoints(r2 * factor); + + int x = marginX; + int tabOffset = 35; + int y = tabOffset + marginY; + + Arc2D.Double outerArc = new Arc2D.Double(); + Arc2D.Double innerArc = new Arc2D.Double(); + + //If the arcs are more than 3/4 of a circle, then assume the height (y) is the same as the radius of the bigger arc. + if (theta >= 270) { + y += r2InPoints; + } + //If the arc is between 1/2 and 3/4 of a circle, then compute the actual height based upon the angle and radius + //of the bigger arc. + else if (theta >= 180) { + double thetaRads = Math.toRadians(theta - 180); + y += (int) ((Math.cos(thetaRads) * r2InPoints) * Math.tan(thetaRads)); + } + + circleCenterY = y; + circleCenterX = r2InPoints + x; + + //Create the larger arc. + outerArc.setArcByCenter(circleCenterX, circleCenterY, r2InPoints, 180, theta, Arc2D.OPEN); + + //Create the smaller arc. + innerArc.setArcByCenter(circleCenterX, circleCenterY, r1InPoints, 180, theta, Arc2D.OPEN); + + //Create the line between the start of the larger arc and the start of the smaller arc. + Path2D.Double line = new Path2D.Double(); + line.setWindingRule(Path2D.WIND_NON_ZERO); + line.moveTo(x, y); + final int width = r2InPoints - r1InPoints; + line.lineTo(width + x, y); + + //Create the line between the endpoint of the larger arc and the endpoint of the smaller arc. + Path2D.Double closingLine = new Path2D.Double(); + closingLine.setWindingRule(Path2D.WIND_NON_ZERO); + Point2D innerArcEndPoint = innerArc.getEndPoint(); + closingLine.moveTo(innerArcEndPoint.getX(), innerArcEndPoint.getY()); + Point2D outerArcEndPoint = outerArc.getEndPoint(); + closingLine.lineTo(outerArcEndPoint.getX(), outerArcEndPoint.getY()); + + //Add all shapes to the polygon path. + gp = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4); + gp.append(line, false); + gp.append(outerArc, false); + gp.append(closingLine, false); + gp.append(innerArc, false); + + //Create the glue tab. + glueTab1 = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4); + glueTab1.moveTo(x, y); + glueTab1.lineTo(x + tabOffset, y - tabOffset); + glueTab1.lineTo(width + x - tabOffset, y - tabOffset); + glueTab1.lineTo(width + x, y); + + //Create tick marks for alignment, 1/4 of the width in from either edge + int fromEdge = width / 4; + final int tickLength = 8; + //Upper left + tick1 = new Line2D.Float(x + fromEdge, y, x + fromEdge, y + tickLength); + //Upper right + tick2 = new Line2D.Float(x + width - fromEdge, y, x + width - fromEdge, y + tickLength); + + tick3X = r2InPoints - fromEdge; + tick4X = r1InPoints + fromEdge; + + setSize(gp.getBounds().width, gp.getBounds().height + tabOffset); + } + + /** + * Draw alignment marks on an angle. + * + * @param g2 the graphics context + * @param x the center of the circle's x coordinate + * @param y the center of the circle's y + * @param line the line to draw + * @param theta the angle + */ + private void drawAlignmentMarks(Graphics2D g2, int x, int y, Line2D.Float line, float theta) { + g2.translate(x, y); + g2.rotate(Math.toRadians(-theta)); + g2.draw(line); + g2.rotate(Math.toRadians(theta)); + g2.translate(-x, -y); + } + + /** + * Draw a transition. + * + * @param g2 the graphics context + */ + @Override + protected void draw(Graphics2D g2) { + //Render it. + g2.draw(gp); + g2.draw(tick1); + g2.draw(tick2); + drawAlignmentMarks(g2, circleCenterX, + circleCenterY, + new Line2D.Float(-tick3X, 0, -tick3X, -8), + theta); + drawAlignmentMarks(g2, circleCenterX, + circleCenterY, + new Line2D.Float(-tick4X, 0, -tick4X, -8), + theta); + + g2.setStroke(dashed); + g2.draw(glueTab1); + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/TemplateProperties.java b/core/src/net/sf/openrocket/gui/print/TemplateProperties.java new file mode 100644 index 00000000..6f88e576 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/TemplateProperties.java @@ -0,0 +1,76 @@ +/* + * TemplateProperties.java + */ +package net.sf.openrocket.gui.print; + +import java.awt.Color; + +import javax.swing.UIManager; + +/** + * This class is responsible for managing various properties of print templates (fin, nose cone, transitions, etc.). + * + * TODO: HIGH: Remove this entire class, and instead pass the PrintSettings object to the print methods. + */ +public class TemplateProperties { + + /** + * The property that defines the fill color. + */ + public static final String TEMPLATE_FILL_COLOR_PROPERTY = "template.fill.color"; + + /** + * The property that defines the line color. + */ + public static final String TEMPLATE_LINE_COLOR_PROPERTY = "template.line.color"; + + /** + * Get the current fill color. + * + * @return a color to be used as the fill in template shapes + */ + public static Color getFillColor() { + Color fillColor = UIManager.getColor(TemplateProperties.TEMPLATE_FILL_COLOR_PROPERTY); + if (fillColor == null) { + fillColor = Color.lightGray; + } + return fillColor; + } + + + /** + * Set the template fill color. + */ + public static void setFillColor(Color c) { + UIManager.put(TemplateProperties.TEMPLATE_FILL_COLOR_PROPERTY, c); + } + + + /** + * Get the current line color. + * + * @return a color to be used as the line in template shapes + */ + public static Color getLineColor() { + Color lineColor = UIManager.getColor(TemplateProperties.TEMPLATE_LINE_COLOR_PROPERTY); + if (lineColor == null) { + lineColor = Color.darkGray; + } + return lineColor; + } + + /** + * Set the template line color. + */ + public static void setLineColor(Color c) { + UIManager.put(TemplateProperties.TEMPLATE_LINE_COLOR_PROPERTY, c); + } + + /** + * Set the template colors from the print settings. + */ + public static void setColors(PrintSettings settings) { + setFillColor(settings.getTemplateFillColor()); + setLineColor(settings.getTemplateBorderColor()); + } +} diff --git a/core/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java b/core/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java new file mode 100644 index 00000000..7fbab0a6 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java @@ -0,0 +1,77 @@ +/* + * CheckBoxNode.java + */ +package net.sf.openrocket.gui.print.components; + +/** + * A class that acts as the textual node of the check box within the JTree. + */ +public class CheckBoxNode { + + /** + * The text label of the check box. + */ + String text; + + /** + * State flag indicating if the check box has been selected. + */ + boolean selected; + + /** + * Constructor. + * + * @param theText the check box label + * @param isSelected true if selected + */ + public CheckBoxNode (String theText, boolean isSelected) { + text = theText; + selected = isSelected; + } + + /** + * Get the current state of the check box. + * + * @return true if selected + */ + public boolean isSelected () { + return selected; + } + + /** + * Set the current state of the check box. Note: this just tracks the state - it + * does NOT actually set the state of the check box. + * + * @param isSelected true if selected + */ + public void setSelected (boolean isSelected) { + selected = isSelected; + } + + /** + * Get the text of the label. + * + * @return the text of the label + */ + public String getText () { + return text; + } + + /** + * Set the text of the label of the check box. + * + * @param theText the text of the label + */ + public void setText (String theText) { + text = theText; + } + + /** + * If someone prints this object, the text label will be displayed. + * + * @return the text label + */ + public String toString () { + return text; + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java b/core/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java new file mode 100644 index 00000000..e4a93373 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java @@ -0,0 +1,83 @@ +/* + * CheckTreeCellRenderer.java + */ +package net.sf.openrocket.gui.print.components; + +import javax.swing.JCheckBox; +import javax.swing.JPanel; +import javax.swing.JTree; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreePath; +import java.awt.BorderLayout; +import java.awt.Component; + +/** + * A cell renderer for JCheckBoxes within nodes of a JTree. + *

+ * Based in part on a blog by Santhosh Kumar. http://www.jroller.com/santhosh/date/20050610 + */ +public class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer { + + /** + * The selection model. + */ + private CheckTreeSelectionModel selectionModel; + /** + * The delegated cell renderer. + */ + private DefaultTreeCellRenderer delegate; + /** + * The check box within this cell. + */ + private JCheckBox checkBox = new JCheckBox(); + + /** + * Constructor. + * + * @param theDelegate the delegated cell renderer + * @param theSelectionModel the selection model + */ + public CheckTreeCellRenderer (DefaultTreeCellRenderer theDelegate, CheckTreeSelectionModel theSelectionModel) { + delegate = theDelegate; + + delegate.setLeafIcon(null); + delegate.setClosedIcon(null); + delegate.setOpenIcon(null); + + + selectionModel = theSelectionModel; + setLayout(new BorderLayout()); + setOpaque(false); + checkBox.setOpaque(false); + checkBox.setSelected(true); + } + + /** + * @{inheritDoc} + */ + @Override + public Component getTreeCellRendererComponent (JTree tree, Object value, boolean selected, boolean expanded, + boolean leaf, int row, boolean hasFocus) { + Component renderer = delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, + hasFocus); + + TreePath path = tree.getPathForRow(row); + if (path != null) { + final boolean b = selectionModel.isPathSelected(path, true); + checkBox.setSelected(b); + if (value instanceof DefaultMutableTreeNode) { + Object obj = ((DefaultMutableTreeNode) value).getUserObject(); + if (obj instanceof CheckBoxNode) { + ((CheckBoxNode) obj).setSelected(b); + } + } + } + + removeAll(); + add(checkBox, BorderLayout.WEST); + add(renderer, BorderLayout.CENTER); + return this; + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java b/core/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java new file mode 100644 index 00000000..719e50d7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java @@ -0,0 +1,99 @@ +/* + * CheckTreeManager.java + */ +package net.sf.openrocket.gui.print.components; + +import javax.swing.JCheckBox; +import javax.swing.JTree; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreePath; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +/** + * This class manages mouse clicks within the JTree, handling selection/deselections within the JCheckBox of each cell in the tree. + */ +public class CheckTreeManager extends MouseAdapter implements TreeSelectionListener { + + /** The selection model. */ + private CheckTreeSelectionModel selectionModel; + /** The actual JTree instance. */ + private JTree tree; + /** The number of pixels of width of the check box. Clicking anywhere within the box will trigger actions. */ + int hotspot = new JCheckBox().getPreferredSize().width; + + /** + * Construct a check box tree manager. + * + * @param theTree the actual tree being managed + */ + public CheckTreeManager (RocketPrintTree theTree) { + tree = theTree; + selectionModel = new CheckTreeSelectionModel(tree.getModel()); + theTree.setCheckBoxSelectionModel(selectionModel); + tree.setCellRenderer(new CheckTreeCellRenderer((DefaultTreeCellRenderer)tree.getCellRenderer(), selectionModel)); + tree.addMouseListener(this); + selectionModel.addTreeSelectionListener(this); + + for (int x = 0; x < tree.getRowCount(); x++) { + tree.getSelectionModel().setSelectionPath(tree.getPathForRow(x)); + } + } + + public void addTreeSelectionListener (TreeSelectionListener tsl) { + selectionModel.addTreeSelectionListener(tsl); + } + + /** + * Called when the mouse clicks within the tree. + * + * @param me the event that triggered this + */ + @Override + public void mouseClicked (MouseEvent me) { + TreePath path = tree.getPathForLocation(me.getX(), me.getY()); + if (path == null) { + return; + } + if (me.getX() > tree.getPathBounds(path).x + hotspot) { + return; + } + + boolean selected = selectionModel.isPathSelected(path, true); + selectionModel.removeTreeSelectionListener(this); + + try { + if (selected) { + selectionModel.removeSelectionPath(path); + } + else { + selectionModel.addSelectionPath(path); + } + } + finally { + selectionModel.addTreeSelectionListener(this); + tree.treeDidChange(); + } + } + + /** + * Get the selection model being used by this manager. + * + * @return the selection model + */ + public CheckTreeSelectionModel getSelectionModel () { + return selectionModel; + } + + /** + * Notify the tree that it changed. + * + * @param e unused + */ + @Override + public void valueChanged (TreeSelectionEvent e) { + tree.treeDidChange(); + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java b/core/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java new file mode 100644 index 00000000..9f34b4b5 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java @@ -0,0 +1,245 @@ +/* + * CheckTreeSelectionModel.java + */ +package net.sf.openrocket.gui.print.components; + +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.util.ArrayList; +import java.util.Stack; + +/** + * This class implements the selection model for the checkbox tree. This specifically is used to keep + * track of the TreePaths that have a selected CheckBox. + */ +public class CheckTreeSelectionModel extends DefaultTreeSelectionModel { + + /** + * The tree model. + */ + private TreeModel model; + + /** + * Constructor. + * + * @param theModel the model in use for the tree + */ + public CheckTreeSelectionModel (TreeModel theModel) { + model = theModel; + setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); + } + + /** + * Tests whether there is any selected node in the subtree of given path. + * + * @param path the path to walk + * + * @return true if any item in the path or its descendants are selected + */ + public boolean isPartiallySelected (TreePath path) { + if (isPathSelected(path, true)) { + return false; + } + TreePath[] selectionPaths = getSelectionPaths(); + if (selectionPaths == null) { + return false; + } + for (TreePath selectionPath : selectionPaths) { + if (isDescendant(selectionPath, path)) { + return true; + } + } + return false; + } + + /** + * Tells whether given path is selected. If dig is true, then a path is assumed to be selected, if one of its + * ancestor is selected. + * + * @param path the path to interrogate + * @param dig if true then check if an ancestor is selected + * + * @return true if the path is selected + */ + public boolean isPathSelected (TreePath path, boolean dig) { + if (!dig) { + return super.isPathSelected(path); + } + while (path != null && !super.isPathSelected(path)) { + path = path.getParentPath(); + } + return path != null; + } + + /** + * Determines if path1 is a descendant of path2. + * + * @param path1 descendant? + * @param path2 ancestor? + * + * @return true if path1 is a descendant of path2 + */ + private boolean isDescendant (TreePath path1, TreePath path2) { + Object obj1[] = path1.getPath(); + Object obj2[] = path2.getPath(); + for (int i = 0; i < obj2.length; i++) { + if (i < obj1.length) { + if (obj1[i] != obj2[i]) { + return false; + } + } + else { + return false; + } + } + return true; + } + + + /** + * Unsupported exception. + * + * @param pPaths an array of paths + */ + public void setSelectionPaths (TreePath[] pPaths) { + TreePath selected[] = getSelectionPaths(); + for (TreePath aSelected : selected) { + removeSelectionPath(aSelected); + } + for (TreePath pPath : pPaths) { + addSelectionPath(pPath); + } + } + + /** + * Add a set of TreePath nodes to the selection model. + * + * @param paths an array of tree path nodes + */ + public void addSelectionPaths (TreePath[] paths) { + // deselect all descendants of paths[] + for (TreePath path : paths) { + TreePath[] selectionPaths = getSelectionPaths(); + if (selectionPaths == null) { + break; + } + ArrayList toBeRemoved = new ArrayList(); + for (TreePath selectionPath : selectionPaths) { + if (isDescendant(selectionPath, path)) { + toBeRemoved.add(selectionPath); + } + } + super.removeSelectionPaths(toBeRemoved.toArray(new TreePath[toBeRemoved.size()])); + } + + // if all siblings are selected then deselect them and select parent recursively + // otherwise just select that path. + for (TreePath path : paths) { + TreePath temp = null; + while (areSiblingsSelected(path)) { + temp = path; + if (path.getParentPath() == null) { + break; + } + path = path.getParentPath(); + } + if (temp != null) { + if (temp.getParentPath() != null) { + addSelectionPath(temp.getParentPath()); + } + else { + if (!isSelectionEmpty()) { + removeSelectionPaths(getSelectionPaths()); + } + super.addSelectionPaths(new TreePath[]{temp}); + } + } + else { + super.addSelectionPaths(new TreePath[]{path}); + } + } + } + + /** + * Tells whether all siblings of given path are selected. + * + * @param path the tree path node + * + * @return true if all sibling nodes are selected + */ + private boolean areSiblingsSelected (TreePath path) { + TreePath parent = path.getParentPath(); + if (parent == null) { + return true; + } + Object node = path.getLastPathComponent(); + Object parentNode = parent.getLastPathComponent(); + + int childCount = model.getChildCount(parentNode); + for (int i = 0; i < childCount; i++) { + Object childNode = model.getChild(parentNode, i); + if (childNode == node) { + continue; + } + if (!isPathSelected(parent.pathByAddingChild(childNode))) { + return false; + } + } + return true; + } + + /** + * Remove paths from the selection model. + * + * @param paths the array of path nodes + */ + public void removeSelectionPaths (TreePath[] paths) { + for (TreePath path : paths) { + if (path.getPathCount() == 1) { + super.removeSelectionPaths(new TreePath[]{path}); + } + else { + toggleRemoveSelection(path); + } + } + } + + /** + * If any ancestor node of given path is selected then deselect it and selection all its descendants except given + * path and descendants. otherwise just deselect the given path. + * + * @param path the tree path node + */ + private void toggleRemoveSelection (TreePath path) { + Stack stack = new Stack(); + TreePath parent = path.getParentPath(); + while (parent != null && !isPathSelected(parent)) { + stack.push(parent); + parent = parent.getParentPath(); + } + if (parent != null) { + stack.push(parent); + } + else { + super.removeSelectionPaths(new TreePath[]{path}); + return; + } + + while (!stack.isEmpty()) { + TreePath temp = stack.pop(); + TreePath peekPath = stack.isEmpty() ? path : stack.peek(); + Object node = temp.getLastPathComponent(); + Object peekNode = peekPath.getLastPathComponent(); + int childCount = model.getChildCount(node); + for (int i = 0; i < childCount; i++) { + Object childNode = model.getChild(node, i); + if (childNode != peekNode) { + super.addSelectionPaths(new TreePath[]{temp.pathByAddingChild(childNode)}); + } + } + } + super.removeSelectionPaths(new TreePath[]{parent}); + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java b/core/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java new file mode 100644 index 00000000..5296ea47 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java @@ -0,0 +1,262 @@ +/* + * RocketPrintTree.java + */ +package net.sf.openrocket.gui.print.components; + +import net.sf.openrocket.gui.print.OpenRocketPrintable; +import net.sf.openrocket.gui.print.PrintableContext; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Stage; + +import javax.swing.*; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Vector; + +/** + * A specialized JTree for displaying various rocket items that can be printed. + */ +public class RocketPrintTree extends JTree { + + /** + * All check boxes are initially set to true (selected). + */ + public static final boolean INITIAL_CHECKBOX_SELECTED = true; + + /** + * The selection model that tracks the check box state. + */ + private TreeSelectionModel theCheckBoxSelectionModel; + + /** + * Constructor. + * + * @param root the vector of check box nodes (rows) to place into the tree + */ + private RocketPrintTree(Vector root) { + super(root); + + //Remove the little down and sideways arrows. These are not needed because the tree expansion is fixed. + ((javax.swing.plaf.basic.BasicTreeUI) this.getUI()). + setExpandedIcon(null); + ((javax.swing.plaf.basic.BasicTreeUI) this.getUI()). + setCollapsedIcon(null); + } + + /** + * Factory method to create a specialized JTree. This version is for rocket's that have more than one stage. + * + * @param rocketName the name of the rocket + * @param stages the array of all stages + * + * @return an instance of JTree + */ + public static RocketPrintTree create(String rocketName, List stages) { + Vector root = new Vector(); + Vector toAddTo = root; + + if (stages != null) { + if (stages.size() > 1) { + final Vector parent = new NamedVector(rocketName != null ? rocketName : "Rocket"); + + root.add(parent); + toAddTo = parent; + } + for (RocketComponent stage : stages) { + if (stage instanceof Stage) { + toAddTo.add(createNamedVector(stage.getName(), createPrintTreeNode(true), stage.getStageNumber())); + } + } + } + + List unstaged = OpenRocketPrintable.getUnstaged(); + for (int i = 0; i < unstaged.size(); i++) { + toAddTo.add(new CheckBoxNode(unstaged.get(i).getDescription(), + INITIAL_CHECKBOX_SELECTED)); + } + + RocketPrintTree tree = new RocketPrintTree(root); + + tree.addTreeWillExpandListener + (new TreeWillExpandListener() { + @Override + public void treeWillExpand(TreeExpansionEvent e) { + } + + @Override + public void treeWillCollapse(TreeExpansionEvent e) + throws ExpandVetoException { + throw new ExpandVetoException(e, "you can't collapse this JTree"); + } + }); + + return tree; + } + + /** + * Factory method to create a specialized JTree. This version is for a rocket with only one stage. + * + * @param rocketName the name of the rocket + * + * @return an instance of JTree + */ + public static RocketPrintTree create(String rocketName) { + Vector root = new Vector(); + root.add(new NamedVector(rocketName != null ? rocketName : "Rocket", createPrintTreeNode(false))); + + RocketPrintTree tree = new RocketPrintTree(root); + + tree.addTreeWillExpandListener + (new TreeWillExpandListener() { + @Override + public void treeWillExpand(TreeExpansionEvent e) { + } + + @Override + public void treeWillCollapse(TreeExpansionEvent e) + throws ExpandVetoException { + throw new ExpandVetoException(e, "you can't collapse this JTree"); + } + }); + + return tree; + } + + /** + * This tree needs to have access both to the normal selection model (for the textual row) which is managed by the + * superclass, as well as the selection model for the check boxes. This mutator method allows an external class to + * set the model back onto this class. Because of some unfortunate circular dependencies this cannot be set at + * construction. + *

+ * TODO: Ensure these circular references get cleaned up properly at dialog disposal so everything can be GC'd. + * + * @param checkBoxSelectionModel the selection model used to keep track of the check box state + */ + public void setCheckBoxSelectionModel(TreeSelectionModel checkBoxSelectionModel) { + theCheckBoxSelectionModel = checkBoxSelectionModel; + } + + /** + * Add a selection path to the internal check box selection model. The normal JTree selection model is unaffected. + * This has the effect of "selecting" the check box, but not highlighting the row. + * + * @param path the path (row) + */ + @Override + public void addSelectionPath(TreePath path) { + theCheckBoxSelectionModel.addSelectionPath(path); + } + + /** + * Helper to construct a named vector. + * + * @param name the name of the vector + * @param nodes the array of nodes to put into the vector + * @param stage the stage number + * + * @return a NamedVector suitable for adding to a JTree + */ + private static Vector createNamedVector(String name, CheckBoxNode[] nodes, int stage) { + return new NamedVector(name, nodes, stage); + } + + /** + * Helper to construct the set of check box rows for each stage. + * + * @param onlyStageSpecific if true then only stage specific OpenRocketPrintable rows are represented (in this part + * of the tree). + * + * @return an array of CheckBoxNode + */ + private static CheckBoxNode[] createPrintTreeNode(boolean onlyStageSpecific) { + List nodes = new ArrayList(); + OpenRocketPrintable[] printables = OpenRocketPrintable.values(); + for (OpenRocketPrintable openRocketPrintable : printables) { + if (!onlyStageSpecific || openRocketPrintable.isStageSpecific()) { + nodes.add(new CheckBoxNode(openRocketPrintable.getDescription(), + INITIAL_CHECKBOX_SELECTED)); + } + } + return nodes.toArray(new CheckBoxNode[nodes.size()]); + } + + /** + * Get the set of items to be printed, as selected by the user. + * + * @return the things to be printed, returned as an Iterator + */ + public Iterator getToBePrinted() { + final DefaultMutableTreeNode mutableTreeNode = (DefaultMutableTreeNode) getModel().getRoot(); + PrintableContext pc = new PrintableContext(); + add(pc, mutableTreeNode); + return pc.iterator(); + } + + /** + * Walk a tree, finding everything that has been selected and aggregating it into something that can be iterated upon + * This method is recursive. + * + * @param pc the printable context that aggregates the choices into an iterator + * @param theMutableTreeNode the root node + */ + private void add(final PrintableContext pc, final DefaultMutableTreeNode theMutableTreeNode) { + int children = theMutableTreeNode.getChildCount(); + for (int x = 0; x < children; x++) { + + final DefaultMutableTreeNode at = (DefaultMutableTreeNode) theMutableTreeNode.getChildAt(x); + if (at.getUserObject() instanceof CheckBoxNode) { + CheckBoxNode cbn = (CheckBoxNode) at.getUserObject(); + if (cbn.isSelected()) { + final OpenRocketPrintable printable = OpenRocketPrintable.findByDescription(cbn.getText()); + pc.add( + printable.isStageSpecific() ? ((NamedVector) theMutableTreeNode.getUserObject()) + .getStage() : null, + printable); + } + } + add(pc, at); + } + } + +} + +/** + * JTree's work off of Vector's (unfortunately). This class is tailored for use with check boxes in the JTree. + */ +class NamedVector extends Vector { + String name; + + int stageNumber; + + public NamedVector(String theName) { + name = theName; + } + + public NamedVector(String theName, CheckBoxNode elements[], int stage) { + this(theName, elements); + stageNumber = stage; + } + + public NamedVector(String theName, CheckBoxNode elements[]) { + name = theName; + for (int i = 0, n = elements.length; i < n; i++) { + add(elements[i]); + } + } + + @Override + public String toString() { + return name; + } + + public int getStage() { + return stageNumber; + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/visitor/FinMarkingGuideStrategy.java b/core/src/net/sf/openrocket/gui/print/visitor/FinMarkingGuideStrategy.java new file mode 100644 index 00000000..f4e5a526 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/visitor/FinMarkingGuideStrategy.java @@ -0,0 +1,167 @@ +package net.sf.openrocket.gui.print.visitor; + +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.PdfContentByte; +import com.itextpdf.text.pdf.PdfWriter; +import net.sf.openrocket.gui.print.FinMarkingGuide; +import net.sf.openrocket.gui.print.ITextHelper; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * A strategy for drawing a fin marking guide. As currently implemented, each body tube with a finset will have + * a marking guide. If a tube has multiple fin sets, they are combined onto one marking guide. Launch lugs are supported + * as well. + */ +public class FinMarkingGuideStrategy { + + /** + * The logger. + */ + private static final LogHelper log = Application.getLogger(); + + /** + * The iText document. + */ + protected Document document; + + /** + * The direct iText writer. + */ + protected PdfWriter writer; + + /** + * Constructor. + * + * @param doc The iText document + * @param theWriter The direct iText writer + */ + public FinMarkingGuideStrategy(Document doc, PdfWriter theWriter) { + document = doc; + writer = theWriter; + } + + /** + * Recurse through the given rocket component. + * + * @param root the root component; all children will be visited recursively + */ + public void writeToDocument(final Rocket root) { + render(root); + } + + + /** + * The core behavior of this strategy. + * + * @param rocket the rocket to render all + */ + private void render(final Rocket rocket) { + try { + FinMarkingGuide pfs = new FinMarkingGuide(rocket); + + java.awt.Dimension size = pfs.getSize(); + final Dimension pageSize = getPageSize(); + if (fitsOnOnePage(pageSize, size.getWidth(), size.getHeight())) { + printOnOnePage(pfs); + } else { + BufferedImage image = (BufferedImage) pfs.createImage(); + ITextHelper.renderImageAcrossPages(new Rectangle(pageSize.getWidth(), pageSize.getHeight()), + document, writer, image); + } + } catch (DocumentException e) { + log.error("Could not render the fin marking guide.", e); + } + } + + /** + * Determine if the image will fit on the given page. + * + * @param pageSize the page size + * @param wImage the width of the thing to be printed + * @param hImage the height of the thing to be printed + * @return true if the thing to be printed will fit on a single page + */ + private boolean fitsOnOnePage(Dimension pageSize, double wImage, double hImage) { + double wPage = pageSize.getWidth(); + double hPage = pageSize.getHeight(); + + int wRatio = (int) Math.ceil(wImage / wPage); + int hRatio = (int) Math.ceil(hImage / hPage); + + return wRatio <= 1.0d && hRatio <= 1.0d; + } + + /** + * Print the transition. + * + * @param theMarkingGuide the fin marking guide + */ + private void printOnOnePage(final FinMarkingGuide theMarkingGuide) { + Dimension d = getPageSize(); + PdfContentByte cb = writer.getDirectContent(); + Graphics2D g2 = cb.createGraphics(d.width, d.height); + theMarkingGuide.print(g2); + g2.dispose(); + document.newPage(); + } + + /** + * Get the dimensions of the paper page. + * + * @return an internal Dimension + */ + protected Dimension getPageSize() { + return new Dimension(document.getPageSize().getWidth(), + document.getPageSize().getHeight()); + } + + /** + * Convenience class to model a dimension. + */ + class Dimension { + /** + * Width, in points. + */ + public float width; + /** + * Height, in points. + */ + public float height; + + /** + * Constructor. + * + * @param w width + * @param h height + */ + public Dimension(float w, float h) { + width = w; + height = h; + } + + /** + * Get the width. + * + * @return the width + */ + public float getWidth() { + return width; + } + + /** + * Get the height. + * + * @return the height + */ + public float getHeight() { + return height; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/print/visitor/FinSetPrintStrategy.java b/core/src/net/sf/openrocket/gui/print/visitor/FinSetPrintStrategy.java new file mode 100644 index 00000000..80e15a21 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/visitor/FinSetPrintStrategy.java @@ -0,0 +1,216 @@ +/* + * FinSetPrintStrategy.java + */ +package net.sf.openrocket.gui.print.visitor; + +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.PdfContentByte; +import com.itextpdf.text.pdf.PdfWriter; +import net.sf.openrocket.gui.print.ITextHelper; +import net.sf.openrocket.gui.print.PrintableFinSet; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Set; + +/** + * A strategy for drawing fin templates. + */ +public class FinSetPrintStrategy { + + /** + * The logger. + */ + private static final LogHelper log = Application.getLogger(); + + /** + * The iText document. + */ + protected Document document; + + /** + * The direct iText writer. + */ + protected PdfWriter writer; + + /** + * The stages selected. + */ + protected Set stages; + + /** + * Constructor. + * + * @param doc The iText document + * @param theWriter The direct iText writer + * @param theStages The stages to be printed by this strategy + */ + public FinSetPrintStrategy(Document doc, PdfWriter theWriter, Set theStages) { + document = doc; + writer = theWriter; + stages = theStages; + } + + /** + * Recurse through the given rocket component. + * + * @param root the root component; all children will be printed recursively + */ + public void writeToDocument (final RocketComponent root) { + List rc = root.getChildren(); + goDeep(rc); + } + + + /** + * Recurse through the given rocket component. + * + * @param theRc an array of rocket components; all children will be printed recursively + */ + protected void goDeep (final List theRc) { + for (RocketComponent rocketComponent : theRc) { + if (rocketComponent instanceof FinSet) { + printFinSet((FinSet) rocketComponent); + } + else if (rocketComponent.getChildCount() > 0) { + goDeep(rocketComponent.getChildren()); + } + } + } + + /** + * The core behavior of this strategy. + * + * @param finSet the object to extract info about; a graphical image of the fin shape is drawn to the document + */ + private void printFinSet(final FinSet finSet) { + if (shouldPrintStage(finSet.getStageNumber())) { + try { + PrintableFinSet pfs = new PrintableFinSet(finSet); + + java.awt.Dimension finSize = pfs.getSize(); + final Dimension pageSize = getPageSize(); + if (fitsOnOnePage(pageSize, finSize.getWidth(), finSize.getHeight())) { + printOnOnePage(pfs); + } + else { + BufferedImage image = (BufferedImage) pfs.createImage(); + ITextHelper.renderImageAcrossPages(new Rectangle(pageSize.getWidth(), pageSize.getHeight()), + document, writer, image); + } + document.newPage(); + } + catch (DocumentException e) { + log.error("Could not render fin.", e); + } + } + } + + /** + * Determine if the strategy's set of stage numbers (to print) contains the specified stage. + * + * @param stageNumber a stage number + * + * @return true if the strategy contains the stage number provided + */ + public boolean shouldPrintStage(int stageNumber) { + if (stages == null || stages.isEmpty()) { + return false; + } + + for (final Integer stage : stages) { + if (stage == stageNumber) { + return true; + } + } + + return false; + } + + /** + * Determine if the image will fit on the given page. + * + * @param pageSize the page size + * @param wImage the width of the thing to be printed + * @param hImage the height of the thing to be printed + * + * @return true if the thing to be printed will fit on a single page + */ + private boolean fitsOnOnePage (Dimension pageSize, double wImage, double hImage) { + double wPage = pageSize.getWidth(); + double hPage = pageSize.getHeight(); + + int wRatio = (int) Math.ceil(wImage / wPage); + int hRatio = (int) Math.ceil(hImage / hPage); + + return wRatio <= 1.0d && hRatio <= 1.0d; + } + + /** + * Print the fin set. + * + * @param thePfs the printable fin set + */ + private void printOnOnePage (final PrintableFinSet thePfs) { + Dimension d = getPageSize(); + PdfContentByte cb = writer.getDirectContent(); + Graphics2D g2 = cb.createGraphics(d.width, d.height); + thePfs.print(g2); + g2.dispose(); + } + + /** + * Get the dimensions of the paper page. + * + * @return an internal Dimension + */ + protected Dimension getPageSize () { + return new Dimension(document.getPageSize().getWidth(), + document.getPageSize().getHeight()); + } + + /** + * Convenience class to model a dimension. + */ + class Dimension { + /** Width, in points. */ + public float width; + /** Height, in points. */ + public float height; + + /** + * Constructor. + * @param w width + * @param h height + */ + public Dimension (float w, float h) { + width = w; + height = h; + } + + /** + * Get the width. + * + * @return the width + */ + public float getWidth () { + return width; + } + + /** + * Get the height. + * + * @return the height + */ + public float getHeight () { + return height; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java b/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java new file mode 100644 index 00000000..cdeb9247 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java @@ -0,0 +1,664 @@ +/* + * PartsDetailVisitorStrategy.java + */ +package net.sf.openrocket.gui.print.visitor; + +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.PdfPCell; +import com.itextpdf.text.pdf.PdfPTable; +import com.itextpdf.text.pdf.PdfWriter; +import net.sf.openrocket.gui.main.ComponentIcons; +import net.sf.openrocket.gui.print.ITextHelper; +import net.sf.openrocket.gui.print.PrintUtilities; +import net.sf.openrocket.gui.print.PrintableFinSet; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.*; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Coordinate; + +import javax.swing.*; +import java.text.NumberFormat; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * A visitor strategy for creating documentation about parts details. + */ +public class PartsDetailVisitorStrategy { + + /** + * The logger. + */ + private static final LogHelper log = Application.getLogger(); + + /** + * The number of columns in the table. + */ + private static final int TABLE_COLUMNS = 7; + + /** + * The parts detail is represented as an iText table. + */ + PdfPTable grid; + + /** + * The iText document. + */ + protected Document document; + + /** + * The direct iText writer. + */ + protected PdfWriter writer; + + /** + * The stages selected. + */ + protected Set stages; + + /** + * State variable to track the level of hierarchy. + */ + protected int level = 0; + + private static final String LINES = "Lines: "; + private static final String MASS = "Mass: "; + private static final String LEN = "Len: "; + private static final String THICK = "Thick: "; + private static final String INNER = "in "; + private static final String DIAMETER = "Dia"; + private static final String OUTER = "out"; + private static final String WIDTH = "Width"; + private static final String LENGTH = "Length"; + private static final String SHROUD_LINES = "Shroud Lines"; + private static final String AFT_DIAMETER = "Aft Dia: "; + private static final String FORE_DIAMETER = "Fore Dia: "; + private static final String PARTS_DETAIL = "Parts Detail"; + + /** + * Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts. + * + * @param doc The iText document + * @param theWriter The direct iText writer + * @param theStagesToVisit The stages to be visited by this strategy + */ + public PartsDetailVisitorStrategy (Document doc, PdfWriter theWriter, Set theStagesToVisit) { + document = doc; + writer = theWriter; + stages = theStagesToVisit; + PrintUtilities.addText(doc, PrintUtilities.BIG_BOLD, PARTS_DETAIL); + } + + /** + * Print the parts detail. + * + * @param root the root component + */ + public void writeToDocument (final RocketComponent root) { + goDeep(root.getChildren()); + } + + /** + * Recurse through the given rocket component. + * + * @param theRc an array of rocket components; all children will be visited recursively + */ + protected void goDeep (final List theRc) { + level++; + for (RocketComponent rocketComponent : theRc) { + handle(rocketComponent); + } + level--; + } + + /** + * Add a line to the detail report based upon the type of the component. + * + * @param component the component to print the detail for + */ + private void handle (RocketComponent component) { + //This ugly if-then-else construct is not object oriented. Originally it was an elegant, and very OO savy, design + //using the Visitor pattern. Unfortunately, it was misunderstood and was removed. + if (component instanceof Stage) { + try { + if (grid != null) { + document.add(grid); + } + document.add(ITextHelper.createPhrase(component.getName())); + grid = new PdfPTable(TABLE_COLUMNS); + grid.setWidthPercentage(100); + grid.setHorizontalAlignment(Element.ALIGN_LEFT); + } + catch (DocumentException e) { + } + + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof LaunchLug) { + LaunchLug ll = (LaunchLug) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + + grid.addCell(createMaterialCell(ll.getMaterial())); + grid.addCell(createOuterInnerDiaCell(ll)); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + } + else if (component instanceof NoseCone) { + NoseCone nc = (NoseCone) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(nc.getMaterial())); + grid.addCell(ITextHelper.createCell(nc.getType().getName(), PdfPCell.BOTTOM)); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof Transition) { + Transition tran = (Transition) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(tran.getMaterial())); + + Chunk fore = new Chunk(FORE_DIAMETER + toLength(tran.getForeRadius() * 2)); + fore.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + Chunk aft = new Chunk(AFT_DIAMETER + toLength(tran.getAftRadius() * 2)); + aft.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + final PdfPCell cell = ITextHelper.createCell(); + cell.addElement(fore); + cell.addElement(aft); + grid.addCell(cell); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof BodyTube) { + BodyTube bt = (BodyTube) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(bt.getMaterial())); + grid.addCell(createOuterInnerDiaCell(bt)); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof FinSet) { + handleFins((FinSet) component); + } + else if (component instanceof BodyComponent) { + grid.addCell(component.getName()); + grid.completeRow(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof ExternalComponent) { + ExternalComponent ext = (ExternalComponent) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + + grid.addCell(createMaterialCell(ext.getMaterial())); + grid.addCell(ITextHelper.createCell()); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof InnerTube) { + InnerTube it = (InnerTube) component; + grid.addCell(iconToImage(component)); + final PdfPCell pCell = createNameCell(component.getName(), true); + grid.addCell(pCell); + grid.addCell(createMaterialCell(it.getMaterial())); + grid.addCell(createOuterInnerDiaCell(it)); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof RadiusRingComponent) { + RadiusRingComponent rrc = (RadiusRingComponent) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(rrc.getMaterial())); + if (component instanceof Bulkhead) { + grid.addCell(createDiaCell(rrc.getOuterRadius()*2)); + } + else { + grid.addCell(createOuterInnerDiaCell(rrc)); + } + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof RingComponent) { + RingComponent ring = (RingComponent) component; + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(ring.getMaterial())); + grid.addCell(createOuterInnerDiaCell(ring)); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof ShockCord) { + ShockCord ring = (ShockCord) component; + PdfPCell cell = ITextHelper.createCell(); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setPaddingBottom(12f); + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(ring.getMaterial())); + grid.addCell(cell); + grid.addCell(createLengthCell(ring.getCordLength())); + grid.addCell(createMassCell(component.getMass())); + } + else if (component instanceof Parachute) { + Parachute chute = (Parachute) component; + PdfPCell cell = ITextHelper.createCell(); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setPaddingBottom(12f); + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(chute.getMaterial())); + grid.addCell(createDiaCell(chute.getDiameter())); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + + grid.addCell(iconToImage(null)); + grid.addCell(createNameCell(SHROUD_LINES, true)); + grid.addCell(createMaterialCell(chute.getLineMaterial())); + grid.addCell(createLinesCell(chute.getLineCount())); + grid.addCell(createLengthCell(chute.getLineLength())); + grid.addCell(cell); + } + else if (component instanceof Streamer) { + Streamer ring = (Streamer) component; + PdfPCell cell = ITextHelper.createCell(); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setPaddingBottom(12f); + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(ring.getMaterial())); + grid.addCell(createStrip(ring)); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + } + else if (component instanceof RecoveryDevice) { + RecoveryDevice device = (RecoveryDevice) component; + PdfPCell cell = ITextHelper.createCell(); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setPaddingBottom(12f); + grid.addCell(iconToImage(component)); + grid.addCell(createNameCell(component.getName(), true)); + grid.addCell(createMaterialCell(device.getMaterial())); + grid.addCell(cell); + grid.addCell(createLengthCell(component.getLength())); + grid.addCell(createMassCell(component.getMass())); + } + else if (component instanceof MassObject) { + PdfPCell cell = ITextHelper.createCell(); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setPaddingBottom(12f); + + grid.addCell(iconToImage(component)); + final PdfPCell nameCell = createNameCell(component.getName(), true); + nameCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + nameCell.setPaddingBottom(12f); + grid.addCell(nameCell); + grid.addCell(cell); + grid.addCell(createDiaCell(((MassObject) component).getRadius() * 2)); + grid.addCell(cell); + grid.addCell(createMassCell(component.getMass())); + } + } + + /** + * Close the strategy by adding the last grid to the document. + */ + public void close () { + try { + if (grid != null) { + document.add(grid); + } + } + catch (DocumentException e) { + log.error("Could not write last cell to document.", e); + } + } + + /** + * Create a cell to document an outer 'diameter'. This is used for components that have no inner diameter, such as + * a solid parachute or bulkhead. + * + * @param diameter the diameter in default length units + * + * @return a formatted cell containing the diameter + */ + private PdfPCell createDiaCell (final double diameter) { + PdfPCell result = new PdfPCell(); + Phrase p = new Phrase(); + p.setLeading(12f); + result.setVerticalAlignment(Element.ALIGN_TOP); + result.setBorder(Rectangle.BOTTOM); + Chunk c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(DIAMETER); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); + c.append(OUTER); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(" " + toLength(diameter)); + p.add(c); + result.addElement(p); + return result; + } + + /** + * Create a PDF cell for a streamer. + * + * @param component a component that is a Coaxial + * @return the PDF cell that has the streamer documented + */ + private PdfPCell createStrip (final Streamer component) { + PdfPCell result = new PdfPCell(); + Phrase p = new Phrase(); + p.setLeading(12f); + result.setVerticalAlignment(Element.ALIGN_TOP); + result.setBorder(Rectangle.BOTTOM); + Chunk c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(LENGTH); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(" " + toLength(component.getStripLength())); + p.add(c); + result.addElement(p); + + Phrase pw = new Phrase(); + pw.setLeading(14f); + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(WIDTH); + pw.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(" " + toLength(component.getStripWidth())); + pw.add(c); + result.addElement(pw); + + return result; + } + + /** + * Create a PDF cell that documents both an outer and an inner diameter of a component. + * + * @param component a component that is a Coaxial + * @return the PDF cell that has the outer and inner diameters documented + */ + private PdfPCell createOuterInnerDiaCell (final Coaxial component) { + PdfPCell result = new PdfPCell(); + Phrase p = new Phrase(); + p.setLeading(12f); + result.setVerticalAlignment(Element.ALIGN_TOP); + result.setBorder(Rectangle.BOTTOM); + Chunk c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(DIAMETER); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); + c.append(OUTER); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(" " + toLength(component.getOuterRadius() * 2)); + p.add(c); + createInnerDiaCell(component, result); + result.addElement(p); + return result; + } + + /** + * Add inner diameter data to a cell. + * + * @param component a component that is a Coaxial + * @param cell the PDF cell to add the inner diameter data to + */ + private void createInnerDiaCell (final Coaxial component, PdfPCell cell) { + Phrase p = new Phrase(); + p.setLeading(14f); + Chunk c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(DIAMETER); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); + c.append(INNER); + p.add(c); + + c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(" " + toLength(component.getInnerRadius() * 2)); + p.add(c); + cell.addElement(p); + } + + /** + * Add PDF cells for a fin set. + * + * @param theFinSet the fin set + */ + private void handleFins (FinSet theFinSet) { + + Image img = null; + java.awt.Image awtImage = new PrintableFinSet(theFinSet).createImage(); + + Collection x = theFinSet.getComponentBounds(); + + try { + img = Image.getInstance(writer, awtImage, 0.25f); + } + catch (Exception e) { + log.error("Could not write image to document.", e); + } + + grid.addCell(iconToImage(theFinSet)); + grid.addCell(createNameCell(theFinSet.getName() + " (" + theFinSet.getFinCount() + ")", true)); + grid.addCell(createMaterialCell(theFinSet.getMaterial())); + grid.addCell(ITextHelper.createCell(THICK + toLength(theFinSet.getThickness()), PdfPCell.BOTTOM)); + final PdfPCell pCell = new PdfPCell(); + pCell.setBorder(Rectangle.BOTTOM); + pCell.addElement(img); + + grid.addCell(ITextHelper.createCell()); + grid.addCell(createMassCell(theFinSet.getMass())); + + List rc = theFinSet.getChildren(); + goDeep(rc); + } + + /** + * Create a length formatted cell. + * + * @param length the length, in default length units + * + * @return a PdfPCell that is formatted with the length + */ + protected PdfPCell createLengthCell (double length) { + return ITextHelper.createCell(LEN + toLength(length), PdfPCell.BOTTOM); + } + + /** + * Create a mass formatted cell. + * + * @param mass the mass, in default mass units + * + * @return a PdfPCell that is formatted with the mass + */ + protected PdfPCell createMassCell (double mass) { + return ITextHelper.createCell(MASS + toMass(mass), PdfPCell.BOTTOM); + } + + /** + * Create a (shroud) line count formatted cell. + * + * @param count the number of shroud lines + * + * @return a PdfPCell that is formatted with the line count + */ + protected PdfPCell createLinesCell (int count) { + return ITextHelper.createCell(LINES + count, PdfPCell.BOTTOM); + } + + /** + * Create a cell formatted for a name (or any string for that matter). + * + * @param v the string to format into a PDF cell + * @param withIndent if true, then an indention is made scaled to the level of the part in the parent hierarchy + * + * @return a PdfPCell that is formatted with the string v + */ + protected PdfPCell createNameCell (String v, boolean withIndent) { + PdfPCell result = new PdfPCell(); + result.setBorder(Rectangle.BOTTOM); + Chunk c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + if (withIndent) { + for (int x = 0; x < (level - 2) * 10; x++) { + c.append(" "); + } + } + c.append(v); + result.setColspan(2); + result.addElement(c); + return result; + } + + /** + * Create a cell that describes a material. + * + * @param material the material + * + * @return a PdfPCell that is formatted with a description of the material + */ + protected PdfPCell createMaterialCell (Material material) { + PdfPCell cell = ITextHelper.createCell(); + cell.setLeading(13f, 0); + + Chunk c = new Chunk(); + c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); + c.append(toMaterialName(material)); + cell.addElement(c); + Chunk density = new Chunk(); + density.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); + density.append(toMaterialDensity(material)); + cell.addElement(density); + return cell; + } + + /** + * Get the icon of the particular type of rocket component and conver it to an image in a PDF cell. + * + * @param visitable the rocket component to create a cell with it's image + * + * @return a PdfPCell that is just an image that can be put into a PDF + */ + protected PdfPCell iconToImage (final RocketComponent visitable) { + if (visitable != null) { + final ImageIcon icon = (ImageIcon) ComponentIcons.getLargeIcon(visitable.getClass()); + try { + if (icon != null) { + Image im = Image.getInstance(icon.getImage(), null); + if (im != null) { + im.scaleToFit(icon.getIconWidth() * 0.6f, icon.getIconHeight() * 0.6f); + PdfPCell cell = new PdfPCell(im); + cell.setFixedHeight(icon.getIconHeight() * 0.6f); + cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setBorder(PdfPCell.NO_BORDER); + return cell; + } + } + } + catch (Exception e) { + } + } + PdfPCell cell = new PdfPCell(); + cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER); + cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); + cell.setBorder(PdfPCell.NO_BORDER); + return cell; + } + + /** + * Format the length as a displayable string. + * + * @param length the length (assumed to be in default length units) + * + * @return a string representation of the length with unit abbreviation + */ + protected String toLength (double length) { + final Unit defaultUnit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); + return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(length)) + defaultUnit.toString(); + } + + /** + * Format the mass as a displayable string. + * + * @param mass the mass (assumed to be in default mass units) + * + * @return a string representation of the mass with mass abbreviation + */ + protected String toMass (double mass) { + final Unit defaultUnit = UnitGroup.UNITS_MASS.getDefaultUnit(); + return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(mass)) + defaultUnit.toString(); + } + + /** + * Get a displayable string of the material's name. + * + * @param material the material to output + * + * @return the material name + */ + protected String toMaterialName (Material material) { + return material.getName(); + } + + /** + * Format the material density as a displayable string. + * + * @param material the material to output + * + * @return a string representation of the material density + */ + protected String toMaterialDensity (Material material) { + return " (" + material.getType().getUnitGroup().getDefaultUnit().toStringUnit(material.getDensity()) + ")"; + } + +} diff --git a/core/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java b/core/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java new file mode 100644 index 00000000..e8ecc0ba --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java @@ -0,0 +1,235 @@ +/* + * PartsListVisitorStrategy.java + */ +package net.sf.openrocket.gui.print.visitor; + +import com.itextpdf.text.Document; +import com.itextpdf.text.pdf.PdfWriter; +import net.sf.openrocket.rocketcomponent.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A visitor strategy for creating documentation about a parts list. + */ +public class PartsListVisitorStrategy { + + /** + * Accumulator for parts data. + */ + private Map crap = new HashMap(); + + /** + * The iText document. + */ + protected Document document; + + /** + * The direct iText writer. + */ + protected PdfWriter writer; + + /** + * The stages selected. + */ + protected Set stages; + + + /** + * Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts. + * + * @param doc The iText document + * @param theWriter The direct iText writer + * @param theStagesToVisit The stages to be visited by this strategy + */ + public PartsListVisitorStrategy (Document doc, PdfWriter theWriter, Set theStagesToVisit) { + document = doc; + writer = theWriter; + stages = theStagesToVisit; + } + + + /** + * Print the parts detail. + * + * @param root the root component + */ + public void doVisit (final RocketComponent root) { + goDeep(root.getChildren()); + } + + /** + * Recurse through the given rocket component. + * + * @param theRc an array of rocket components; all children will be visited recursively + */ + protected void goDeep (final List theRc) { + for (RocketComponent rocketComponent : theRc) { + doIt(rocketComponent); + } + } + + /** + * {@inheritDoc} + */ + private void doIt (final RocketComponent component) { + if (component instanceof InnerTube) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof LaunchLug) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + } + + else if (component instanceof NoseCone) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof Transition) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof RadiusRingComponent) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof RingComponent) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof BodyTube) { + final PartsAccumulator key = new PartsAccumulator(component); + PartsAccumulator pa = crap.get(key); + if (pa == null) { + pa = key; + crap.put(pa, pa); + } + pa.increment(); + List rc = component.getChildren(); + goDeep(rc); + } + else if (component instanceof TrapezoidFinSet) { + } + else if (component instanceof EllipticalFinSet) { + } + else if (component instanceof FreeformFinSet) { + } + } + + + /** + * {@inheritDoc} + */ + public void close () { + for (PartsAccumulator partsAccumulator : crap.keySet()) { + System.err.println(partsAccumulator.component.getComponentName() + " " + partsAccumulator.quantity); + } + } + +} + +class PartsAccumulator { + + int quantity = 0; + + RocketComponent component; + + PartsAccumulator (RocketComponent theComponent) { + component = theComponent; + } + + void increment () { + quantity++; + } + + int quantity () { + return quantity; + } + + @Override + public boolean equals (final Object o1) { + if (this == o1) { + return true; + } + + RocketComponent that; + if (o1 instanceof net.sf.openrocket.gui.print.visitor.PartsAccumulator) { + that = ((net.sf.openrocket.gui.print.visitor.PartsAccumulator) o1).component; + } + else if (o1 instanceof RocketComponent) { + that = (RocketComponent) o1; + } + else { + return false; + } + + if (this.component.getClass().equals(that.getClass())) { + //If + if (that.getLength() == this.component.getLength()) { + if (that.getMass() == this.component.getMass()) { + return true; + } + } + if (this.component instanceof Coaxial && + that instanceof Coaxial) { + Coaxial cThis = (Coaxial) this.component; + Coaxial cThat = (Coaxial) that; + if (cThis.getInnerRadius() == cThat.getInnerRadius() && + cThis.getOuterRadius() == cThat.getOuterRadius()) { + return true; + } + } + return false; + } + return false; + } + + @Override + public int hashCode () { + return component.getComponentName().hashCode(); + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/print/visitor/TransitionStrategy.java b/core/src/net/sf/openrocket/gui/print/visitor/TransitionStrategy.java new file mode 100644 index 00000000..1988ab8a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/print/visitor/TransitionStrategy.java @@ -0,0 +1,205 @@ +package net.sf.openrocket.gui.print.visitor; + +import com.itextpdf.text.Document; +import com.itextpdf.text.DocumentException; +import com.itextpdf.text.Rectangle; +import com.itextpdf.text.pdf.PdfContentByte; +import com.itextpdf.text.pdf.PdfWriter; +import net.sf.openrocket.gui.print.AbstractPrintableTransition; +import net.sf.openrocket.gui.print.ITextHelper; +import net.sf.openrocket.gui.print.PrintableNoseCone; +import net.sf.openrocket.gui.print.PrintableTransition; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.startup.Application; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.Set; + +/** + * A strategy for drawing transition/shroud/nose cone templates. + */ +public class TransitionStrategy { + + /** + * The logger. + */ + private static final LogHelper log = Application.getLogger(); + + /** + * The iText document. + */ + protected Document document; + + /** + * The direct iText writer. + */ + protected PdfWriter writer; + + /** + * The stages selected. + */ + protected Set stages; + + /** + * Constructor. + * + * @param doc The iText document + * @param theWriter The direct iText writer + * @param theStagesToVisit The stages to be visited by this strategy + */ + public TransitionStrategy(Document doc, PdfWriter theWriter, Set theStagesToVisit) { + document = doc; + writer = theWriter; + stages = theStagesToVisit; + } + + /** + * Recurse through the given rocket component. + * + * @param root the root component; all children will be visited recursively + * @param noseCones nose cones are a special form of a transition; if true, then print nose cones + */ + public void writeToDocument(final RocketComponent root, boolean noseCones) { + List rc = root.getChildren(); + goDeep(rc, noseCones); + } + + + /** + * Recurse through the given rocket component. + * + * @param theRc an array of rocket components; all children will be visited recursively + * @param noseCones nose cones are a special form of a transition; if true, then print nose cones + */ + protected void goDeep(final List theRc, boolean noseCones) { + for (RocketComponent rocketComponent : theRc) { + if (rocketComponent instanceof NoseCone) { + if (noseCones) { + render((Transition) rocketComponent); + } + } else if (rocketComponent instanceof Transition && !noseCones) { + render((Transition) rocketComponent); + } else if (rocketComponent.getChildCount() > 0) { + goDeep(rocketComponent.getChildren(), noseCones); + } + } + } + + /** + * The core behavior of this visitor. + * + * @param component the object to extract info about; a graphical image of the transition shape is drawn to the document + */ + private void render(final Transition component) { + try { + AbstractPrintableTransition pfs; + if (component instanceof NoseCone) { + pfs = new PrintableNoseCone(component); + } else { + pfs = new PrintableTransition(component); + } + + java.awt.Dimension size = pfs.getSize(); + final Dimension pageSize = getPageSize(); + if (fitsOnOnePage(pageSize, size.getWidth(), size.getHeight())) { + printOnOnePage(pfs); + } else { + BufferedImage image = (BufferedImage) pfs.createImage(); + ITextHelper.renderImageAcrossPages(new Rectangle(pageSize.getWidth(), pageSize.getHeight()), + document, writer, image); + } + } catch (DocumentException e) { + log.error("Could not render the transition.", e); + } + } + + /** + * Determine if the image will fit on the given page. + * + * @param pageSize the page size + * @param wImage the width of the thing to be printed + * @param hImage the height of the thing to be printed + * @return true if the thing to be printed will fit on a single page + */ + private boolean fitsOnOnePage(Dimension pageSize, double wImage, double hImage) { + double wPage = pageSize.getWidth(); + double hPage = pageSize.getHeight(); + + int wRatio = (int) Math.ceil(wImage / wPage); + int hRatio = (int) Math.ceil(hImage / hPage); + + return wRatio <= 1.0d && hRatio <= 1.0d; + } + + /** + * Print the transition. + * + * @param theTransition the printable transition + */ + private void printOnOnePage(final AbstractPrintableTransition theTransition) { + Dimension d = getPageSize(); + PdfContentByte cb = writer.getDirectContent(); + Graphics2D g2 = cb.createGraphics(d.width, d.height); + theTransition.print(g2); + g2.dispose(); + document.newPage(); + } + + /** + * Get the dimensions of the paper page. + * + * @return an internal Dimension + */ + protected Dimension getPageSize() { + return new Dimension(document.getPageSize().getWidth(), + document.getPageSize().getHeight()); + } + + /** + * Convenience class to model a dimension. + */ + class Dimension { + /** + * Width, in points. + */ + public float width; + /** + * Height, in points. + */ + public float height; + + /** + * Constructor. + * + * @param w width + * @param h height + */ + public Dimension(float w, float h) { + width = w; + height = h; + } + + /** + * Get the width. + * + * @return the width + */ + public float getWidth() { + return width; + } + + /** + * Get the height. + * + * @return the height + */ + public float getHeight() { + return height; + } + } +} diff --git a/core/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java b/core/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java new file mode 100644 index 00000000..aa299490 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java @@ -0,0 +1,46 @@ +package net.sf.openrocket.gui.rocketfigure; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Transformation; + +import java.awt.Shape; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Rectangle2D; + + +public class BodyTubeShapes extends RocketComponentShapes { + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component; + + double length = tube.getLength(); + double radius = tube.getOuterRadius(); + Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); + + Shape[] s = new Shape[start.length]; + for (int i=0; i < start.length; i++) { + s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S, + length*S,2*radius*S); + } + return s; + } + + + public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component; + + double or = tube.getOuterRadius(); + + Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); + + Shape[] s = new Shape[start.length]; + for (int i=0; i < start.length; i++) { + s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S); + } + return s; + } + + +} diff --git a/core/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java b/core/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java new file mode 100644 index 00000000..0eb856f5 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java @@ -0,0 +1,297 @@ +package net.sf.openrocket.gui.rocketfigure; + +import java.awt.Shape; +import java.awt.geom.Path2D; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Transformation; + + +public class FinSetShapes extends RocketComponentShapes { + + // TODO: LOW: Clustering is ignored (FinSet cannot currently be clustered) + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component; + + + int fins = finset.getFinCount(); + Transformation cantRotation = finset.getCantRotation(); + Transformation baseRotation = finset.getBaseRotationTransformation(); + Transformation finRotation = finset.getFinRotationTransformation(); + + Coordinate finPoints[] = finset.getFinPointsWithTab(); + + + // TODO: MEDIUM: sloping radius + double radius = finset.getBodyRadius(); + + // Translate & rotate the coordinates + for (int i=0; i 0; maxIndex--) { + if (points[maxIndex-1].y < points[maxIndex].y) + break; + } + + transformPoints(points,cantRotation); + transformPoints(points,new Transformation(0,radius,0)); + transformPoints(points,baseRotation); + + + sidePoints = new Coordinate[points.length]; + backPoints = new Coordinate[2*(points.length-maxIndex)]; + double sign; + if (finset.getCantAngle() > 0) { + sign = 1.0; + } else { + sign = -1.0; + } + + // Calculate points for the side panel + for (i=0; i < points.length; i++) { + sidePoints[i] = points[i].add(0,0,sign*thickness/2); + } + + // Calculate points for the back portion + i=0; + for (int j=points.length-1; j >= maxIndex; j--, i++) { + backPoints[i] = points[j].add(0,0,sign*thickness/2); + } + for (int j=maxIndex; j <= points.length-1; j++, i++) { + backPoints[i] = points[j].add(0,0,-sign*thickness/2); + } + + // Generate shapes + Shape[] s; + if (thickness > 0.0005) { + + s = new Shape[fins*2]; + for (int fin=0; fin= 0.0012) && (ir > 0)) { + // Draw outer and inner + s = new Shape[start.length*2]; + for (int i=0; i < start.length; i++) { + s[2*i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S, + length*S,2*or*S); + s[2*i+1] = new Rectangle2D.Double(start[i].x*S,(start[i].y-ir)*S, + length*S,2*ir*S); + } + } else { + // Draw only outer + s = new Shape[start.length]; + for (int i=0; i < start.length; i++) { + s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S, + length*S,2*or*S); + } + } + return s; + } + + + public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component; + Shape[] s; + + double or = tube.getOuterRadius(); + double ir = tube.getInnerRadius(); + + + Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); + + if ((ir < or) && (ir > 0)) { + // Draw inner and outer + s = new Shape[start.length*2]; + for (int i=0; i < start.length; i++) { + s[2*i] = new Ellipse2D.Double((start[i].z-or)*S, (start[i].y-or)*S, + 2*or*S, 2*or*S); + s[2*i+1] = new Ellipse2D.Double((start[i].z-ir)*S, (start[i].y-ir)*S, + 2*ir*S, 2*ir*S); + } + } else { + // Draw only outer + s = new Shape[start.length]; + for (int i=0; i < start.length; i++) { + s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S); + } + } + return s; + } + +} diff --git a/core/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java b/core/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java new file mode 100644 index 00000000..43096273 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.gui.rocketfigure; + + +import java.awt.Shape; + +import net.sf.openrocket.gui.scalefigure.RocketFigure; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Transformation; + + +/** + * A catch-all, no-operation drawing component. + */ +public class RocketComponentShapes { + + protected static final double S = RocketFigure.EXTRA_SCALE; + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation t) { + // no-op + Application.getExceptionHandler().handleErrorCondition("ERROR: RocketComponent.getShapesSide called with " + + component); + return new Shape[0]; + } + + public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation t) { + // no-op + Application.getExceptionHandler().handleErrorCondition("ERROR: RocketComponent.getShapesBack called with " + +component); + return new Shape[0]; + } + + +} diff --git a/core/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java b/core/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java new file mode 100644 index 00000000..791057c7 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java @@ -0,0 +1,114 @@ +package net.sf.openrocket.gui.rocketfigure; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Transformation; + +import java.awt.*; +import java.awt.geom.Path2D; +import java.util.ArrayList; + + +public class SymmetricComponentShapes extends RocketComponentShapes { + private static final int MINPOINTS = 91; + private static final double ACCEPTABLE_ANGLE = Math.cos(7.0 * Math.PI / 180.0); + + // TODO: HIGH: adaptiveness sucks, remove it. + + // TODO: LOW: Uses only first component of cluster (not currently clusterable) + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + return getShapesSide(component, transformation, S); + } + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation, final double scaleFactor) { + net.sf.openrocket.rocketcomponent.SymmetricComponent c = (net.sf.openrocket.rocketcomponent.SymmetricComponent) component; + int i; + + final double delta = 0.0000001; + double x; + + ArrayList points = new ArrayList(); + x = delta; + points.add(new Coordinate(x, c.getRadius(x), 0)); + for (i = 1; i < MINPOINTS - 1; i++) { + x = c.getLength() * i / (MINPOINTS - 1); + points.add(new Coordinate(x, c.getRadius(x), 0)); + //System.out.println("Starting with x="+x); + } + x = c.getLength() - delta; + points.add(new Coordinate(x, c.getRadius(x), 0)); + + + i = 0; + while (i < points.size() - 2) { + if (angleAcceptable(points.get(i), points.get(i + 1), points.get(i + 2)) || + points.get(i + 1).x - points.get(i).x < 0.001) { // 1mm + i++; + continue; + } + + // Split the longer of the areas + int n; + if (points.get(i + 2).x - points.get(i + 1).x > points.get(i + 1).x - points.get(i).x) + n = i + 1; + else + n = i; + + x = (points.get(n).x + points.get(n + 1).x) / 2; + points.add(n + 1, new Coordinate(x, c.getRadius(x), 0)); + } + + + //System.out.println("Final points: "+points.size()); + + final int len = points.size(); + + for (i = 0; i < len; i++) { + points.set(i, c.toAbsolute(points.get(i))[0]); + } + + /* Show points: + Shape[] s = new Shape[len+1]; + final double d=0.001; + for (i=0; i= 0; i--) { + path.lineTo(points.get(i).x * scaleFactor, points.get(i).y * scaleFactor); + } + for (i = 0; i < len; i++) { + path.lineTo(points.get(i).x * scaleFactor, -points.get(i).y * scaleFactor); + } + path.lineTo(points.get(len - 1).x * scaleFactor, points.get(len - 1).y * scaleFactor); + path.closePath(); + + //s[len] = path; + //return s; + return new Shape[] { path }; + } + + private static boolean angleAcceptable(Coordinate v1, Coordinate v2, Coordinate v3) { + return (cosAngle(v1, v2, v3) > ACCEPTABLE_ANGLE); + } + + /* + * cosAngle = v1.v2 / |v1|*|v2| = v1.v2 / sqrt(v1.v1*v2.v2) + */ + private static double cosAngle(Coordinate v1, Coordinate v2, Coordinate v3) { + double cos; + double len; + cos = Coordinate.dot(v1.sub(v2), v2.sub(v3)); + len = MathUtil.safeSqrt(v1.sub(v2).length2() * v2.sub(v3).length2()); + return cos / len; + } +} diff --git a/core/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java b/core/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java new file mode 100644 index 00000000..2312c1bd --- /dev/null +++ b/core/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java @@ -0,0 +1,101 @@ +package net.sf.openrocket.gui.rocketfigure; + +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Transformation; + +import java.awt.*; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; + + +public class TransitionShapes extends RocketComponentShapes { + + // TODO: LOW: Uses only first component of cluster (not currently clusterable). + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + return getShapesSide(component, transformation, S); + } + + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation, final double scaleFactor) { + net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component; + + Shape[] mainShapes; + + // Simpler shape for conical transition, others use the method from SymmetricComponent + if (transition.getType() == Transition.Shape.CONICAL) { + double length = transition.getLength(); + double r1 = transition.getForeRadius(); + double r2 = transition.getAftRadius(); + Coordinate start = transformation.transform(transition. + toAbsolute(Coordinate.NUL)[0]); + + Path2D.Float path = new Path2D.Float(); + path.moveTo(start.x* scaleFactor, r1* scaleFactor); + path.lineTo((start.x+length)* scaleFactor, r2* scaleFactor); + path.lineTo((start.x+length)* scaleFactor, -r2* scaleFactor); + path.lineTo(start.x* scaleFactor, -r1* scaleFactor); + path.closePath(); + + mainShapes = new Shape[] { path }; + } else { + mainShapes = SymmetricComponentShapes.getShapesSide(component, transformation, scaleFactor); + } + + Rectangle2D.Double shoulder1=null, shoulder2=null; + int arrayLength = mainShapes.length; + + if (transition.getForeShoulderLength() > 0.0005) { + Coordinate start = transformation.transform(transition. + toAbsolute(Coordinate.NUL)[0]); + double r = transition.getForeShoulderRadius(); + double l = transition.getForeShoulderLength(); + shoulder1 = new Rectangle2D.Double((start.x-l)* scaleFactor, -r* scaleFactor, l* scaleFactor, 2*r* scaleFactor); + arrayLength++; + } + if (transition.getAftShoulderLength() > 0.0005) { + Coordinate start = transformation.transform(transition. + toAbsolute(new Coordinate(transition.getLength()))[0]); + double r = transition.getAftShoulderRadius(); + double l = transition.getAftShoulderLength(); + shoulder2 = new Rectangle2D.Double(start.x* scaleFactor, -r* scaleFactor, l* scaleFactor, 2*r* scaleFactor); + arrayLength++; + } + if (shoulder1==null && shoulder2==null) + return mainShapes; + + Shape[] shapes = new Shape[arrayLength]; + int i; + + for (i=0; i < mainShapes.length; i++) { + shapes[i] = mainShapes[i]; + } + if (shoulder1 != null) { + shapes[i] = shoulder1; + i++; + } + if (shoulder2 != null) { + shapes[i] = shoulder2; + } + return shapes; + } + + + public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, + Transformation transformation) { + net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component; + + double r1 = transition.getForeRadius(); + double r2 = transition.getAftRadius(); + + Shape[] s = new Shape[2]; + s[0] = new Ellipse2D.Double(-r1*S,-r1*S,2*r1*S,2*r1*S); + s[1] = new Ellipse2D.Double(-r2*S,-r2*S,2*r2*S,2*r2*S); + return s; + } + + +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java b/core/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java new file mode 100644 index 00000000..e22243de --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java @@ -0,0 +1,133 @@ +package net.sf.openrocket.gui.scalefigure; + +import java.awt.Color; +import java.awt.Dimension; +import java.util.EventListener; +import java.util.EventObject; +import java.util.LinkedList; +import java.util.List; + +import javax.swing.JPanel; + +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.util.StateChangeListener; + + +public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure { + + // Number of pixels to leave at edges when fitting figure + private static final int DEFAULT_BORDER_PIXELS_WIDTH = 30; + private static final int DEFAULT_BORDER_PIXELS_HEIGHT = 20; + + + protected final double dpi; + + protected double scale = 1.0; + protected double scaling = 1.0; + + protected int borderPixelsWidth = DEFAULT_BORDER_PIXELS_WIDTH; + protected int borderPixelsHeight = DEFAULT_BORDER_PIXELS_HEIGHT; + + protected final List listeners = new LinkedList(); + + + public AbstractScaleFigure() { + this.dpi = GUIUtil.getDPI(); + this.scaling = 1.0; + this.scale = dpi / 0.0254 * scaling; + + setBackground(Color.WHITE); + setOpaque(true); + } + + + + public abstract void updateFigure(); + + public abstract double getFigureWidth(); + + public abstract double getFigureHeight(); + + + @Override + public double getScaling() { + return scaling; + } + + @Override + public double getAbsoluteScale() { + return scale; + } + + @Override + public void setScaling(double scaling) { + if (Double.isInfinite(scaling) || Double.isNaN(scaling)) + scaling = 1.0; + if (scaling < 0.001) + scaling = 0.001; + if (scaling > 1000) + scaling = 1000; + if (Math.abs(this.scaling - scaling) < 0.01) + return; + this.scaling = scaling; + this.scale = dpi / 0.0254 * scaling; + updateFigure(); + } + + @Override + public void setScaling(Dimension bounds) { + double zh = 1, zv = 1; + int w = bounds.width - 2 * borderPixelsWidth - 20; + int h = bounds.height - 2 * borderPixelsHeight - 20; + + if (w < 10) + w = 10; + if (h < 10) + h = 10; + + zh = (w) / getFigureWidth(); + zv = (h) / getFigureHeight(); + + double s = Math.min(zh, zv) / dpi * 0.0254 - 0.001; + + setScaling(s); + } + + + @Override + public Dimension getBorderPixels() { + return new Dimension(borderPixelsWidth, borderPixelsHeight); + } + + @Override + public void setBorderPixels(int width, int height) { + this.borderPixelsWidth = width; + this.borderPixelsHeight = height; + } + + + @Override + public void addChangeListener(EventListener listener) { + listeners.add(0, listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listeners.remove(listener); + } + + private EventObject changeEvent = null; + + protected void fireChangeEvent() { + if (changeEvent == null) + changeEvent = new EventObject(this); + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] list = listeners.toArray(new EventListener[0]); + for (EventListener l : list) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(changeEvent); + } + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java b/core/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java new file mode 100644 index 00000000..0f08be21 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java @@ -0,0 +1,344 @@ +package net.sf.openrocket.gui.scalefigure; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.geom.AffineTransform; +import java.awt.geom.Line2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Path2D; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; + +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.unit.Tick; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +// TODO: MEDIUM: the figure jumps and bugs when using automatic fitting + +public class FinPointFigure extends AbstractScaleFigure { + + private static final int BOX_SIZE = 4; + + private final FreeformFinSet finset; + private int modID = -1; + + private double minX, maxX, maxY; + private double figureWidth = 0; + private double figureHeight = 0; + private double translateX = 0; + private double translateY = 0; + + private AffineTransform transform; + private Rectangle2D.Double[] handles = null; + + + public FinPointFigure(FreeformFinSet finset) { + this.finset = finset; + } + + + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + + if (modID != finset.getRocket().getAerodynamicModID()) { + modID = finset.getRocket().getAerodynamicModID(); + calculateDimensions(); + } + + + double tx, ty; + // Calculate translation for figure centering + if (figureWidth * scale + 2 * borderPixelsWidth < getWidth()) { + + // Figure fits in the viewport + tx = (getWidth() - figureWidth * scale) / 2 - minX * scale; + + } else { + + // Figure does not fit in viewport + tx = borderPixelsWidth - minX * scale; + + } + + + if (figureHeight * scale + 2 * borderPixelsHeight < getHeight()) { + ty = getHeight() - borderPixelsHeight; + } else { + ty = borderPixelsHeight + figureHeight * scale; + } + + if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { + // Origin has changed, fire event + translateX = tx; + translateY = ty; + fireChangeEvent(); + } + + + if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { + // Origin has changed, fire event + translateX = tx; + translateY = ty; + fireChangeEvent(); + } + + + // Calculate and store the transformation used + transform = new AffineTransform(); + transform.translate(translateX, translateY); + transform.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); + + // TODO: HIGH: border Y-scale upwards + + g2.transform(transform); + + // Set rendering hints appropriately + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + + + Rectangle visible = g2.getClipBounds(); + double x0 = ((double) visible.x - 3) / EXTRA_SCALE; + double x1 = ((double) visible.x + visible.width + 4) / EXTRA_SCALE; + double y0 = ((double) visible.y - 3) / EXTRA_SCALE; + double y1 = ((double) visible.y + visible.height + 4) / EXTRA_SCALE; + + + // Background grid + + g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(new Color(0, 0, 255, 30)); + + Unit unit; + if (this.getParent() != null && + this.getParent().getParent() instanceof ScaleScrollPane) { + unit = ((ScaleScrollPane) this.getParent().getParent()).getCurrentUnit(); + } else { + unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); + } + + // vertical + Tick[] ticks = unit.getTicks(x0, x1, + ScaleScrollPane.MINOR_TICKS / scale, + ScaleScrollPane.MAJOR_TICKS / scale); + Line2D.Double line = new Line2D.Double(); + for (Tick t : ticks) { + if (t.major) { + line.setLine(t.value * EXTRA_SCALE, y0 * EXTRA_SCALE, + t.value * EXTRA_SCALE, y1 * EXTRA_SCALE); + g2.draw(line); + } + } + + // horizontal + ticks = unit.getTicks(y0, y1, + ScaleScrollPane.MINOR_TICKS / scale, + ScaleScrollPane.MAJOR_TICKS / scale); + for (Tick t : ticks) { + if (t.major) { + line.setLine(x0 * EXTRA_SCALE, t.value * EXTRA_SCALE, + x1 * EXTRA_SCALE, t.value * EXTRA_SCALE); + g2.draw(line); + } + } + + + + + + // Base rocket line + g2.setStroke(new BasicStroke((float) (3.0 * EXTRA_SCALE / scale), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(Color.GRAY); + + g2.drawLine((int) (x0 * EXTRA_SCALE), 0, (int) (x1 * EXTRA_SCALE), 0); + + + // Fin shape + Coordinate[] points = finset.getFinPoints(); + Path2D.Double shape = new Path2D.Double(); + shape.moveTo(0, 0); + for (int i = 1; i < points.length; i++) { + shape.lineTo(points[i].x * EXTRA_SCALE, points[i].y * EXTRA_SCALE); + } + + g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setColor(Color.BLACK); + g2.draw(shape); + + + // Fin point boxes + g2.setColor(new Color(150, 0, 0)); + double s = BOX_SIZE * EXTRA_SCALE / scale; + handles = new Rectangle2D.Double[points.length]; + for (int i = 0; i < points.length; i++) { + Coordinate c = points[i]; + handles[i] = new Rectangle2D.Double(c.x * EXTRA_SCALE - s, c.y * EXTRA_SCALE - s, 2 * s, 2 * s); + g2.draw(handles[i]); + } + + } + + + + public int getIndexByPoint(double x, double y) { + if (handles == null) + return -1; + + // Calculate point in shapes' coordinates + Point2D.Double p = new Point2D.Double(x, y); + try { + transform.inverseTransform(p, p); + } catch (NoninvertibleTransformException e) { + return -1; + } + + for (int i = 0; i < handles.length; i++) { + if (handles[i].contains(p)) + return i; + } + return -1; + } + + + public int getSegmentByPoint(double x, double y) { + if (handles == null) + return -1; + + // Calculate point in shapes' coordinates + Point2D.Double p = new Point2D.Double(x, y); + try { + transform.inverseTransform(p, p); + } catch (NoninvertibleTransformException e) { + return -1; + } + + double x0 = p.x / EXTRA_SCALE; + double y0 = p.y / EXTRA_SCALE; + double delta = BOX_SIZE / scale; + + System.out.println("Point: " + x0 + "," + y0); + System.out.println("delta: " + (BOX_SIZE / scale)); + + Coordinate[] points = finset.getFinPoints(); + for (int i = 1; i < points.length; i++) { + double x1 = points[i - 1].x; + double y1 = points[i - 1].y; + double x2 = points[i].x; + double y2 = points[i].y; + + // System.out.println("point1:"+x1+","+y1+" point2:"+x2+","+y2); + + double u = Math.abs((x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1)) / + MathUtil.hypot(x2 - x1, y2 - y1); + System.out.println("Distance of segment " + i + " is " + u); + if (u < delta) + return i; + } + + return -1; + } + + + public Point2D.Double convertPoint(double x, double y) { + Point2D.Double p = new Point2D.Double(x, y); + try { + transform.inverseTransform(p, p); + } catch (NoninvertibleTransformException e) { + assert (false) : "Should not occur"; + return new Point2D.Double(0, 0); + } + + p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE); + return p; + } + + + + @Override + public Dimension getOrigin() { + if (modID != finset.getRocket().getAerodynamicModID()) { + modID = finset.getRocket().getAerodynamicModID(); + calculateDimensions(); + } + return new Dimension((int) translateX, (int) translateY); + } + + @Override + public double getFigureWidth() { + if (modID != finset.getRocket().getAerodynamicModID()) { + modID = finset.getRocket().getAerodynamicModID(); + calculateDimensions(); + } + return figureWidth; + } + + @Override + public double getFigureHeight() { + if (modID != finset.getRocket().getAerodynamicModID()) { + modID = finset.getRocket().getAerodynamicModID(); + calculateDimensions(); + } + return figureHeight; + } + + + private void calculateDimensions() { + minX = 0; + maxX = 0; + maxY = 0; + + for (Coordinate c : finset.getFinPoints()) { + if (c.x < minX) + minX = c.x; + if (c.x > maxX) + maxX = c.x; + if (c.y > maxY) + maxY = c.y; + } + + if (maxX < 0.01) + maxX = 0.01; + + figureWidth = maxX - minX; + figureHeight = maxY; + + + Dimension d = new Dimension((int) (figureWidth * scale + 2 * borderPixelsWidth), + (int) (figureHeight * scale + 2 * borderPixelsHeight)); + + if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { + setPreferredSize(d); + setMinimumSize(d); + revalidate(); + } + } + + + + @Override + public void updateFigure() { + repaint(); + } + + + +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java b/core/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java new file mode 100644 index 00000000..854eb17a --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java @@ -0,0 +1,561 @@ +package net.sf.openrocket.gui.scalefigure; + + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; + +import net.sf.openrocket.gui.figureelements.FigureElement; +import net.sf.openrocket.gui.util.ColorConversion; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.LineStyle; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.Transformation; + +/** + * A ScaleFigure that draws a complete rocket. Extra information can + * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)}, + * {@link #clearRelativeExtra()}. + * + * @author Sampo Niskanen + */ +public class RocketFigure extends AbstractScaleFigure { + private static final long serialVersionUID = 1L; + + private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure"; + private static final String ROCKET_FIGURE_SUFFIX = "Shapes"; + + public static final int TYPE_SIDE = 1; + public static final int TYPE_BACK = 2; + + // Width for drawing normal and selected components + public static final double NORMAL_WIDTH = 1.0; + public static final double SELECTED_WIDTH = 2.0; + + + private Configuration configuration; + private RocketComponent[] selection = new RocketComponent[0]; + + private int type = TYPE_SIDE; + + private double rotation; + private Transformation transformation; + + private double translateX, translateY; + + + + /* + * figureComponents contains the corresponding RocketComponents of the figureShapes + */ + private final ArrayList figureShapes = new ArrayList(); + private final ArrayList figureComponents = + new ArrayList(); + + private double minX = 0, maxX = 0, maxR = 0; + // Figure width and height in SI-units and pixels + private double figureWidth = 0, figureHeight = 0; + private int figureWidthPx = 0, figureHeightPx = 0; + + private AffineTransform g2transformation = null; + + private final ArrayList relativeExtra = new ArrayList(); + private final ArrayList absoluteExtra = new ArrayList(); + + + /** + * Creates a new rocket figure. + */ + public RocketFigure(Configuration configuration) { + super(); + + this.configuration = configuration; + + this.rotation = 0.0; + this.transformation = Transformation.rotate_x(0.0); + + updateFigure(); + } + + + /** + * Set the configuration displayed by the figure. It may use the same or different rocket. + * + * @param configuration the configuration to display. + */ + public void setConfiguration(Configuration configuration) { + this.configuration = configuration; + updateFigure(); + } + + + @Override + public Dimension getOrigin() { + return new Dimension((int) translateX, (int) translateY); + } + + @Override + public double getFigureHeight() { + return figureHeight; + } + + @Override + public double getFigureWidth() { + return figureWidth; + } + + + public RocketComponent[] getSelection() { + return selection; + } + + public void setSelection(RocketComponent[] selection) { + if (selection == null) { + this.selection = new RocketComponent[0]; + } else { + this.selection = selection; + } + updateFigure(); + } + + + public double getRotation() { + return rotation; + } + + public Transformation getRotateTransformation() { + return transformation; + } + + public void setRotation(double rot) { + if (MathUtil.equals(rotation, rot)) + return; + this.rotation = rot; + this.transformation = Transformation.rotate_x(rotation); + updateFigure(); + } + + + public int getType() { + return type; + } + + public void setType(int type) { + if (type != TYPE_BACK && type != TYPE_SIDE) { + throw new IllegalArgumentException("Illegal type: " + type); + } + if (this.type == type) + return; + this.type = type; + updateFigure(); + } + + + + + + /** + * Updates the figure shapes and figure size. + */ + @Override + public void updateFigure() { + figureShapes.clear(); + figureComponents.clear(); + + calculateSize(); + + // Get shapes for all active components + for (RocketComponent c : configuration) { + Shape[] s = getShapes(c); + for (int i = 0; i < s.length; i++) { + figureShapes.add(s[i]); + figureComponents.add(c); + } + } + + repaint(); + fireChangeEvent(); + } + + + public void addRelativeExtra(FigureElement p) { + relativeExtra.add(p); + } + + public void removeRelativeExtra(FigureElement p) { + relativeExtra.remove(p); + } + + public void clearRelativeExtra() { + relativeExtra.clear(); + } + + + public void addAbsoluteExtra(FigureElement p) { + absoluteExtra.add(p); + } + + public void removeAbsoluteExtra(FigureElement p) { + absoluteExtra.remove(p); + } + + public void clearAbsoluteExtra() { + absoluteExtra.clear(); + } + + + /** + * Paints the rocket on to the Graphics element. + *

+ * Warning: If paintComponent is used outside the normal Swing usage, some Swing + * dependent parameters may be left wrong (mainly transformation). If it is used, + * the RocketFigure should be repainted immediately afterwards. + */ + @Override + public void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + + + AffineTransform baseTransform = g2.getTransform(); + + // Update figure shapes if necessary + if (figureShapes == null) + updateFigure(); + + + double tx, ty; + // Calculate translation for figure centering + if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) { + + // Figure fits in the viewport + if (type == TYPE_BACK) + tx = getWidth() / 2; + else + tx = (getWidth() - figureWidthPx) / 2 - minX * scale; + + } else { + + // Figure does not fit in viewport + if (type == TYPE_BACK) + tx = borderPixelsWidth + figureWidthPx / 2; + else + tx = borderPixelsWidth - minX * scale; + + } + + ty = computeTy(figureHeightPx); + + if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { + // Origin has changed, fire event + translateX = tx; + translateY = ty; + fireChangeEvent(); + } + + + // Calculate and store the transformation used + // (inverse is used in detecting clicks on objects) + g2transformation = new AffineTransform(); + g2transformation.translate(translateX, translateY); + // Mirror position Y-axis upwards + g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); + + g2.transform(g2transformation); + + // Set rendering hints appropriately + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + + + // Draw all shapes + + for (int i = 0; i < figureShapes.size(); i++) { + RocketComponent c = figureComponents.get(i); + Shape s = figureShapes.get(i); + boolean selected = false; + + // Check if component is in the selection + for (int j = 0; j < selection.length; j++) { + if (c == selection[j]) { + selected = true; + break; + } + } + + // Set component color and line style + net.sf.openrocket.util.Color color = c.getColor(); + if (color == null) { + color = Application.getPreferences().getDefaultColor(c.getClass()); + } + g2.setColor(ColorConversion.toAwtColor(color)); + + LineStyle style = c.getLineStyle(); + if (style == null) + style = Application.getPreferences().getDefaultLineStyle(c.getClass()); + + float[] dashes = style.getDashes(); + for (int j = 0; j < dashes.length; j++) { + dashes[j] *= EXTRA_SCALE / scale; + } + + if (selected) { + g2.setStroke(new BasicStroke((float) (SELECTED_WIDTH * EXTRA_SCALE / scale), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0)); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_PURE); + } else { + g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0)); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + } + g2.draw(s); + + } + + g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale), + BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + + + // Draw motors + String motorID = configuration.getMotorConfigurationID(); + Color fillColor = ((SwingPreferences)Application.getPreferences()).getMotorFillColor(); + Color borderColor = ((SwingPreferences)Application.getPreferences()).getMotorBorderColor(); + Iterator iterator = configuration.motorIterator(); + while (iterator.hasNext()) { + MotorMount mount = iterator.next(); + Motor motor = mount.getMotor(motorID); + double length = motor.getLength(); + double radius = motor.getDiameter() / 2; + + Coordinate[] position = ((RocketComponent) mount).toAbsolute( + new Coordinate(((RocketComponent) mount).getLength() + + mount.getMotorOverhang() - length)); + + for (int i = 0; i < position.length; i++) { + position[i] = transformation.transform(position[i]); + } + + for (Coordinate coord : position) { + Shape s; + if (type == TYPE_SIDE) { + s = new Rectangle2D.Double(EXTRA_SCALE * coord.x, + EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * length, + EXTRA_SCALE * 2 * radius); + } else { + s = new Ellipse2D.Double(EXTRA_SCALE * (coord.z - radius), + EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * 2 * radius, + EXTRA_SCALE * 2 * radius); + } + g2.setColor(fillColor); + g2.fill(s); + g2.setColor(borderColor); + g2.draw(s); + } + } + + + + // Draw relative extras + for (FigureElement e : relativeExtra) { + e.paint(g2, scale / EXTRA_SCALE); + } + + // Draw absolute extras + g2.setTransform(baseTransform); + Rectangle rect = this.getVisibleRect(); + + for (FigureElement e : absoluteExtra) { + e.paint(g2, 1.0, rect); + } + + } + + protected double computeTy(int heightPx) { + final double ty; + if (heightPx + 2 * borderPixelsHeight < getHeight()) { + ty = getHeight() / 2; + } else { + ty = borderPixelsHeight + heightPx / 2; + } + return ty; + } + + + public RocketComponent[] getComponentsByPoint(double x, double y) { + // Calculate point in shapes' coordinates + Point2D.Double p = new Point2D.Double(x, y); + try { + g2transformation.inverseTransform(p, p); + } catch (NoninvertibleTransformException e) { + return new RocketComponent[0]; + } + + LinkedHashSet l = new LinkedHashSet(); + + for (int i = 0; i < figureShapes.size(); i++) { + if (figureShapes.get(i).contains(p)) + l.add(figureComponents.get(i)); + } + return l.toArray(new RocketComponent[0]); + } + + + + /** + * Gets the shapes required to draw the component. + * + * @param component + * @param params + * @return + */ + private Shape[] getShapes(RocketComponent component) { + Reflection.Method m; + + // Find the appropriate method + switch (type) { + case TYPE_SIDE: + m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", + RocketComponent.class, Transformation.class); + break; + + case TYPE_BACK: + m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", + RocketComponent.class, Transformation.class); + break; + + default: + throw new BugException("Unknown figure type = " + type); + } + + if (m == null) { + Application.getExceptionHandler().handleErrorCondition("ERROR: Rocket figure paint method not found for " + + component); + return new Shape[0]; + } + + return (Shape[]) m.invokeStatic(component, transformation); + } + + + + /** + * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions. + * The bounds are stored in the variables minX, maxX and maxR. + */ + private void calculateFigureBounds() { + Collection bounds = configuration.getBounds(); + + if (bounds.isEmpty()) { + minX = 0; + maxX = 0; + maxR = 0; + return; + } + + minX = Double.MAX_VALUE; + maxX = Double.MIN_VALUE; + maxR = 0; + for (Coordinate c : bounds) { + double x = c.x, r = MathUtil.hypot(c.y, c.z); + if (x < minX) + minX = x; + if (x > maxX) + maxX = x; + if (r > maxR) + maxR = r; + } + } + + + public double getBestZoom(Rectangle2D bounds) { + double zh = 1, zv = 1; + if (bounds.getWidth() > 0.0001) + zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth(); + if (bounds.getHeight() > 0.0001) + zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight(); + return Math.min(zh, zv); + } + + + + /** + * Calculates the necessary size of the figure and set the PreferredSize + * property accordingly. + */ + private void calculateSize() { + calculateFigureBounds(); + + switch (type) { + case TYPE_SIDE: + figureWidth = maxX - minX; + figureHeight = 2 * maxR; + break; + + case TYPE_BACK: + figureWidth = 2 * maxR; + figureHeight = 2 * maxR; + break; + + default: + assert (false) : "Should not occur, type=" + type; + figureWidth = 0; + figureHeight = 0; + } + + figureWidthPx = (int) (figureWidth * scale); + figureHeightPx = (int) (figureHeight * scale); + + Dimension d = new Dimension(figureWidthPx + 2 * borderPixelsWidth, + figureHeightPx + 2 * borderPixelsHeight); + + if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { + setPreferredSize(d); + setMinimumSize(d); + revalidate(); + } + } + + public Rectangle2D getDimensions() { + switch (type) { + case TYPE_SIDE: + return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR); + + case TYPE_BACK: + return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR); + + default: + throw new BugException("Illegal figure type = " + type); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java new file mode 100644 index 00000000..cf458724 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -0,0 +1,742 @@ +package net.sf.openrocket.gui.scalefigure; + + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.InputEvent; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JToggleButton; +import javax.swing.JViewport; +import javax.swing.SwingUtilities; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; +import net.sf.openrocket.gui.components.BasicSlider; +import net.sf.openrocket.gui.components.StageSelector; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; +import net.sf.openrocket.gui.figureelements.CGCaret; +import net.sf.openrocket.gui.figureelements.CPCaret; +import net.sf.openrocket.gui.figureelements.Caret; +import net.sf.openrocket.gui.figureelements.RocketInfo; +import net.sf.openrocket.gui.main.SimulationWorker; +import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; +import net.sf.openrocket.simulation.listeners.system.InterruptListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.StateChangeListener; + +/** + * A JPanel that contains a RocketFigure and buttons to manipulate the figure. + * + * @author Sampo Niskanen + */ +public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource { + + private static final Translator trans = Application.getTranslator(); + private final RocketFigure figure; + private final ScaleScrollPane scrollPane; + + private JLabel infoMessage; + + private TreeSelectionModel selectionModel = null; + + + /* Calculation of CP and CG */ + private AerodynamicCalculator aerodynamicCalculator; + private MassCalculator massCalculator; + + + private final OpenRocketDocument document; + private final Configuration configuration; + + private Caret extraCP = null; + private Caret extraCG = null; + private RocketInfo extraText = null; + + + private double cpAOA = Double.NaN; + private double cpTheta = Double.NaN; + private double cpMach = Double.NaN; + private double cpRoll = Double.NaN; + + // The functional ID of the rocket that was simulated + private int flightDataFunctionalID = -1; + private String flightDataMotorID = null; + + + private SimulationWorker backgroundSimulationWorker = null; + + private List listeners = new ArrayList(); + + + /** + * The executor service used for running the background simulations. + * This uses a fixed-sized thread pool for all background simulations + * with all threads in daemon mode and with minimum priority. + */ + private static final Executor backgroundSimulationExecutor; + static { + backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(), + new ThreadFactory() { + private ThreadFactory factory = Executors.defaultThreadFactory(); + + @Override + public Thread newThread(Runnable r) { + Thread t = factory.newThread(r); + t.setDaemon(true); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + + public RocketPanel(OpenRocketDocument document) { + + this.document = document; + configuration = document.getDefaultConfiguration(); + + // TODO: FUTURE: calculator selection + aerodynamicCalculator = new BarrowmanCalculator(); + massCalculator = new BasicMassCalculator(); + + // Create figure and custom scroll pane + figure = new RocketFigure(configuration); + + scrollPane = new ScaleScrollPane(figure) { + @Override + public void mouseClicked(MouseEvent event) { + handleMouseClick(event); + } + }; + scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE); + scrollPane.setFitting(true); + + createPanel(); + + configuration.addChangeListener(new StateChangeListener() { + @Override + public void stateChanged(EventObject e) { + // System.out.println("Configuration changed, calling updateFigure"); + updateExtras(); + figure.updateFigure(); + } + }); + } + + + /** + * Creates the layout and components of the panel. + */ + private void createPanel() { + setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]")); + + setPreferredSize(new Dimension(800, 300)); + + + //// Create toolbar + + // Side/back buttons + FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE); + //// Side view + action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Sideview")); + //// Side view + action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Sideview")); + JToggleButton toggle = new JToggleButton(action); + add(toggle, "spanx, split"); + + action = new FigureTypeAction(RocketFigure.TYPE_BACK); + //// Back view + action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Backview")); + //// Back view + action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Backview")); + toggle = new JToggleButton(action); + add(toggle, "gap rel"); + + + // Zoom level selector + ScaleSelector scaleSelector = new ScaleSelector(scrollPane); + add(scaleSelector); + + + + // Stage selector + StageSelector stageSelector = new StageSelector(configuration); + add(stageSelector, ""); + + + + // Motor configuration selector + //// Motor configuration: + JLabel label = new JLabel(trans.get("RocketPanel.lbl.Motorcfg")); + label.setHorizontalAlignment(JLabel.RIGHT); + add(label, "growx, right"); + add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap"); + + + + + + // Create slider and scroll pane + + DoubleModel theta = new DoubleModel(figure, "Rotation", + UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI); + UnitSelector us = new UnitSelector(theta, true); + us.setHorizontalAlignment(JLabel.CENTER); + add(us, "alignx 50%, growx"); + + // Add the rocket figure + add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap"); + + + // Add rotation slider + // Minimum size to fit "360deg" + JLabel l = new JLabel("360" + Chars.DEGREE); + Dimension d = l.getPreferredSize(); + + add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true), + "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy"); + + + //// Click to select    Shift+click to select other    Double-click to edit    Click+drag to move + infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage")); + infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9)); + add(infoMessage, "skip, span, gapleft 25, wrap"); + + + addExtras(); + } + + + + public RocketFigure getFigure() { + return figure; + } + + public AerodynamicCalculator getAerodynamicCalculator() { + return aerodynamicCalculator; + } + + public Configuration getConfiguration() { + return configuration; + } + + /** + * Get the center of pressure figure element. + * + * @return center of pressure info + */ + public Caret getExtraCP() { + return extraCP; + } + + /** + * Get the center of gravity figure element. + * + * @return center of gravity info + */ + public Caret getExtraCG() { + return extraCG; + } + + /** + * Get the extra text figure element. + * + * @return extra text that contains info about the rocket design + */ + public RocketInfo getExtraText() { + return extraText; + } + + public void setSelectionModel(TreeSelectionModel m) { + if (selectionModel != null) { + selectionModel.removeTreeSelectionListener(this); + } + selectionModel = m; + selectionModel.addTreeSelectionListener(this); + valueChanged((TreeSelectionEvent) null); // updates FigureParameters + } + + + + /** + * Return the angle of attack used in CP calculation. NaN signifies the default value + * of zero. + * @return the angle of attack used, or NaN. + */ + public double getCPAOA() { + return cpAOA; + } + + /** + * Set the angle of attack to be used in CP calculation. A value of NaN signifies that + * the default AOA (zero) should be used. + * @param aoa the angle of attack to use, or NaN + */ + public void setCPAOA(double aoa) { + if (MathUtil.equals(aoa, cpAOA) || + (Double.isNaN(aoa) && Double.isNaN(cpAOA))) + return; + cpAOA = aoa; + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + public double getCPTheta() { + return cpTheta; + } + + public void setCPTheta(double theta) { + if (MathUtil.equals(theta, cpTheta) || + (Double.isNaN(theta) && Double.isNaN(cpTheta))) + return; + cpTheta = theta; + if (!Double.isNaN(theta)) + figure.setRotation(theta); + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + public double getCPMach() { + return cpMach; + } + + public void setCPMach(double mach) { + if (MathUtil.equals(mach, cpMach) || + (Double.isNaN(mach) && Double.isNaN(cpMach))) + return; + cpMach = mach; + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + public double getCPRoll() { + return cpRoll; + } + + public void setCPRoll(double roll) { + if (MathUtil.equals(roll, cpRoll) || + (Double.isNaN(roll) && Double.isNaN(cpRoll))) + return; + cpRoll = roll; + updateExtras(); + figure.updateFigure(); + fireChangeEvent(); + } + + + + @Override + public void addChangeListener(EventListener listener) { + listeners.add(0, listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listeners.remove(listener); + } + + protected void fireChangeEvent() { + EventObject e = new EventObject(this); + for (EventListener l : listeners) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(e); + } + } + } + + + + + /** + * Handle clicking on figure shapes. The functioning is the following: + * + * Get the components clicked. + * If no component is clicked, do nothing. + * If the currently selected component is in the set, keep it, + * unless the selector specified is pressed. If it is pressed, cycle to + * the next component. Otherwise select the first component in the list. + */ + public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK; + + private void handleMouseClick(MouseEvent event) { + if (event.getButton() != MouseEvent.BUTTON1) + return; + Point p0 = event.getPoint(); + Point p1 = scrollPane.getViewport().getViewPosition(); + int x = p0.x + p1.x; + int y = p0.y + p1.y; + + RocketComponent[] clicked = figure.getComponentsByPoint(x, y); + + // If no component is clicked, do nothing + if (clicked.length == 0) + return; + + // Check whether the currently selected component is in the clicked components. + TreePath path = selectionModel.getSelectionPath(); + if (path != null) { + RocketComponent current = (RocketComponent) path.getLastPathComponent(); + path = null; + for (int i = 0; i < clicked.length; i++) { + if (clicked[i] == current) { + if (event.isShiftDown() && (event.getClickCount() == 1)) { + path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]); + } else { + path = ComponentTreeModel.makeTreePath(clicked[i]); + } + break; + } + } + } + + // Currently selected component not clicked + if (path == null) { + if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) { + path = ComponentTreeModel.makeTreePath(clicked[1]); + } else { + path = ComponentTreeModel.makeTreePath(clicked[0]); + } + } + + // Set selection and check for double-click + selectionModel.setSelectionPath(path); + if (event.getClickCount() == 2) { + RocketComponent component = (RocketComponent) path.getLastPathComponent(); + + ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this), + document, component); + } + } + + + + + /** + * Updates the extra data included in the figure. Currently this includes + * the CP and CG carets. + */ + private WarningSet warnings = new WarningSet(); + + private void updateExtras() { + Coordinate cp, cg; + double cpx, cgx; + + // TODO: MEDIUM: User-definable conditions + FlightConditions conditions = new FlightConditions(configuration); + warnings.clear(); + + if (!Double.isNaN(cpMach)) { + conditions.setMach(cpMach); + extraText.setMach(cpMach); + } else { + conditions.setMach(Application.getPreferences().getDefaultMach()); + extraText.setMach(Application.getPreferences().getDefaultMach()); + } + + if (!Double.isNaN(cpAOA)) { + conditions.setAOA(cpAOA); + } else { + conditions.setAOA(0); + } + extraText.setAOA(cpAOA); + + if (!Double.isNaN(cpRoll)) { + conditions.setRollRate(cpRoll); + } else { + conditions.setRollRate(0); + } + + if (!Double.isNaN(cpTheta)) { + conditions.setTheta(cpTheta); + cp = aerodynamicCalculator.getCP(configuration, conditions, warnings); + } else { + cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings); + } + extraText.setTheta(cpTheta); + + + cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); + // System.out.println("CG computed as "+cg+ " CP as "+cp); + + if (cp.weight > 0.000001) + cpx = cp.x; + else + cpx = Double.NaN; + + if (cg.weight > 0.000001) + cgx = cg.x; + else + cgx = Double.NaN; + + // Length bound is assumed to be tight + double length = 0, diameter = 0; + Collection bounds = configuration.getBounds(); + if (!bounds.isEmpty()) { + double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; + for (Coordinate c : bounds) { + if (c.x < minX) + minX = c.x; + if (c.x > maxX) + maxX = c.x; + } + length = maxX - minX; + } + + for (RocketComponent c : configuration) { + if (c instanceof SymmetricComponent) { + double d1 = ((SymmetricComponent) c).getForeRadius() * 2; + double d2 = ((SymmetricComponent) c).getAftRadius() * 2; + diameter = MathUtil.max(diameter, d1, d2); + } + } + + extraText.setCG(cgx); + extraText.setCP(cpx); + extraText.setLength(length); + extraText.setDiameter(diameter); + extraText.setMass(cg.weight); + extraText.setWarnings(warnings); + + + if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) { + + // TODO: LOW: Y-coordinate and rotation + extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0); + extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0); + + } else { + + extraCP.setPosition(Double.NaN, Double.NaN); + extraCG.setPosition(Double.NaN, Double.NaN); + + } + + + //////// Flight simulation in background + + // Check whether to compute or not + if (!((SwingPreferences) Application.getPreferences()).computeFlightInBackground()) { + extraText.setFlightData(null); + extraText.setCalculatingData(false); + stopBackgroundSimulation(); + return; + } + + // Check whether data is already up to date + if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() && + flightDataMotorID == configuration.getMotorConfigurationID()) { + return; + } + + flightDataFunctionalID = configuration.getRocket().getFunctionalModID(); + flightDataMotorID = configuration.getMotorConfigurationID(); + + // Stop previous computation (if any) + stopBackgroundSimulation(); + + // Check that configuration has motors + if (!configuration.hasMotors()) { + extraText.setFlightData(FlightData.NaN_DATA); + extraText.setCalculatingData(false); + return; + } + + // Start calculation process + extraText.setCalculatingData(true); + + Rocket duplicate = (Rocket) configuration.getRocket().copy(); + Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate); + simulation.getOptions().setMotorConfigurationID( + configuration.getMotorConfigurationID()); + + backgroundSimulationWorker = new BackgroundSimulationWorker(simulation); + backgroundSimulationExecutor.execute(backgroundSimulationWorker); + } + + /** + * Cancels the current background simulation worker, if any. + */ + private void stopBackgroundSimulation() { + if (backgroundSimulationWorker != null) { + backgroundSimulationWorker.cancel(true); + backgroundSimulationWorker = null; + } + } + + + /** + * A SimulationWorker that simulates the rocket flight in the background and + * sets the results to the extra text when finished. The worker can be cancelled + * if necessary. + */ + private class BackgroundSimulationWorker extends SimulationWorker { + + public BackgroundSimulationWorker(Simulation sim) { + super(sim); + } + + @Override + protected FlightData doInBackground() { + + // Pause a little while to allow faster UI reaction + try { + Thread.sleep(300); + } catch (InterruptedException ignore) { + } + if (isCancelled() || backgroundSimulationWorker != this) + return null; + + return super.doInBackground(); + } + + @Override + protected void simulationDone() { + // Do nothing if cancelled + if (isCancelled() || backgroundSimulationWorker != this) + return; + + backgroundSimulationWorker = null; + extraText.setFlightData(simulation.getSimulatedData()); + extraText.setCalculatingData(false); + figure.repaint(); + } + + @Override + protected SimulationListener[] getExtraListeners() { + return new SimulationListener[] { + InterruptListener.INSTANCE, + ApogeeEndListener.INSTANCE }; + } + + @Override + protected void simulationInterrupted(Throwable t) { + // Do nothing on cancel, set N/A data otherwise + if (isCancelled() || backgroundSimulationWorker != this) // Double-check + return; + + backgroundSimulationWorker = null; + extraText.setFlightData(FlightData.NaN_DATA); + extraText.setCalculatingData(false); + figure.repaint(); + } + } + + + + /** + * Adds the extra data to the figure. Currently this includes the CP and CG carets. + */ + private void addExtras() { + figure.clearRelativeExtra(); + extraCG = new CGCaret(0, 0); + extraCP = new CPCaret(0, 0); + extraText = new RocketInfo(configuration); + updateExtras(); + figure.addRelativeExtra(extraCP); + figure.addRelativeExtra(extraCG); + figure.addAbsoluteExtra(extraText); + } + + + /** + * Updates the selection in the FigureParameters and repaints the figure. + * Ignores the event itself. + */ + @Override + public void valueChanged(TreeSelectionEvent e) { + TreePath[] paths = selectionModel.getSelectionPaths(); + if (paths == null) { + figure.setSelection(null); + return; + } + + RocketComponent[] components = new RocketComponent[paths.length]; + for (int i = 0; i < paths.length; i++) + components[i] = (RocketComponent) paths[i].getLastPathComponent(); + figure.setSelection(components); + } + + + + /** + * An Action that shows whether the figure type is the type + * given in the constructor. + * + * @author Sampo Niskanen + */ + private class FigureTypeAction extends AbstractAction implements StateChangeListener { + private final int type; + + public FigureTypeAction(int type) { + this.type = type; + stateChanged(null); + figure.addChangeListener(this); + } + + @Override + public void actionPerformed(ActionEvent e) { + boolean state = (Boolean) getValue(Action.SELECTED_KEY); + if (state == true) { + // This view has been selected + figure.setType(type); + updateExtras(); + } + stateChanged(null); + } + + @Override + public void stateChanged(EventObject e) { + putValue(Action.SELECTED_KEY, figure.getType() == type); + } + } + +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java b/core/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java new file mode 100644 index 00000000..48440fe3 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java @@ -0,0 +1,82 @@ +package net.sf.openrocket.gui.scalefigure; + +import java.awt.Dimension; + +import net.sf.openrocket.util.ChangeSource; + + +public interface ScaleFigure extends ChangeSource { + + /** + * Extra scaling applied to the figure. The f***ing Java JRE doesn't know + * how to draw shapes when using very large scaling factors, so this must + * be manually applied to every single shape used. + *

+ * The scaling factor used is divided by this value, and every coordinate used + * in the figures must be multiplied by this factor. + */ + public static final double EXTRA_SCALE = 1000; + + /** + * Shorthand for {@link #EXTRA_SCALE}. + */ + public static final double S = EXTRA_SCALE; + + + /** + * Set the scale level of the figure. A scale value of 1.0 indicates an original + * size when using the current DPI level. + * + * @param scale the scale level. + */ + public void setScaling(double scale); + + + /** + * Set the scale level so that the figure fits into the given bounds. + * + * @param bounds the bounds of the figure. + */ + public void setScaling(Dimension bounds); + + + /** + * Return the scale level of the figure. A scale value of 1.0 indicates an original + * size when using the current DPI level. + * + * @return the current scale level. + */ + public double getScaling(); + + + /** + * Return the scale of the figure on px/m. + * + * @return the current scale value. + */ + public double getAbsoluteScale(); + + + /** + * Return the pixel coordinates of the figure origin. + * + * @return the pixel coordinates of the figure origin. + */ + public Dimension getOrigin(); + + + /** + * Get the amount of blank space left around the figure. + * + * @return the amount of horizontal and vertical space left on both sides of the figure. + */ + public Dimension getBorderPixels(); + + /** + * Set the amount of blank space left around the figure. + * + * @param width the amount of horizontal space left on both sides of the figure. + * @param height the amount of vertical space left on both sides of the figure. + */ + public void setBorderPixels(int width, int height); +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java b/core/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java new file mode 100644 index 00000000..121dc0e0 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java @@ -0,0 +1,397 @@ +package net.sf.openrocket.gui.scalefigure; + + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.MouseMotionListener; +import java.util.EventObject; + +import javax.swing.BorderFactory; +import javax.swing.JComponent; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JViewport; +import javax.swing.ScrollPaneConstants; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import net.sf.openrocket.gui.adaptors.DoubleModel; +import net.sf.openrocket.gui.components.UnitSelector; +import net.sf.openrocket.unit.Tick; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.StateChangeListener; + + + +/** + * A scroll pane that holds a {@link ScaleFigure} and includes rulers that show + * natural units. The figure can be moved by dragging on the figure. + *

+ * This class implements both MouseListener and + * MouseMotionListener. If subclasses require extra functionality + * (e.g. checking for clicks) then these methods may be overridden, and only unhandled + * events passed to this class. + * + * @author Sampo Niskanen + */ +public class ScaleScrollPane extends JScrollPane + implements MouseListener, MouseMotionListener { + + public static final int RULER_SIZE = 20; + public static final int MINOR_TICKS = 3; + public static final int MAJOR_TICKS = 30; + + + private JComponent component; + private ScaleFigure figure; + private JViewport viewport; + + private DoubleModel rulerUnit; + private Ruler horizontalRuler; + private Ruler verticalRuler; + + private final boolean allowFit; + + private boolean fit = false; + + + /** + * Create a scale scroll pane that allows fitting. + * + * @param component the component to contain (must implement ScaleFigure) + */ + public ScaleScrollPane(JComponent component) { + this(component, true); + } + + /** + * Create a scale scroll pane. + * + * @param component the component to contain (must implement ScaleFigure) + * @param allowFit whether automatic fitting of the figure is allowed + */ + public ScaleScrollPane(JComponent component, boolean allowFit) { + super(component); + + if (!(component instanceof ScaleFigure)) { + throw new IllegalArgumentException("component must implement ScaleFigure"); + } + + this.component = component; + this.figure = (ScaleFigure) component; + this.allowFit = allowFit; + + + rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH); + rulerUnit.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + ScaleScrollPane.this.component.repaint(); + } + }); + horizontalRuler = new Ruler(Ruler.HORIZONTAL); + verticalRuler = new Ruler(Ruler.VERTICAL); + this.setColumnHeaderView(horizontalRuler); + this.setRowHeaderView(verticalRuler); + + UnitSelector selector = new UnitSelector(rulerUnit); + selector.setFont(new Font("SansSerif", Font.PLAIN, 8)); + this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector); + this.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new JPanel()); + this.setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel()); + this.setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel()); + + this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY)); + + + viewport = this.getViewport(); + viewport.addMouseListener(this); + viewport.addMouseMotionListener(this); + + figure.addChangeListener(new StateChangeListener() { + @Override + public void stateChanged(EventObject e) { + horizontalRuler.updateSize(); + verticalRuler.updateSize(); + if (fit) { + setFitting(true); + } + } + }); + + viewport.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + if (fit) { + setFitting(true); + } + } + }); + + } + + public ScaleFigure getFigure() { + return figure; + } + + + /** + * Return whether automatic fitting of the figure is allowed. + */ + public boolean isFittingAllowed() { + return allowFit; + } + + /** + * Return whether the figure is currently automatically fitted within the component bounds. + */ + public boolean isFitting() { + return fit; + } + + /** + * Set whether the figure is automatically fitted within the component bounds. + * + * @throws BugException if automatic fitting is disallowed and fit is true + */ + public void setFitting(boolean fit) { + if (fit && !allowFit) { + throw new BugException("Attempting to fit figure not allowing fit."); + } + this.fit = fit; + if (fit) { + setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); + validate(); + Dimension view = viewport.getExtentSize(); + figure.setScaling(view); + } else { + setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); + } + } + + + + public double getScaling() { + return figure.getScaling(); + } + + public double getScale() { + return figure.getAbsoluteScale(); + } + + public void setScaling(double scale) { + if (fit) { + setFitting(false); + } + figure.setScaling(scale); + horizontalRuler.repaint(); + verticalRuler.repaint(); + } + + + public Unit getCurrentUnit() { + return rulerUnit.getCurrentUnit(); + } + + + //////////////// Mouse handlers //////////////// + + + private int dragStartX = 0; + private int dragStartY = 0; + private Rectangle dragRectangle = null; + + @Override + public void mousePressed(MouseEvent e) { + dragStartX = e.getX(); + dragStartY = e.getY(); + dragRectangle = viewport.getViewRect(); + } + + @Override + public void mouseReleased(MouseEvent e) { + dragRectangle = null; + } + + @Override + public void mouseDragged(MouseEvent e) { + if (dragRectangle == null) { + return; + } + + dragRectangle.setLocation(dragStartX - e.getX(), dragStartY - e.getY()); + + dragStartX = e.getX(); + dragStartY = e.getY(); + + viewport.scrollRectToVisible(dragRectangle); + } + + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mouseMoved(MouseEvent e) { + } + + + + //////////////// The view port rulers //////////////// + + + private class Ruler extends JComponent { + public static final int HORIZONTAL = 0; + public static final int VERTICAL = 1; + + private final int orientation; + + public Ruler(int orientation) { + this.orientation = orientation; + updateSize(); + + rulerUnit.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + Ruler.this.repaint(); + } + }); + } + + + public void updateSize() { + Dimension d = component.getPreferredSize(); + if (orientation == HORIZONTAL) { + setPreferredSize(new Dimension(d.width + 10, RULER_SIZE)); + } else { + setPreferredSize(new Dimension(RULER_SIZE, d.height + 10)); + } + revalidate(); + repaint(); + } + + private double fromPx(int px) { + Dimension origin = figure.getOrigin(); + if (orientation == HORIZONTAL) { + px -= origin.width; + } else { + // px = -(px - origin.height); + px -= origin.height; + } + return px / figure.getAbsoluteScale(); + } + + private int toPx(double l) { + Dimension origin = figure.getOrigin(); + int px = (int) (l * figure.getAbsoluteScale() + 0.5); + if (orientation == HORIZONTAL) { + px += origin.width; + } else { + px = px + origin.height; + // px += origin.height; + } + return px; + } + + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; + + Rectangle area = g2.getClipBounds(); + + // Fill area with background color + g2.setColor(getBackground()); + g2.fillRect(area.x, area.y, area.width, area.height + 100); + + + int startpx, endpx; + if (orientation == HORIZONTAL) { + startpx = area.x; + endpx = area.x + area.width; + } else { + startpx = area.y; + endpx = area.y + area.height; + } + + Unit unit = rulerUnit.getCurrentUnit(); + double start, end, minor, major; + start = fromPx(startpx); + end = fromPx(endpx); + minor = MINOR_TICKS / figure.getAbsoluteScale(); + major = MAJOR_TICKS / figure.getAbsoluteScale(); + + Tick[] ticks = unit.getTicks(start, end, minor, major); + + + // Set color & hints + g2.setColor(Color.BLACK); + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + RenderingHints.VALUE_STROKE_NORMALIZE); + g2.setRenderingHint(RenderingHints.KEY_RENDERING, + RenderingHints.VALUE_RENDER_QUALITY); + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, + RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + for (Tick t : ticks) { + int position = toPx(t.value); + drawTick(g2, position, t); + } + } + + private void drawTick(Graphics g, int position, Tick t) { + int length; + String str = null; + if (t.major) { + length = RULER_SIZE / 2; + } else { + if (t.notable) + length = RULER_SIZE / 3; + else + length = RULER_SIZE / 6; + } + + // Set font + if (t.major) { + str = rulerUnit.getCurrentUnit().toString(t.value); + if (t.notable) + g.setFont(new Font("SansSerif", Font.BOLD, 9)); + else + g.setFont(new Font("SansSerif", Font.PLAIN, 9)); + } + + // Draw tick & text + if (orientation == HORIZONTAL) { + g.drawLine(position, RULER_SIZE - length, position, RULER_SIZE); + if (str != null) + g.drawString(str, position, RULER_SIZE - length - 1); + } else { + g.drawLine(RULER_SIZE - length, position, RULER_SIZE, position); + if (str != null) + g.drawString(str, 1, position - 1); + } + } + } +} diff --git a/core/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java b/core/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java new file mode 100644 index 00000000..1e966a05 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java @@ -0,0 +1,155 @@ +package net.sf.openrocket.gui.scalefigure; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.DecimalFormat; +import java.util.Arrays; +import java.util.EventObject; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JPanel; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.gui.util.Icons; +import net.sf.openrocket.util.StateChangeListener; + +public class ScaleSelector extends JPanel { + + // Ready zoom settings + private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%"); + + private static final double[] ZOOM_LEVELS = { 0.15, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0 }; + private static final String ZOOM_FIT = "Fit"; + private static final String[] ZOOM_SETTINGS; + static { + ZOOM_SETTINGS = new String[ZOOM_LEVELS.length+1]; + for (int i=0; i ZOOM_LEVELS[i]+0.05 && scale < ZOOM_LEVELS[i+1]+0.05) + return ZOOM_LEVELS[i]; + } + if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) { + // scale is large, drop to next lowest full 100% + scale = Math.ceil(scale-1.05); + return Math.max(scale, ZOOM_LEVELS[i]); + } + // scale is small + return scale/1.5; + } + + + private double getNextScale(double scale) { + int i; + for (i=0; i ZOOM_LEVELS[i]-0.05 && scale < ZOOM_LEVELS[i+1]-0.05) + return ZOOM_LEVELS[i+1]; + } + if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) { + // scale is large, give next full 100% + scale = Math.floor(scale+1.05); + return scale; + } + return scale*1.5; + } + +} diff --git a/core/src/net/sf/openrocket/gui/util/ColorConversion.java b/core/src/net/sf/openrocket/gui/util/ColorConversion.java new file mode 100644 index 00000000..e9a2eae4 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/ColorConversion.java @@ -0,0 +1,18 @@ +package net.sf.openrocket.gui.util; + +public class ColorConversion { + + public static java.awt.Color toAwtColor( net.sf.openrocket.util.Color c ) { + if ( c == null ) { + return null; + } + return new java.awt.Color(c.getRed(),c.getGreen(),c.getBlue(),c.getAlpha()); + } + + public static net.sf.openrocket.util.Color fromAwtColor( java.awt.Color c ) { + if ( c == null ) { + return null; + } + return new net.sf.openrocket.util.Color( c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha()); + } +} diff --git a/core/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitor.java b/core/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitor.java new file mode 100644 index 00000000..b6aa6ea3 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitor.java @@ -0,0 +1,61 @@ +package net.sf.openrocket.gui.util; + +import java.awt.Component; + +import javax.swing.ProgressMonitor; +import javax.swing.SwingUtilities; + + +/** + * A thread-safe ProgressMonitor. This class may be instantiated + * and the method {@link #setProgress(int)} called safely from any thread. + *

+ * Why the FSCK&!#&% isn't the default API version thread-safe?!?! + * + * @author Sampo Niskanen + */ +public class ConcurrentProgressMonitor extends ProgressMonitor { + + public ConcurrentProgressMonitor(Component parentComponent, Object message, + String note, int min, int max) { + super(parentComponent, message, note, min, max); + } + + @Override + public void setProgress(final int nv) { + + if (SwingUtilities.isEventDispatchThread()) { + super.setProgress(nv); + } else { + + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + ConcurrentProgressMonitor.super.setProgress(nv); + } + + }); + } + } + + + @Override + public void close() { + if (SwingUtilities.isEventDispatchThread()) { + super.close(); + } else { + + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + ConcurrentProgressMonitor.super.close(); + } + + }); + } + } + + +} diff --git a/core/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitorInputStream.java b/core/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitorInputStream.java new file mode 100644 index 00000000..ccc0d799 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitorInputStream.java @@ -0,0 +1,144 @@ +package net.sf.openrocket.gui.util; + +import java.awt.Component; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; + + + +/** + * A functional equivalent of ProgressMonitorInputStream which + * uses {@link ConcurrentProgressMonitor} and leaves the progress dialog open + * to be manually closed later on. + */ + +public class ConcurrentProgressMonitorInputStream extends FilterInputStream { + private ConcurrentProgressMonitor monitor; + private int nread = 0; + private int size = 0; + + + /** + * Constructs an object to monitor the progress of an input stream. + * + * @param message Descriptive text to be placed in the dialog box + * if one is popped up. + * @param parentComponent The component triggering the operation + * being monitored. + * @param in The input stream to be monitored. + */ + public ConcurrentProgressMonitorInputStream(Component parentComponent, + Object message, InputStream in) { + super(in); + try { + size = in.available(); + } catch (IOException ioe) { + size = 0; + } + monitor = new ConcurrentProgressMonitor(parentComponent, message, null, 0, + size + 1); + } + + + /** + * Get the ProgressMonitor object being used by this stream. Normally + * this isn't needed unless you want to do something like change the + * descriptive text partway through reading the file. + * @return the ProgressMonitor object used by this object + */ + public ConcurrentProgressMonitor getProgressMonitor() { + return monitor; + } + + + /** + * Overrides FilterInputStream.read + * to update the progress monitor after the read. + */ + @Override + public int read() throws IOException { + int c = in.read(); + if (c >= 0) + monitor.setProgress(++nread); + if (monitor.isCanceled()) { + InterruptedIOException exc = new InterruptedIOException("progress"); + exc.bytesTransferred = nread; + throw exc; + } + return c; + } + + + /** + * Overrides FilterInputStream.read + * to update the progress monitor after the read. + */ + @Override + public int read(byte b[]) throws IOException { + int nr = in.read(b); + if (nr > 0) + monitor.setProgress(nread += nr); + if (monitor.isCanceled()) { + InterruptedIOException exc = new InterruptedIOException("progress"); + exc.bytesTransferred = nread; + throw exc; + } + return nr; + } + + + /** + * Overrides FilterInputStream.read + * to update the progress monitor after the read. + */ + @Override + public int read(byte b[], int off, int len) throws IOException { + int nr = in.read(b, off, len); + if (nr > 0) + monitor.setProgress(nread += nr); + if (monitor.isCanceled()) { + InterruptedIOException exc = new InterruptedIOException("progress"); + exc.bytesTransferred = nread; + throw exc; + } + return nr; + } + + + /** + * Overrides FilterInputStream.skip + * to update the progress monitor after the skip. + */ + @Override + public long skip(long n) throws IOException { + long nr = in.skip(n); + if (nr > 0) + monitor.setProgress(nread += nr); + return nr; + } + + + /** + * Overrides FilterInputStream.close + * to close the progress monitor as well as the stream. + */ + @Override + public void close() throws IOException { + in.close(); + monitor.close(); + } + + + /** + * Overrides FilterInputStream.reset + * to reset the progress monitor as well as the stream. + */ + @Override + public synchronized void reset() throws IOException { + in.reset(); + nread = size - in.available(); + monitor.setProgress(nread); + } +} diff --git a/core/src/net/sf/openrocket/gui/util/FileHelper.java b/core/src/net/sf/openrocket/gui/util/FileHelper.java new file mode 100644 index 00000000..8c496393 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/FileHelper.java @@ -0,0 +1,120 @@ +package net.sf.openrocket.gui.util; + +import java.awt.Component; +import java.io.File; +import java.io.IOException; + +import javax.swing.JOptionPane; +import javax.swing.filechooser.FileFilter; + +import net.sf.openrocket.l10n.L10N; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * Helper methods related to user-initiated file manipulation. + *

+ * These methods log the necessary information to the debug log. +* + * @author Sampo Niskanen + */ +public final class FileHelper { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + // TODO: HIGH: Rename translation keys + + /** File filter for any rocket designs (*.ork, *.rkt) */ + public static final FileFilter ALL_DESIGNS_FILTER = + new SimpleFileFilter(trans.get("BasicFrame.SimpleFileFilter1"), + ".ork", ".ork.gz", ".rkt", ".rkt.gz"); + + /** File filter for OpenRocket designs (*.ork) */ + public static final FileFilter OPENROCKET_DESIGN_FILTER = + new SimpleFileFilter(trans.get("BasicFrame.SimpleFileFilter2"), ".ork", ".ork.gz"); + + /** File filter for RockSim designs (*.rkt) */ + public static final FileFilter ROCKSIM_DESIGN_FILTER = + new SimpleFileFilter(trans.get("BasicFrame.SimpleFileFilter3"), ".rkt", ".rkt.gz"); + + /** File filter for PDF files (*.pdf) */ + public static final FileFilter PDF_FILTER = + new SimpleFileFilter(trans.get("filetypes.pdf"), ".pdf"); + + /** File filter for CSV files (*.csv) */ + public static final FileFilter CSV_FILE_FILTER = + new SimpleFileFilter(trans.get("SimExpPan.desc"), ".csv"); + + + + + + private FileHelper() { + // Prevent instantiation + } + + /** + * Ensure that the provided file has a file extension. If the file does not have + * any extension, append the provided extension to it. + * + * @param original the original file + * @param extension the extension to append if none exists (without preceding dot) + * @return the resulting filen + */ + public static File ensureExtension(File original, String extension) { + + if (original.getName().indexOf('.') < 0) { + log.debug(1, "File name does not contain extension, adding '" + extension + "'"); + String name = original.getAbsolutePath(); + name = name + "." + extension; + return new File(name); + } + + return original; + } + + + /** + * Confirm that it is allowed to write to a file. If the file exists, + * a confirmation dialog will be presented to the user to ensure overwriting is ok. + * + * @param file the file that is going to be written. + * @param parent the parent component for the dialog. + * @return true to write, false to abort. + */ + public static boolean confirmWrite(File file, Component parent) { + if (file.exists()) { + log.info(1, "File " + file + " exists, confirming overwrite from user"); + int result = JOptionPane.showConfirmDialog(parent, + L10N.replace(trans.get("error.fileExists.desc"), "{filename}", file.getName()), + trans.get("error.fileExists.title"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); + if (result != JOptionPane.YES_OPTION) { + log.user(1, "User decided not to overwrite the file"); + return false; + } + log.user(1, "User decided to overwrite the file"); + } + return true; + } + + + /** + * Display an error message to the user that writing a file failed. + * + * @param e the I/O exception that caused the error. + * @param parent the parent component for the dialog. + */ + public static void errorWriting(IOException e, Component parent) { + + log.warn(1, "Error writing to file", e); + JOptionPane.showMessageDialog(parent, + new Object[] { + trans.get("error.writing.desc"), + e.getLocalizedMessage() + }, trans.get("error.writing.title"), JOptionPane.ERROR_MESSAGE); + + } + +} diff --git a/core/src/net/sf/openrocket/gui/util/GUIUtil.java b/core/src/net/sf/openrocket/gui/util/GUIUtil.java new file mode 100644 index 00000000..4ee74ba4 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/GUIUtil.java @@ -0,0 +1,588 @@ +package net.sf.openrocket.gui.util; + +import java.awt.Component; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Image; +import java.awt.KeyboardFocusManager; +import java.awt.Point; +import java.awt.Toolkit; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.event.ComponentListener; +import java.awt.event.FocusListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.imageio.ImageIO; +import javax.swing.AbstractAction; +import javax.swing.AbstractButton; +import javax.swing.Action; +import javax.swing.BoundedRangeModel; +import javax.swing.ComboBoxModel; +import javax.swing.DefaultBoundedRangeModel; +import javax.swing.DefaultComboBoxModel; +import javax.swing.DefaultListSelectionModel; +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JRootPane; +import javax.swing.JSlider; +import javax.swing.JSpinner; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.LookAndFeel; +import javax.swing.RootPaneContainer; +import javax.swing.SpinnerModel; +import javax.swing.SpinnerNumberModel; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.TitledBorder; +import javax.swing.event.ChangeListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableColumnModel; +import javax.swing.table.DefaultTableModel; +import javax.swing.table.TableColumnModel; +import javax.swing.table.TableModel; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.DefaultTreeSelectionModel; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreeSelectionModel; + +import net.sf.openrocket.gui.Resettable; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Invalidatable; +import net.sf.openrocket.util.MemoryManagement; + +public class GUIUtil { + private static final LogHelper log = Application.getLogger(); + + private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING"; + + private static final List images = new ArrayList(); + static { + loadImage("pix/icon/icon-256.png"); + loadImage("pix/icon/icon-064.png"); + loadImage("pix/icon/icon-048.png"); + loadImage("pix/icon/icon-032.png"); + loadImage("pix/icon/icon-016.png"); + } + + private static void loadImage(String file) { + InputStream is; + + is = ClassLoader.getSystemResourceAsStream(file); + if (is == null) + return; + + try { + Image image = ImageIO.read(is); + images.add(image); + } catch (IOException ignore) { + ignore.printStackTrace(); + } + } + + /** + * Return the DPI setting of the monitor. This is either the setting provided + * by the system or a user-specified DPI setting. + * + * @return the DPI setting to use. + */ + public static double getDPI() { + int dpi = Application.getPreferences().getInt("DPI", 0); // Tenths of a dpi + + if (dpi < 10) { + dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10; + } + if (dpi < 10) + dpi = 960; + + return (dpi) / 10.0; + } + + + + + /** + * Set suitable options for a single-use disposable dialog. This includes + * setting ESC to close the dialog, adding the appropriate window icons and + * setting the location based on the platform. If defaultButton is provided, + * it is set to the default button action. + *

+ * The default button must be already attached to the dialog. + * + * @param dialog the dialog. + * @param defaultButton the default button of the dialog, or null. + */ + public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) { + installEscapeCloseOperation(dialog); + setWindowIcons(dialog); + addModelNullingListener(dialog); + dialog.setLocationByPlatform(true); + dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); + dialog.pack(); + if (defaultButton != null) { + setDefaultButton(defaultButton); + } + } + + + + /** + * Add the correct action to close a JDialog when the ESC key is pressed. + * The dialog is closed by sending is a WINDOW_CLOSING event. + * + * @param dialog the dialog for which to install the action. + */ + public static void installEscapeCloseOperation(final JDialog dialog) { + Action dispatchClosing = new AbstractAction() { + @Override + public void actionPerformed(ActionEvent event) { + log.user("Closing dialog " + dialog); + dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING)); + } + }; + JRootPane root = dialog.getRootPane(); + root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY); + root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing); + } + + + /** + * Set the given button as the default button of the frame/dialog it is in. The button + * must be first attached to the window component hierarchy. + * + * @param button the button to set as the default button. + */ + public static void setDefaultButton(JButton button) { + Window w = SwingUtilities.windowForComponent(button); + if (w == null) { + throw new IllegalArgumentException("Attach button to a window first."); + } + if (!(w instanceof RootPaneContainer)) { + throw new IllegalArgumentException("Button not attached to RootPaneContainer, w=" + w); + } + ((RootPaneContainer) w).getRootPane().setDefaultButton(button); + } + + + + /** + * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of + * the components. This is necessary for e.g. JTextArea. + * + * @param c the component to modify + */ + public static void setTabToFocusing(Component c) { + Set strokes = new HashSet(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB"))); + c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes); + strokes = new HashSet(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB"))); + c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes); + } + + + + /** + * Set the OpenRocket icons to the window icons. + * + * @param window the window to set. + */ + public static void setWindowIcons(Window window) { + window.setIconImages(images); + } + + /** + * Add a listener to the provided window that will call {@link #setNullModels(Component)} + * on the window once it is closed. This method may only be used on single-use + * windows and dialogs, that will never be shown again once closed! + * + * @param window the window to add the listener to. + */ + public static void addModelNullingListener(final Window window) { + window.addWindowListener(new WindowAdapter() { + @Override + public void windowClosed(WindowEvent e) { + log.debug("Clearing all models of window " + window); + setNullModels(window); + MemoryManagement.collectable(window); + } + }); + } + + + + /** + * Set the best available look-and-feel into use. + */ + public static void setBestLAF() { + /* + * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used + * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few + * other alternatives. + */ + try { + // Set system L&F + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + + // Check whether we have an ugly L&F + LookAndFeel laf = UIManager.getLookAndFeel(); + if (laf == null || + laf.getName().matches(".*[mM][oO][tT][iI][fF].*") || + laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) { + + // Search for better LAF + UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels(); + String lafNames[] = { + ".*[gG][tT][kK].*", + ".*[wW][iI][nN].*", + ".*[mM][aA][cC].*", + ".*[aA][qQ][uU][aA].*", + ".*[nN][iI][mM][bB].*" + }; + + lf: for (String lafName : lafNames) { + for (UIManager.LookAndFeelInfo l : info) { + if (l.getName().matches(lafName)) { + UIManager.setLookAndFeel(l.getClassName()); + break lf; + } + } + } + } + } catch (Exception e) { + log.warn("Error setting LAF: " + e); + } + } + + + /** + * Changes the size of the font of the specified component by the given amount. + * + * @param component the component for which to change the font + * @param size the change in the font size + */ + public static void changeFontSize(JComponent component, float size) { + Font font = component.getFont(); + font = font.deriveFont(font.getSize2D() + size); + component.setFont(font); + } + + + + /** + * Automatically remember the size of a window. This stores the window size in the user + * preferences when resizing/maximizing the window and sets the state on the first call. + */ + public static void rememberWindowSize(final Window window) { + window.addComponentListener(new ComponentAdapter() { + @Override + public void componentResized(ComponentEvent e) { + log.debug("Storing size of " + window.getClass().getName() + ": " + window.getSize()); + ((SwingPreferences) Application.getPreferences()).setWindowSize(window.getClass(), window.getSize()); + if (window instanceof JFrame) { + if ((((JFrame) window).getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) { + log.debug("Storing maximized state of " + window.getClass().getName()); + ((SwingPreferences) Application.getPreferences()).setWindowMaximized(window.getClass()); + } + } + } + }); + + if (((SwingPreferences) Application.getPreferences()).isWindowMaximized(window.getClass())) { + if (window instanceof JFrame) { + ((JFrame) window).setExtendedState(JFrame.MAXIMIZED_BOTH); + } + } else { + Dimension dim = ((SwingPreferences) Application.getPreferences()).getWindowSize(window.getClass()); + if (dim != null) { + window.setSize(dim); + } + } + } + + + /** + * Automatically remember the position of a window. The position is stored in the user preferences + * every time the window is moved and set from there when first calling this method. + */ + public static void rememberWindowPosition(final Window window) { + window.addComponentListener(new ComponentAdapter() { + @Override + public void componentMoved(ComponentEvent e) { + ((SwingPreferences) Application.getPreferences()).setWindowPosition(window.getClass(), window.getLocation()); + } + }); + + // Set window position according to preferences, and set prefs when moving + Point position = ((SwingPreferences) Application.getPreferences()).getWindowPosition(window.getClass()); + if (position != null) { + window.setLocationByPlatform(false); + window.setLocation(position); + } + } + + + /** + * Changes the style of the font of the specified border. + * + * @param border the component for which to change the font + * @param style the change in the font style + */ + public static void changeFontStyle(TitledBorder border, int style) { + /* + * The fix of JRE bug #4129681 causes a TitledBorder occasionally to + * return a null font. We try to work around the issue by detecting it + * and reverting to the font of a JLabel instead. + */ + Font font = border.getTitleFont(); + if (font == null) { + log.error("Border font is null, reverting to JLabel font"); + font = new JLabel().getFont(); + if (font == null) { + log.error("JLabel font is null, not modifying font"); + return; + } + } + font = font.deriveFont(style); + if (font == null) { + throw new BugException("Derived font is null"); + } + border.setTitleFont(font); + } + + + + /** + * Traverses recursively the component tree, and sets all applicable component + * models to null, so as to remove the listener connections. After calling this + * method the component hierarchy should no longed be used. + *

+ * All components that use custom models should be added to this method, as + * there exists no standard way of removing the model from a component. + * + * @param c the component (null is ok) + */ + public static void setNullModels(Component c) { + if (c == null) + return; + + // Remove various listeners + for (ComponentListener l : c.getComponentListeners()) { + c.removeComponentListener(l); + } + for (FocusListener l : c.getFocusListeners()) { + c.removeFocusListener(l); + } + for (MouseListener l : c.getMouseListeners()) { + c.removeMouseListener(l); + } + for (PropertyChangeListener l : c.getPropertyChangeListeners()) { + c.removePropertyChangeListener(l); + } + for (PropertyChangeListener l : c.getPropertyChangeListeners("model")) { + c.removePropertyChangeListener("model", l); + } + for (PropertyChangeListener l : c.getPropertyChangeListeners("action")) { + c.removePropertyChangeListener("action", l); + } + + // Remove models for known components + // Why the FSCK must this be so hard?!?!? + + if (c instanceof JSpinner) { + + JSpinner spinner = (JSpinner) c; + for (ChangeListener l : spinner.getChangeListeners()) { + spinner.removeChangeListener(l); + } + SpinnerModel model = spinner.getModel(); + spinner.setModel(new SpinnerNumberModel()); + if (model instanceof Invalidatable) { + ((Invalidatable) model).invalidate(); + } + + } else if (c instanceof JSlider) { + + JSlider slider = (JSlider) c; + for (ChangeListener l : slider.getChangeListeners()) { + slider.removeChangeListener(l); + } + BoundedRangeModel model = slider.getModel(); + slider.setModel(new DefaultBoundedRangeModel()); + if (model instanceof Invalidatable) { + ((Invalidatable) model).invalidate(); + } + + } else if (c instanceof JComboBox) { + + JComboBox combo = (JComboBox) c; + for (ActionListener l : combo.getActionListeners()) { + combo.removeActionListener(l); + } + ComboBoxModel model = combo.getModel(); + combo.setModel(new DefaultComboBoxModel()); + if (model instanceof Invalidatable) { + ((Invalidatable) model).invalidate(); + } + + } else if (c instanceof AbstractButton) { + + AbstractButton button = (AbstractButton) c; + for (ActionListener l : button.getActionListeners()) { + button.removeActionListener(l); + } + Action model = button.getAction(); + button.setAction(new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + } + }); + if (model instanceof Invalidatable) { + ((Invalidatable) model).invalidate(); + } + + } else if (c instanceof JTable) { + + JTable table = (JTable) c; + TableModel model1 = table.getModel(); + table.setModel(new DefaultTableModel()); + if (model1 instanceof Invalidatable) { + ((Invalidatable) model1).invalidate(); + } + + TableColumnModel model2 = table.getColumnModel(); + table.setColumnModel(new DefaultTableColumnModel()); + if (model2 instanceof Invalidatable) { + ((Invalidatable) model2).invalidate(); + } + + ListSelectionModel model3 = table.getSelectionModel(); + table.setSelectionModel(new DefaultListSelectionModel()); + if (model3 instanceof Invalidatable) { + ((Invalidatable) model3).invalidate(); + } + + } else if (c instanceof JTree) { + + JTree tree = (JTree) c; + TreeModel model1 = tree.getModel(); + tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode())); + if (model1 instanceof Invalidatable) { + ((Invalidatable) model1).invalidate(); + } + + TreeSelectionModel model2 = tree.getSelectionModel(); + tree.setSelectionModel(new DefaultTreeSelectionModel()); + if (model2 instanceof Invalidatable) { + ((Invalidatable) model2).invalidate(); + } + + } else if (c instanceof Resettable) { + + ((Resettable) c).resetModel(); + + } + + // Recurse the component + if (c instanceof Container) { + Component[] cs = ((Container) c).getComponents(); + for (Component sub : cs) + setNullModels(sub); + } + + } + + + + /** + * A mouse listener that toggles the state of a boolean value in a table model + * when clicked on another column of the table. + *

+ * NOTE: If the table model does not extend AbstractTableModel, the model must + * fire a change event (which in normal table usage is not necessary). + * + * @author Sampo Niskanen + */ + public static class BooleanTableClickListener extends MouseAdapter { + + private final JTable table; + private final int clickColumn; + private final int booleanColumn; + + + public BooleanTableClickListener(JTable table) { + this(table, 1, 0); + } + + + public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) { + this.table = table; + this.clickColumn = clickColumn; + this.booleanColumn = booleanColumn; + } + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getButton() != MouseEvent.BUTTON1) + return; + + Point p = e.getPoint(); + int col = table.columnAtPoint(p); + if (col < 0) + return; + col = table.convertColumnIndexToModel(col); + if (col != clickColumn) + return; + + int row = table.rowAtPoint(p); + if (row < 0) + return; + row = table.convertRowIndexToModel(row); + if (row < 0) + return; + + TableModel model = table.getModel(); + Object value = model.getValueAt(row, booleanColumn); + + if (!(value instanceof Boolean)) { + throw new IllegalStateException("Table value at row=" + row + " col=" + + booleanColumn + " is not a Boolean, value=" + value); + } + + Boolean b = (Boolean) value; + b = !b; + model.setValueAt(b, row, booleanColumn); + if (model instanceof AbstractTableModel) { + ((AbstractTableModel) model).fireTableCellUpdated(row, booleanColumn); + } + } + + } + +} diff --git a/core/src/net/sf/openrocket/gui/util/Icons.java b/core/src/net/sf/openrocket/gui/util/Icons.java new file mode 100644 index 00000000..5c76bf24 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/Icons.java @@ -0,0 +1,101 @@ +package net.sf.openrocket.gui.util; + +import java.net.URL; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.Icon; +import javax.swing.ImageIcon; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + + +public class Icons { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + static { + log.debug("Starting to load icons"); + } + + /** + * Icons used for showing the status of a simulation (up to date, out of date, etc). + */ + public static final Map SIMULATION_STATUS_ICON_MAP; + static { + HashMap map = new HashMap(); + map.put(Simulation.Status.NOT_SIMULATED, loadImageIcon("pix/spheres/gray-16x16.png", "Not simulated")); + map.put(Simulation.Status.UPTODATE, loadImageIcon("pix/spheres/green-16x16.png", "Up to date")); + map.put(Simulation.Status.LOADED, loadImageIcon("pix/spheres/yellow-16x16.png", "Loaded from file")); + map.put(Simulation.Status.OUTDATED, loadImageIcon("pix/spheres/red-16x16.png", "Out-of-date")); + map.put(Simulation.Status.EXTERNAL, loadImageIcon("pix/spheres/blue-16x16.png", "Imported data")); + SIMULATION_STATUS_ICON_MAP = Collections.unmodifiableMap(map); + } + + public static final Icon SIMULATION_LISTENER_OK; + public static final Icon SIMULATION_LISTENER_ERROR; + static { + SIMULATION_LISTENER_OK = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.UPTODATE); + SIMULATION_LISTENER_ERROR = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.OUTDATED); + } + + + public static final Icon FILE_NEW = loadImageIcon("pix/icons/document-new.png", "New document"); + public static final Icon FILE_OPEN = loadImageIcon("pix/icons/document-open.png", "Open document"); + public static final Icon FILE_OPEN_EXAMPLE = loadImageIcon("pix/icons/document-open-example.png", "Open example document"); + public static final Icon FILE_SAVE = loadImageIcon("pix/icons/document-save.png", "Save document"); + public static final Icon FILE_SAVE_AS = loadImageIcon("pix/icons/document-save-as.png", "Save document as"); + public static final Icon FILE_PRINT = loadImageIcon("pix/icons/document-print.png", "Print document"); + public static final Icon FILE_CLOSE = loadImageIcon("pix/icons/document-close.png", "Close document"); + public static final Icon FILE_QUIT = loadImageIcon("pix/icons/application-exit.png", "Quit OpenRocket"); + + public static final Icon EDIT_UNDO = loadImageIcon("pix/icons/edit-undo.png", trans.get("Icons.Undo")); + public static final Icon EDIT_REDO = loadImageIcon("pix/icons/edit-redo.png", trans.get("Icons.Redo")); + public static final Icon EDIT_CUT = loadImageIcon("pix/icons/edit-cut.png", "Cut"); + public static final Icon EDIT_COPY = loadImageIcon("pix/icons/edit-copy.png", "Copy"); + public static final Icon EDIT_PASTE = loadImageIcon("pix/icons/edit-paste.png", "Paste"); + public static final Icon EDIT_DELETE = loadImageIcon("pix/icons/edit-delete.png", "Delete"); + public static final Icon EDIT_SCALE = loadImageIcon("pix/icons/edit-scale.png", "Scale"); + + public static final Icon HELP_ABOUT = loadImageIcon("pix/icons/help-about.png", "About"); + public static final Icon HELP_BUG_REPORT = loadImageIcon("pix/icons/help-bug.png", "Bug report"); + public static final Icon HELP_DEBUG_LOG = loadImageIcon("pix/icons/help-log.png", "Debug log"); + public static final Icon HELP_LICENSE = loadImageIcon("pix/icons/help-license.png", "License"); + + public static final Icon ZOOM_IN = loadImageIcon("pix/icons/zoom-in.png", "Zoom in"); + public static final Icon ZOOM_OUT = loadImageIcon("pix/icons/zoom-out.png", "Zoom out"); + + public static final Icon PREFERENCES = loadImageIcon("pix/icons/preferences.png", "Preferences"); + + public static final Icon DELETE = loadImageIcon("pix/icons/delete.png", "Delete"); + + static { + log.debug("Icons loaded"); + } + + /** + * Load an ImageIcon from the specified file. The file is obtained as a system + * resource from the normal classpath. If the file cannot be loaded a bug dialog + * is opened and null is returned. + * + * @param file the file to load. + * @param name the description of the icon. + * @return the ImageIcon, or null if could not be loaded (after the user closes the dialog) + */ + public static ImageIcon loadImageIcon(String file, String name) { + if (System.getProperty("openrocket.unittest") != null) { + return new ImageIcon(); + } + + URL url = ClassLoader.getSystemResource(file); + if (url == null) { + Application.getExceptionHandler().handleErrorCondition("Image file " + file + " not found, ignoring."); + return null; + } + return new ImageIcon(url, name); + } +} diff --git a/core/src/net/sf/openrocket/gui/util/OpenFileWorker.java b/core/src/net/sf/openrocket/gui/util/OpenFileWorker.java new file mode 100644 index 00000000..1b186408 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/OpenFileWorker.java @@ -0,0 +1,174 @@ +package net.sf.openrocket.gui.util; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; + +import javax.swing.SwingWorker; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.RocketLoader; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; + + +/** + * A SwingWorker thread that opens a rocket design file. + * + * @author Sampo Niskanen + */ +public class OpenFileWorker extends SwingWorker { + private static final LogHelper log = Application.getLogger(); + + private final File file; + private final InputStream stream; + private final RocketLoader loader; + + public OpenFileWorker(File file, RocketLoader loader) { + this.file = file; + this.stream = null; + this.loader = loader; + } + + + public OpenFileWorker(InputStream stream, RocketLoader loader) { + this.stream = stream; + this.file = null; + this.loader = loader; + } + + public RocketLoader getRocketLoader() { + return loader; + } + + @Override + protected OpenRocketDocument doInBackground() throws Exception { + InputStream is; + + // Get the correct input stream + if (file != null) { + is = new FileInputStream(file); + } else { + is = stream; + } + + // Buffer stream unless already buffered + if (!(is instanceof BufferedInputStream)) { + is = new BufferedInputStream(is); + } + + // Encapsulate in a ProgressInputStream + is = new ProgressInputStream(is); + + try { + return loader.load(is); + } finally { + try { + is.close(); + } catch (Exception e) { + Application.getExceptionHandler().handleErrorCondition("Error closing file", e); + } + } + } + + + + + private class ProgressInputStream extends FilterInputStream { + + private final int size; + private int readBytes = 0; + private int progress = -1; + + protected ProgressInputStream(InputStream in) { + super(in); + int s; + try { + s = in.available(); + } catch (IOException e) { + log.info("Exception while estimating available bytes!", e); + s = 0; + } + size = Math.max(s, 1); + } + + + + @Override + public int read() throws IOException { + int c = in.read(); + if (c >= 0) { + readBytes++; + setProgress(); + } + if (isCancelled()) { + throw new InterruptedIOException("OpenFileWorker was cancelled"); + } + return c; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int n = in.read(b, off, len); + if (n > 0) { + readBytes += n; + setProgress(); + } + if (isCancelled()) { + throw new InterruptedIOException("OpenFileWorker was cancelled"); + } + return n; + } + + @Override + public int read(byte[] b) throws IOException { + int n = in.read(b); + if (n > 0) { + readBytes += n; + setProgress(); + } + if (isCancelled()) { + throw new InterruptedIOException("OpenFileWorker was cancelled"); + } + return n; + } + + @Override + public long skip(long n) throws IOException { + long nr = in.skip(n); + if (nr > 0) { + readBytes += nr; + setProgress(); + } + if (isCancelled()) { + throw new InterruptedIOException("OpenFileWorker was cancelled"); + } + return nr; + } + + @Override + public synchronized void reset() throws IOException { + in.reset(); + readBytes = size - in.available(); + setProgress(); + if (isCancelled()) { + throw new InterruptedIOException("OpenFileWorker was cancelled"); + } + } + + + + private void setProgress() { + int p = MathUtil.clamp(readBytes * 100 / size, 0, 100); + if (progress != p) { + progress = p; + OpenFileWorker.this.setProgress(progress); + } + } + } +} diff --git a/core/src/net/sf/openrocket/gui/util/ProgressOutputStream.java b/core/src/net/sf/openrocket/gui/util/ProgressOutputStream.java new file mode 100644 index 00000000..22935736 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/ProgressOutputStream.java @@ -0,0 +1,73 @@ +package net.sf.openrocket.gui.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; + +import javax.swing.SwingWorker; + +import net.sf.openrocket.util.MathUtil; + + +public abstract class ProgressOutputStream extends FilterOutputStream { + + private final int totalBytes; + private final SwingWorker worker; + private int writtenBytes = 0; + private int progress = -1; + + public ProgressOutputStream(OutputStream out, int estimate, SwingWorker worker) { + super(out); + this.totalBytes = estimate; + this.worker = worker; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + writtenBytes += len; + setProgress(); + if (worker.isCancelled()) { + throw new InterruptedIOException("SaveFileWorker was cancelled"); + } + } + + @Override + public void write(byte[] b) throws IOException { + out.write(b); + writtenBytes += b.length; + setProgress(); + if (worker.isCancelled()) { + throw new InterruptedIOException("SaveFileWorker was cancelled"); + } + } + + @Override + public void write(int b) throws IOException { + out.write(b); + writtenBytes++; + setProgress(); + if (worker.isCancelled()) { + throw new InterruptedIOException("SaveFileWorker was cancelled"); + } + } + + + private void setProgress() { + int p = MathUtil.clamp(writtenBytes * 100 / totalBytes, 0, 100); + if (progress != p) { + progress = p; + setProgress(progress); + } + } + + /** + * Set the current progress. The value of progress is guaranteed + * to be between 0 and 100, inclusive. + * + * @param progress the current progress in the range 0-100. + */ + protected abstract void setProgress(int progress); + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/gui/util/SaveCSVWorker.java b/core/src/net/sf/openrocket/gui/util/SaveCSVWorker.java new file mode 100644 index 00000000..efa7d13d --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/SaveCSVWorker.java @@ -0,0 +1,131 @@ +package net.sf.openrocket.gui.util; + +import java.awt.Window; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.file.CSVExport; +import net.sf.openrocket.gui.dialogs.SwingWorkerDialog; +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.util.BugException; + + +public class SaveCSVWorker extends SwingWorker { + + private static final int BYTES_PER_FIELD_PER_POINT = 7; + + private final File file; + private final Simulation simulation; + private final FlightDataBranch branch; + private final FlightDataType[] fields; + private final Unit[] units; + private final String fieldSeparator; + private final String commentStarter; + private final boolean simulationComments; + private final boolean fieldComments; + private final boolean eventComments; + + + public SaveCSVWorker(File file, Simulation simulation, FlightDataBranch branch, + FlightDataType[] fields, Unit[] units, String fieldSeparator, String commentStarter, + boolean simulationComments, boolean fieldComments, boolean eventComments) { + this.file = file; + this.simulation = simulation; + this.branch = branch; + this.fields = fields; + this.units = units; + this.fieldSeparator = fieldSeparator; + this.commentStarter = commentStarter; + this.simulationComments = simulationComments; + this.fieldComments = fieldComments; + this.eventComments = eventComments; + } + + + @Override + protected Void doInBackground() throws Exception { + + int estimate = BYTES_PER_FIELD_PER_POINT * fields.length * branch.getLength(); + estimate = Math.max(estimate, 1000); + + // Create the ProgressOutputStream that provides progress estimates + ProgressOutputStream os = new ProgressOutputStream( + new BufferedOutputStream(new FileOutputStream(file)), + estimate, this) { + + @Override + protected void setProgress(int progress) { + SaveCSVWorker.this.setProgress(progress); + } + + }; + + try { + CSVExport.exportCSV(os, simulation, branch, fields, units, fieldSeparator, + commentStarter, simulationComments, fieldComments, eventComments); + } finally { + try { + os.close(); + } catch (Exception e) { + Application.getExceptionHandler().handleErrorCondition("Error closing file", e); + } + } + return null; + } + + + + /** + * Exports a CSV file using a progress dialog if necessary. + * + * @return true if the save was successful, false otherwise. + */ + public static boolean export(File file, Simulation simulation, FlightDataBranch branch, + FlightDataType[] fields, Unit[] units, String fieldSeparator, String commentStarter, + boolean simulationComments, boolean fieldComments, boolean eventComments, + Window parent) { + + + SaveCSVWorker worker = new SaveCSVWorker(file, simulation, branch, fields, units, + fieldSeparator, commentStarter, simulationComments, fieldComments, + eventComments); + + if (!SwingWorkerDialog.runWorker(parent, "Exporting flight data", + "Writing " + file.getName() + "...", worker)) { + + // User cancelled the save + file.delete(); + return false; + } + + try { + worker.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + + if (cause instanceof IOException) { + JOptionPane.showMessageDialog(parent, new String[] { + "An I/O error occurred while saving:", + e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE); + return false; + } else { + throw new BugException("Unknown error when saving file", e); + } + + } catch (InterruptedException e) { + throw new BugException("EDT was interrupted", e); + } + + return true; + } +} diff --git a/core/src/net/sf/openrocket/gui/util/SaveFileWorker.java b/core/src/net/sf/openrocket/gui/util/SaveFileWorker.java new file mode 100644 index 00000000..1ccc3145 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/SaveFileWorker.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.gui.util; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; + +import javax.swing.SwingWorker; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.startup.Application; + +public class SaveFileWorker extends SwingWorker { + + private final OpenRocketDocument document; + private final File file; + private final RocketSaver saver; + + public SaveFileWorker(OpenRocketDocument document, File file, RocketSaver saver) { + this.document = document; + this.file = file; + this.saver = saver; + } + + + @Override + protected Void doInBackground() throws Exception { + + int estimate = (int)saver.estimateFileSize(document, + document.getDefaultStorageOptions()); + + // Create the ProgressOutputStream that provides progress estimates + ProgressOutputStream os = new ProgressOutputStream( + new BufferedOutputStream(new FileOutputStream(file)), + estimate, this) { + + @Override + protected void setProgress(int progress) { + SaveFileWorker.this.setProgress(progress); + } + + }; + + try { + saver.save(os, document); + } finally { + try { + os.close(); + } catch (Exception e) { + Application.getExceptionHandler().handleErrorCondition("Error closing file", e); + } + } + return null; + } + +} diff --git a/core/src/net/sf/openrocket/gui/util/SimpleFileFilter.java b/core/src/net/sf/openrocket/gui/util/SimpleFileFilter.java new file mode 100644 index 00000000..39ded917 --- /dev/null +++ b/core/src/net/sf/openrocket/gui/util/SimpleFileFilter.java @@ -0,0 +1,77 @@ +package net.sf.openrocket.gui.util; + +import java.io.File; + +import javax.swing.filechooser.FileFilter; + +/** + * A FileFilter similar to FileNameExtensionFilter except that + * it allows multipart extensions (.ork.gz), and also implements + * the java.io.FileFilter interface. + * + * @author Sampo Niskanen + */ +public class SimpleFileFilter extends FileFilter implements java.io.FileFilter { + + private final String description; + private final boolean acceptDir; + private final String[] extensions; + + + /** + * Create filter that accepts files with the provided extensions that + * accepts directories as well. + * + * @param description the description of this file filter. + * @param extensions an array of extensions that match this filter. + */ + public SimpleFileFilter(String description, String ... extensions) { + this(description, true, extensions); + } + + + /** + * Create filter that accepts files with the provided extensions. + * + * @param description the description of this file filter. + * @param acceptDir whether to accept directories + * @param extensions an array of extensions that match this filter. + */ + public SimpleFileFilter(String description, boolean acceptDir, String ... extensions) { + this.description = description; + this.acceptDir = acceptDir; + this.extensions = new String[extensions.length]; + for (int i=0; i SUPPORTED_LOCALES; + static { + List list = new ArrayList(); + for (String lang : new String[] { "en", "de", "es", "fr" }) { + list.add(new Locale(lang)); + } + SUPPORTED_LOCALES = Collections.unmodifiableList(list); + } + + + /** + * Whether to use the debug-node instead of the normal node. + */ + private static final boolean DEBUG; + static { + DEBUG = (System.getProperty("openrocket.debug.prefs") != null); + } + + /** + * Whether to clear all preferences at application startup. This has an effect only + * if DEBUG is true. + */ + private static final boolean CLEARPREFS = true; + + /** + * The node name to use in the Java preferences storage. + */ + private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket"); + + private final Preferences PREFNODE; + + + public SwingPreferences() { + Preferences root = Preferences.userRoot(); + if (DEBUG && CLEARPREFS) { + try { + if (root.nodeExists(NODENAME)) { + root.node(NODENAME).removeNode(); + } + } catch (BackingStoreException e) { + throw new BugException("Unable to clear preference node", e); + } + } + PREFNODE = root.node(NODENAME); + } + + + + + ////////////////////// + + + + /** + * Store the current OpenRocket version into the preferences to allow for preferences migration. + */ + private void storeVersion() { + PREFNODE.put("OpenRocketVersion", BuildProperties.getVersion()); + } + + /** + * Return a string preference. + * + * @param key the preference key. + * @param def the default if no preference is stored + * @return the preference value + */ + @Override + public String getString(String key, String def) { + return PREFNODE.get(key, def); + } + + @Override + public String getString( String directory, String key, String defaultValue ) { + Preferences p = PREFNODE.node(directory); + return p.get(key,defaultValue); + } + + /** + * Set a string preference. + * + * @param key the preference key + * @param value the value to set, or null to remove the key + */ + @Override + public void putString(String key, String value) { + if (value == null) { + PREFNODE.remove(key); + } else { + PREFNODE.put(key, value); + } + storeVersion(); + } + + @Override + public void putString(String directory, String key, String value ) { + Preferences p = PREFNODE.node(directory); + if ( value == null ) { + p.remove(key); + } else { + p.put(key,value); + } + storeVersion(); + } + + /** + * Return a boolean preference. + * + * @param key the preference key + * @param def the default if no preference is stored + * @return the preference value + */ + @Override + public boolean getBoolean(String key, boolean def) { + return PREFNODE.getBoolean(key, def); + } + + /** + * Set a boolean preference. + * + * @param key the preference key + * @param value the value to set + */ + @Override + public void putBoolean(String key, boolean value) { + PREFNODE.putBoolean(key, value); + storeVersion(); + } + + @Override + public int getInt( String key, int defaultValue ) { + return PREFNODE.getInt(key, defaultValue); + } + + @Override + public void putInt( String key , int value ) { + PREFNODE.putInt(key, value ); + storeVersion(); + } + + @Override + public double getDouble(String key, double defaultValue) { + return PREFNODE.getDouble(key, defaultValue ); + } + + @Override + public void putDouble(String key, double value) { + PREFNODE.putDouble(key,value); + storeVersion(); + } + + + + /** + * Return a preferences object for the specified node name. + * + * @param nodeName the node name + * @return the preferences object for that node + */ + public Preferences getNode(String nodeName) { + return PREFNODE.node(nodeName); + } + + + ////////////////// + + + public static List getSupportedLocales() { + return SUPPORTED_LOCALES; + } + + public File getDefaultDirectory() { + String file = getString("defaultDirectory", null); + if (file == null) + return null; + return new File(file); + } + + public void setDefaultDirectory(File dir) { + String d; + if (dir == null) { + d = null; + } else { + d = dir.getAbsolutePath(); + } + putString("defaultDirectory", d); + storeVersion(); + } + + + /** + * Return a list of files/directories to be loaded as custom thrust curves. + *

+ * If this property has not been set, the directory "ThrustCurves" in the user + * application directory will be used. The directory will be created if it does not + * exist. + * + * @return a list of files to load as thrust curves. + */ + public List getUserThrustCurveFiles() { + List list = new ArrayList(); + + String files = getString(USER_THRUST_CURVES_KEY, null); + if (files == null) { + // Default to application directory + File tcdir = getDefaultUserThrustCurveFile(); + if (!tcdir.isDirectory()) { + tcdir.mkdirs(); + } + list.add(tcdir); + } else { + for (String file : files.split("\\" + SPLIT_CHARACTER)) { + file = file.trim(); + if (file.length() > 0) { + list.add(new File(file)); + } + } + } + + return list; + } + + public File getDefaultUserThrustCurveFile() { + File appdir = SystemInfo.getUserApplicationDirectory(); + File tcdir = new File(appdir, "ThrustCurves"); + return tcdir; + } + + + /** + * Set the list of files/directories to be loaded as custom thrust curves. + * + * @param files the files to load, or null to reset to default value. + */ + public void setUserThrustCurveFiles(List files) { + if (files == null) { + putString(USER_THRUST_CURVES_KEY, null); + return; + } + + String str = ""; + + for (File file : files) { + if (str.length() > 0) { + str += SPLIT_CHARACTER; + } + str += file.getAbsolutePath(); + } + putString(USER_THRUST_CURVES_KEY, str); + } + + public Color getMotorBorderColor() { + // TODO: MEDIUM: Motor color (settable?) + return new Color(0, 0, 0, 200); + } + + + public Color getMotorFillColor() { + // TODO: MEDIUM: Motor fill color (settable?) + return new Color(0, 0, 0, 100); + } + + + + public static int getMaxThreadCount() { + return Runtime.getRuntime().availableProcessors(); + } + + + + public Point getWindowPosition(Class c) { + int x, y; + String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null); + + if (pref == null) + return null; + + if (pref.indexOf(',') < 0) + return null; + + try { + x = Integer.parseInt(pref.substring(0, pref.indexOf(','))); + y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1)); + } catch (NumberFormatException e) { + return null; + } + return new Point(x, y); + } + + public void setWindowPosition(Class c, Point p) { + PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y); + storeVersion(); + } + + + + + public Dimension getWindowSize(Class c) { + int x, y; + String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null); + + if (pref == null) + return null; + + if (pref.indexOf(',') < 0) + return null; + + try { + x = Integer.parseInt(pref.substring(0, pref.indexOf(','))); + y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1)); + } catch (NumberFormatException e) { + return null; + } + return new Dimension(x, y); + } + + + public boolean isWindowMaximized(Class c) { + String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null); + return "max".equals(pref); + } + + public void setWindowSize(Class c, Dimension d) { + PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height); + storeVersion(); + } + + public void setWindowMaximized(Class c) { + PREFNODE.node("windows").put("size." + c.getCanonicalName(), "max"); + storeVersion(); + } + + /** + * this class returns a java.awt.Color object for the specified key. + * you can pass (java.awt.Color) null to the second argument to + * disambiguate + */ + public Color getColor( String key, Color defaultValue ) { + net.sf.openrocket.util.Color c = super.getColor(key, (net.sf.openrocket.util.Color) null); + if ( c == null ) { + return defaultValue; + } + return ColorConversion.toAwtColor(c); + } + + /** + * + */ + public void putColor( String key, Color value ) { + net.sf.openrocket.util.Color c = ColorConversion.fromAwtColor(value); + super.putColor(key, c); + } + + //// Printing + + + //// Background flight data computation + + public boolean computeFlightInBackground() { + return PREFNODE.getBoolean("backgroundFlight", true); + } + + public Simulation getBackgroundSimulation(Rocket rocket) { + Simulation s = new Simulation(rocket); + SimulationOptions cond = s.getOptions(); + + cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2); + cond.setWindSpeedAverage(1.0); + cond.setWindSpeedDeviation(0.1); + cond.setLaunchRodLength(5); + return s; + } + + + + ///////// Export variables + + public boolean isExportSelected(FlightDataType type) { + Preferences prefs = PREFNODE.node("exports"); + return prefs.getBoolean(type.getName(), false); + } + + public void setExportSelected(FlightDataType type, boolean selected) { + Preferences prefs = PREFNODE.node("exports"); + prefs.putBoolean(type.getName(), selected); + } + + + + ///////// Default unit storage + + public void loadDefaultUnits() { + Preferences prefs = PREFNODE.node("units"); + try { + + for (String key : prefs.keys()) { + UnitGroup group = UnitGroup.UNITS.get(key); + if (group == null) + continue; + + try { + group.setDefaultUnit(prefs.get(key, null)); + } catch (IllegalArgumentException ignore) { + } + } + + } catch (BackingStoreException e) { + Application.getExceptionHandler().handleErrorCondition(e); + } + } + + public void storeDefaultUnits() { + Preferences prefs = PREFNODE.node("units"); + + for (String key : UnitGroup.UNITS.keySet()) { + UnitGroup group = UnitGroup.UNITS.get(key); + if (group == null || group.getUnitCount() < 2) + continue; + + prefs.put(key, group.getDefaultUnit().getUnit()); + } + } + + + + //// Material storage + + + /** + * Add a user-defined material to the preferences. The preferences are + * first checked for an existing material matching the provided one using + * {@link Material#equals(Object)}. + * + * @param m the material to add. + */ + public void addUserMaterial(Material m) { + Preferences prefs = PREFNODE.node("userMaterials"); + + + // Check whether material already exists + if (getUserMaterials().contains(m)) { + return; + } + + // Add material using next free key (key is not used when loading) + String mat = m.toStorableString(); + for (int i = 0;; i++) { + String key = "material" + i; + if (prefs.get(key, null) == null) { + prefs.put(key, mat); + return; + } + } + } + + + /** + * Remove a user-defined material from the preferences. The matching is performed + * using {@link Material#equals(Object)}. + * + * @param m the material to remove. + */ + public void removeUserMaterial(Material m) { + Preferences prefs = PREFNODE.node("userMaterials"); + + try { + + // Iterate through materials and remove all keys with a matching material + for (String key : prefs.keys()) { + String value = prefs.get(key, null); + try { + + Material existing = Material.fromStorableString(value, true); + if (existing.equals(m)) { + prefs.remove(key); + } + + } catch (IllegalArgumentException ignore) { + } + + } + + } catch (BackingStoreException e) { + throw new IllegalStateException("Cannot read preferences!", e); + } + } + + + /** + * Return a set of all user-defined materials in the preferences. The materials + * are created marked as user-defined. + * + * @return a set of all user-defined materials. + */ + public Set getUserMaterials() { + Preferences prefs = PREFNODE.node("userMaterials"); + + HashSet materials = new HashSet(); + try { + + for (String key : prefs.keys()) { + String value = prefs.get(key, null); + try { + + Material m = Material.fromStorableString(value, true); + materials.add(m); + + } catch (IllegalArgumentException e) { + log.warn("Illegal material string " + value); + } + + } + + } catch (BackingStoreException e) { + throw new IllegalStateException("Cannot read preferences!", e); + } + + return materials; + } + + + //// Helper methods + +} diff --git a/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java b/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java new file mode 100644 index 00000000..b4212dc9 --- /dev/null +++ b/core/src/net/sf/openrocket/l10n/ClassBasedTranslator.java @@ -0,0 +1,92 @@ +package net.sf.openrocket.l10n; + +import java.util.MissingResourceException; + +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.util.BugException; + +/** + * A translator that prepends a pre-defined class name in front of a translation key + * and retrieves the translator for that key, and only if that is missing reverts to + * the base key name. The base class name can either be provided to the constructor + * or retrieved from the stack. + * + * @author Sampo Niskanen + */ +public class ClassBasedTranslator implements Translator { + + + private final Translator translator; + private final String className; + + /** + * Construct a translator using a specified class name. + * + * @param translator the translator from which to obtain the translations. + * @param className the base class name to prepend. + */ + public ClassBasedTranslator(Translator translator, String className) { + this.translator = translator; + this.className = className; + } + + /** + * Construct a translator by obtaining the base class name from the stack. + * + * @param translator the translator from which to obtain the translations. + * @param levels the number of levels to move upwards in the stack from the point where this method is called. + */ + public ClassBasedTranslator(Translator translator, int levels) { + this(translator, getStackClass(levels)); + } + + + + @Override + public String get(String key) { + String classKey = className + "." + key; + + try { + return translator.get(classKey); + } catch (MissingResourceException e) { + // Ignore + } + + try { + return translator.get(key); + } catch (MissingResourceException e) { + MissingResourceException mre = new MissingResourceException( + "Neither key '" + classKey + "' nor '" + key + "' could be found", e.getClassName(), key); + mre.initCause(e); + throw mre; + } + } + + + + private static String getStackClass(int levels) { + TraceException trace = new TraceException(); + StackTraceElement stack[] = trace.getStackTrace(); + final int index = levels + 2; + if (stack.length <= index) { + throw new BugException("Stack trace is too short, length=" + stack.length + ", expected=" + index, trace); + } + + StackTraceElement element = stack[index]; + String cn = element.getClassName(); + int pos = cn.lastIndexOf('.'); + if (pos >= 0) { + cn = cn.substring(pos + 1); + } + return cn; + } + + + + + // For unit testing purposes + String getClassName() { + return className; + } + +} diff --git a/core/src/net/sf/openrocket/l10n/DebugTranslator.java b/core/src/net/sf/openrocket/l10n/DebugTranslator.java new file mode 100644 index 00000000..5a2bf592 --- /dev/null +++ b/core/src/net/sf/openrocket/l10n/DebugTranslator.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.l10n; + +/** + * A translator implementation that returns the logical key in brackets instead + * of an actual translation. The class optionally verifies that the translation + * is actually obtainable from some other translator. + * + * @author Sampo Niskanen + */ +public class DebugTranslator implements Translator { + + private final Translator translator; + + + /** + * Sole constructor. + * + * @param translator the translator to verify the translation exists, or null not to verify. + */ + public DebugTranslator(Translator translator) { + this.translator = translator; + } + + + + @Override + public String get(String key) { + if (translator != null) { + translator.get(key); + } + return "[" + key + "]"; + } + +} diff --git a/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java b/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java new file mode 100644 index 00000000..dd916b6c --- /dev/null +++ b/core/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java @@ -0,0 +1,53 @@ +package net.sf.openrocket.l10n; + +import java.util.Locale; +import java.util.MissingResourceException; + +import net.sf.openrocket.startup.Application; + +/** + * A translator that suppresses MissingResourceExceptions and handles them gracefully. + * For the first missing key this class calls the exception handler, and afterwards + * always returns the key for missing translations. + * + * @author Sampo Niskanen + */ +public class ExceptionSuppressingTranslator implements Translator { + + static boolean errorReported = false; + + private final Translator translator; + + + /** + * Sole constructor. + * + * @param translator the translator to use + */ + public ExceptionSuppressingTranslator(Translator translator) { + this.translator = translator; + } + + + + @Override + public String get(String key) { + try { + return translator.get(key); + } catch (MissingResourceException e) { + handleError(key, e); + } + + return key; + } + + + + private static synchronized void handleError(String key, MissingResourceException e) { + if (!errorReported) { + errorReported = true; + Application.getExceptionHandler().handleErrorCondition("Can not find translation for '" + key + "' locale=" + Locale.getDefault(), e); + } + } + +} diff --git a/core/src/net/sf/openrocket/l10n/L10N.java b/core/src/net/sf/openrocket/l10n/L10N.java new file mode 100644 index 00000000..878b3c41 --- /dev/null +++ b/core/src/net/sf/openrocket/l10n/L10N.java @@ -0,0 +1,57 @@ +package net.sf.openrocket.l10n; + +import java.util.Locale; +import java.util.regex.Pattern; + +/** + * Helper methods for localization needs. + * + * @author Sampo Niskanen + */ +public final class L10N { + + private L10N() { + // Prevent instantiation + } + + + /** + * Replace a text token by a replacement value. + *

+ * A text token is a string portion that should be surrounded by + * braces, "{text}". + * + * @param original the original string. + * @param token the text token to replace. + * @param replacement the replacement text. + * @return the modified string. + */ + public static String replace(String original, String token, String replacement) { + return Pattern.compile(token, Pattern.LITERAL).matcher(original).replaceAll(replacement); + } + + + /** + * Convert a language code into a Locale. + * + * @param langcode the language code (null ok). + * @return the corresponding locale (or null if the input was null) + */ + public static Locale toLocale(String langcode) { + if (langcode == null) { + return null; + } + + Locale l; + String[] split = langcode.split("[_-]", 3); + if (split.length == 1) { + l = new Locale(split[0]); + } else if (split.length == 2) { + l = new Locale(split[0], split[1]); + } else { + l = new Locale(split[0], split[1], split[2]); + } + return l; + } + +} diff --git a/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java b/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java new file mode 100644 index 00000000..241ecefc --- /dev/null +++ b/core/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java @@ -0,0 +1,43 @@ +package net.sf.openrocket.l10n; + +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * A translator that obtains translated strings from a resource bundle. + * + * @author Sampo Niskanen + */ +public class ResourceBundleTranslator implements Translator { + + private final ResourceBundle bundle; + + /** + * Create a ResourceBundleTranslator using the default Locale. + * + * @param baseName the base name of the resource bundle + */ + public ResourceBundleTranslator(String baseName) { + this(baseName, Locale.getDefault()); + } + + /** + * Create a ResourceBundleTranslator using the specified Locale. + * + * @param baseName the base name of the resource bundle + * @param locale the locale to use + */ + public ResourceBundleTranslator(String baseName, Locale locale) { + this.bundle = ResourceBundle.getBundle(baseName, locale); + } + + + /* + * NOTE: This method must be thread-safe! + */ + @Override + public synchronized String get(String key) { + return bundle.getString(key); + } + +} diff --git a/core/src/net/sf/openrocket/l10n/Translator.java b/core/src/net/sf/openrocket/l10n/Translator.java new file mode 100644 index 00000000..9eed2cdf --- /dev/null +++ b/core/src/net/sf/openrocket/l10n/Translator.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.l10n; + +import java.util.MissingResourceException; + +/** + * An interface for obtaining translations from logical keys. + *

+ * Translator implementations must be thread-safe. + * + * @author Sampo Niskanen + */ +public interface Translator { + + /** + * Retrieve a translated string based on a logical key. This always returns + * some string, potentially falling back to the key itself. + * + * @param key the logical string key. + * @return the translated string. + * @throws MissingResourceException if the translation corresponding to the key is not found. + * @throws NullPointerException if key is null. + */ + public String get(String key); + +} diff --git a/core/src/net/sf/openrocket/logging/BufferLogger.java b/core/src/net/sf/openrocket/logging/BufferLogger.java new file mode 100644 index 00000000..eab392b6 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/BufferLogger.java @@ -0,0 +1,80 @@ +package net.sf.openrocket.logging; + +import java.util.EnumMap; +import java.util.List; + +/** + * A logger implementation that buffers specific levels of log lines. + * The levels that are logged are set using the method + * {@link #setStoreLevel(LogLevel, boolean)}. The stored LogLines can + * be obtained using {@link #getLogs()}. + * + * @author Sampo Niskanen + */ +public class BufferLogger extends LogHelper { + + private final CyclicBuffer buffer; + private final EnumMap storeLevels = + new EnumMap(LogLevel.class); + + + /** + * Create a buffered logger with that logs the specified number of log + * lines. By default all log levels are buffered. + * + * @param length the length of the buffer. + */ + public BufferLogger(int length) { + for (LogLevel l: LogLevel.values()) { + storeLevels.put(l, true); + } + buffer = new CyclicBuffer(length); + } + + + @Override + public void log(LogLine line) { + if (storeLevels.get(line.getLevel())) { + buffer.add(line); + } + } + + /** + * Set whether the specified log level is buffered. + * + * @param level the log level. + * @param store whether to store the level. + */ + public void setStoreLevel(LogLevel level, boolean store) { + storeLevels.put(level, store); + } + + /** + * Get whether the specified log level is buffered. + * + * @param level the log level. + * @return whether the log level is stored. + */ + public boolean getStoreLevel(LogLevel level) { + return storeLevels.get(level); + } + + + /** + * Return all the buffered log lines. + * + * @return a list of all buffered log lines. + */ + public List getLogs() { + return buffer.asList(); + } + + /** + * Return the number of log lines that has been overwritten. + * + * @return the number of log lines missed. + */ + public int getOverwriteCount() { + return buffer.getOverwriteCount(); + } +} diff --git a/core/src/net/sf/openrocket/logging/CyclicBuffer.java b/core/src/net/sf/openrocket/logging/CyclicBuffer.java new file mode 100644 index 00000000..31328ba2 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/CyclicBuffer.java @@ -0,0 +1,176 @@ +package net.sf.openrocket.logging; + +import java.util.AbstractQueue; +import java.util.ArrayList; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * A cyclic buffer with a fixed size. When more data is inserted, the newest + * data will overwrite the oldest data. + *

+ * Though this class implements the Queue interface, it specifically breaks the + * contract by overwriting (removing) data without specific removal. It also + * currently does not support removing arbitrary elements from the set. + *

+ * The methods in this class are synchronized for concurrent modification. + * However, iterating over the set is not thread-safe. To obtain a snapshot + * of the state of the buffer, use {@link #asList()}. + * + * @param the object type that is stored. + * @author Sampo Niskanen + */ +public class CyclicBuffer extends AbstractQueue { + + private final ArrayList buffer; + private final int maxSize; + + private int startPosition = 0; + private int size = 0; + private int overwriteCount = 0; + + private int modCount = 0; + + + /** + * Create a cyclic buffer of the specified size. + * + * @param size the size of the cyclic buffer. + */ + public CyclicBuffer(int size) { + this.buffer = new ArrayList(size); + for (int i=0; i iterator() { + return new CyclicBufferIterator(); + } + + + /** + * Return a snapshot of the current buffered objects in the order they + * were placed in the buffer. The list is independent of the buffer. + * + * @return a list of the buffered objects. + */ + public synchronized List asList() { + ArrayList list = new ArrayList(size); + if (startPosition + size > maxSize) { + list.addAll(buffer.subList(startPosition, maxSize)); + list.addAll(buffer.subList(0, startPosition + size - maxSize)); + } else { + list.addAll(buffer.subList(startPosition, startPosition+size)); + } + return list; + } + + + /** + * Return the number of elements that have been overwritten in the buffer. + * The overwritten elements are the elements that have been added to the + * buffer, have not been explicitly removed but are not present in the list. + * + * @return the number of overwritten elements this far. + */ + public synchronized int getOverwriteCount() { + return overwriteCount; + } + + + private int next(int n) { + return (n+1) % maxSize; + } + + + private class CyclicBufferIterator implements Iterator { + + private int expectedModCount; + private int n = 0; + + public CyclicBufferIterator() { + this.expectedModCount = modCount; + } + + @Override + public boolean hasNext() { + synchronized (CyclicBuffer.this) { + if (expectedModCount != modCount) { + throw new ConcurrentModificationException("expectedModCount="+ + expectedModCount+" modCount=" + modCount); + } + return (n < size); + } + } + + @Override + public E next() { + synchronized (CyclicBuffer.this) { + if (expectedModCount != modCount) { + throw new ConcurrentModificationException("expectedModCount="+ + expectedModCount+" modCount=" + modCount); + } + if (n >= size) { + throw new NoSuchElementException("n="+n+" size="+size); + } + n++; + return buffer.get((startPosition + n-1) % maxSize); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("random remove not supported"); + } + } +} diff --git a/core/src/net/sf/openrocket/logging/DelegatorLogger.java b/core/src/net/sf/openrocket/logging/DelegatorLogger.java new file mode 100644 index 00000000..8c3a22df --- /dev/null +++ b/core/src/net/sf/openrocket/logging/DelegatorLogger.java @@ -0,0 +1,52 @@ +package net.sf.openrocket.logging; + +import java.util.List; + +import net.sf.openrocket.util.ArrayList; + +/** + * A logger implementation that delegates logging to other logger implementations. + * Multiple loggers can be added to the delegator, all of which will receive + * all of the log lines. + * + * @author Sampo Niskanen + */ +public class DelegatorLogger extends LogHelper { + + /** + * List of loggers. This list must not be modified, instead it should be + * replaced every time the list is changed. + */ + private volatile ArrayList loggers = new ArrayList(); + + @Override + public void log(LogLine line) { + // Must create local reference for thread safety + List list = loggers; + for (LogHelper l : list) { + l.log(line); + } + } + + + /** + * Add a logger from the delegation list. + * @param logger the logger to add. + */ + public synchronized void addLogger(LogHelper logger) { + ArrayList newList = loggers.clone(); + newList.add(logger); + this.loggers = newList; + } + + /** + * Remove a logger from the delegation list. + * @param logger the logger to be removed. + */ + public synchronized void removeLogger(LogHelper logger) { + ArrayList newList = loggers.clone(); + newList.remove(logger); + this.loggers = newList; + } + +} diff --git a/core/src/net/sf/openrocket/logging/LogHelper.java b/core/src/net/sf/openrocket/logging/LogHelper.java new file mode 100644 index 00000000..d6b52104 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/LogHelper.java @@ -0,0 +1,517 @@ +package net.sf.openrocket.logging; + +import net.sf.openrocket.util.BugException; + + +/** + * Base class for all loggers used in OpenRocket. + *

+ * This class contains methods for logging at various log levels, and methods + * which take the logging level as a parameter. All methods may take three types + * of parameters: + *

    + *
  • levels number of additional levels of the stack trace to print + * on the log line. This is useful to determine from where + * the current method has been called. Zero if not provided. + *
  • message the String message (may be null). + *
  • cause the exception that caused this log (may be null). + *
+ *

+ * The logging methods are guaranteed never to throw an exception, and can thus be safely + * used in finally blocks. + * + * @author Sampo Niskanen + */ +public abstract class LogHelper { + /** + * Level from which upward a TraceException is added to the log lines. + */ + private static final LogLevel TRACING_LOG_LEVEL = + LogLevel.fromString(System.getProperty("openrocket.log.tracelevel"), LogLevel.INFO); + + private static final DelegatorLogger delegator = new DelegatorLogger(); + + + + /** + * Get the logger to be used in logging. + * + * @return the logger to be used in all logging. + */ + public static LogHelper getInstance() { + return delegator; + } + + + + /** + * Log a LogLine object. This method needs to be able to cope with multiple threads + * accessing it concurrently (for example by being synchronized). + * + * @param line the LogLine to log. + */ + public abstract void log(LogLine line); + + + /** + * Log using VBOSE level. + * + * @param message the logged message (may be null). + */ + public void verbose(String message) { + try { + log(createLogLine(0, LogLevel.VBOSE, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using VBOSE level. + * + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void verbose(String message, Throwable cause) { + try { + log(createLogLine(0, LogLevel.VBOSE, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using VBOSE level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + */ + public void verbose(int levels, String message) { + try { + log(createLogLine(levels, LogLevel.VBOSE, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using VBOSE level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void verbose(int levels, String message, Throwable cause) { + try { + log(createLogLine(levels, LogLevel.VBOSE, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Log using DEBUG level. + * + * @param message the logged message (may be null). + */ + public void debug(String message) { + try { + log(createLogLine(0, LogLevel.DEBUG, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using DEBUG level. + * + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void debug(String message, Throwable cause) { + try { + log(createLogLine(0, LogLevel.DEBUG, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using DEBUG level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + */ + public void debug(int levels, String message) { + try { + log(createLogLine(levels, LogLevel.DEBUG, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using DEBUG level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void debug(int levels, String message, Throwable cause) { + try { + log(createLogLine(levels, LogLevel.DEBUG, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Log using INFO level. + * + * @param message the logged message (may be null). + */ + public void info(String message) { + try { + log(createLogLine(0, LogLevel.INFO, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using INFO level. + * + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void info(String message, Throwable cause) { + try { + log(createLogLine(0, LogLevel.INFO, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using INFO level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + */ + public void info(int levels, String message) { + try { + log(createLogLine(levels, LogLevel.INFO, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using INFO level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void info(int levels, String message, Throwable cause) { + try { + log(createLogLine(levels, LogLevel.INFO, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Log using USER level. + * + * @param message the logged message (may be null). + */ + public void user(String message) { + try { + log(createLogLine(0, LogLevel.USER, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using USER level. + * + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void user(String message, Throwable cause) { + try { + log(createLogLine(0, LogLevel.USER, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using USER level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + */ + public void user(int levels, String message) { + try { + log(createLogLine(levels, LogLevel.USER, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using USER level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void user(int levels, String message, Throwable cause) { + try { + log(createLogLine(levels, LogLevel.USER, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Log using WARN level. + * + * @param message the logged message (may be null). + */ + public void warn(String message) { + try { + log(createLogLine(0, LogLevel.WARN, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using WARN level. + * + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void warn(String message, Throwable cause) { + try { + log(createLogLine(0, LogLevel.WARN, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using WARN level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + */ + public void warn(int levels, String message) { + try { + log(createLogLine(levels, LogLevel.WARN, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using WARN level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void warn(int levels, String message, Throwable cause) { + try { + log(createLogLine(levels, LogLevel.WARN, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * Log using ERROR level. + * + * @param message the logged message (may be null). + */ + public void error(String message) { + try { + log(createLogLine(0, LogLevel.ERROR, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using ERROR level. + * + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void error(String message, Throwable cause) { + try { + log(createLogLine(0, LogLevel.ERROR, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using ERROR level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + */ + public void error(int levels, String message) { + try { + log(createLogLine(levels, LogLevel.ERROR, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using ERROR level. + * + * @param levels number of additional levels of stack trace to include. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void error(int levels, String message, Throwable cause) { + try { + log(createLogLine(levels, LogLevel.ERROR, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + + /** + * Log using the provided log level. + * + * @param level the logging level. + * @param message the logged message (may be null). + */ + public void log(LogLevel level, String message) { + try { + log(createLogLine(0, level, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using the provided log level. + * + * @param level the logging level. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void log(LogLevel level, String message, Throwable cause) { + try { + log(createLogLine(0, level, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using the provided log level. + * + * @param levels number of additional levels of stack trace to include. + * @param level the logging level. + * @param message the logged message (may be null). + */ + public void log(int levels, LogLevel level, String message) { + try { + log(createLogLine(levels, level, message, null)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Log using the provided log level. + * + * @param levels number of additional levels of stack trace to include. + * @param level the logging level. + * @param message the logged message (may be null). + * @param cause the causing exception (may be null). + */ + public void log(int levels, LogLevel level, String message, Throwable cause) { + try { + log(createLogLine(levels, level, message, cause)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + + /** + * Instantiates, logs and throws a BugException. The message is logged at + * ERROR level. + *

+ * This method never returns normally. + * + * @param message the message for the log and exception. + * @throws BugException always. + */ + public void throwBugException(String message) throws BugException { + BugException e = new BugException(message); + log(createLogLine(0, LogLevel.ERROR, message, e)); + throw e; + } + + /** + * Instantiates, logs and throws a BugException. The message is logged at + * ERROR level with the specified cause. + *

+ * This method never returns normally. + * + * @param message the message for the log and exception. + * @param cause the causing exception (may be null). + * @throws BugException always. + */ + public void throwBugException(String message, Throwable cause) throws BugException { + BugException e = new BugException(message, cause); + log(createLogLine(0, LogLevel.ERROR, message, cause)); + throw e; + } + + + + + /** + * Create a LogLine object from the provided information. This method must be + * called directly from the called method in order for the trace position + * to be correct! + * + * @param additionalLevels how many additional stack trace levels to include on the line. + * @param level the log level. + * @param message the log message (null ok). + * @param cause the log exception (null ok). + * + * @return a LogLine populated with all necessary fields. + */ + private LogLine createLogLine(int additionalLevels, LogLevel level, String message, + Throwable cause) { + TraceException trace; + if (level.atLeast(TRACING_LOG_LEVEL)) { + trace = new TraceException(2, 2 + additionalLevels); + } else { + trace = null; + } + return new LogLine(level, trace, message, cause); + } +} diff --git a/core/src/net/sf/openrocket/logging/LogLevel.java b/core/src/net/sf/openrocket/logging/LogLevel.java new file mode 100644 index 00000000..b52d1453 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/LogLevel.java @@ -0,0 +1,123 @@ +package net.sf.openrocket.logging; + +/** + * The logging level. The natural order of the LogLevel orders the levels + * from highest priority to lowest priority. Comparisons of the relative levels + * should be performed using the methods {@link #atLeast(LogLevel)}, + * {@link #moreThan(LogLevel)} and {@link #compareTo(LogLevel)}. + *

+ * A description of the level can be obtained using {@link #toString()}. + * + * @author Sampo Niskanen + */ +public enum LogLevel { + /** + * Level for indicating a bug or error condition noticed in the software or JRE. + * No ERROR level events _should_ occur while running the program. + */ + ERROR, + + /** + * Level for indicating error conditions or atypical events that can occur during + * normal operation (errors while loading files, weird computation results etc). + */ + WARN, + + /** + * Level for logging user actions (adding and modifying components, running + * simulations etc). A user action should be logged as soon as possible on this + * level. The level is separate so that additional INFO messages won't purge + * user actions from a bounded log buffer. + */ + USER, + + /** + * Level for indicating general level actions the software is performing and + * other notable events during execution (dialogs shown, simulations run etc). + */ + INFO, + + /** + * Level for indicating mid-results, outcomes of methods and other debugging + * information. The data logged should be of value when analyzing error + * conditions and what has caused them. Places that are called repeatedly + * during e.g. flight simulation should use the VBOSE level instead. + */ + DEBUG, + + /** + * Level of verbose debug logging to be used in areas which are called repeatedly, + * such as computational methods used in simulations. This level is separated to + * allow filtering out the verbose logs generated during simulations, DnD etc. + * from the normal debug logs. + */ + VBOSE; + + /** The log level with highest priority */ + public static final LogLevel HIGHEST; + /** The log level with lowest priority */ + public static final LogLevel LOWEST; + /** The maximum length of a level textual description */ + public static final int LENGTH; + + static { + int length = 0; + for (LogLevel l : LogLevel.values()) { + length = Math.max(length, l.toString().length()); + } + LENGTH = length; + + LogLevel[] values = LogLevel.values(); + HIGHEST = values[0]; + LOWEST = values[values.length - 1]; + } + + /** + * Return true if this log level is of a priority at least that of + * level. + */ + public boolean atLeast(LogLevel level) { + return this.compareTo(level) <= 0; + } + + /** + * Return true if this log level is of a priority greater than that of + * level. + */ + public boolean moreThan(LogLevel level) { + return this.compareTo(level) < 0; + } + + + /** + * Return a log level corresponding to a string. The string is case-insensitive. If the + * string is case-insensitively equal to "all", then the lowest logging level is returned. + * + * @param value the string name of a log level, or "all" + * @param defaultLevel the value to return if the string doesn't correspond to any log level or is null + * @return the corresponding log level, of defaultLevel. + */ + public static LogLevel fromString(String value, LogLevel defaultLevel) { + + // Normalize the string + if (value == null) { + return defaultLevel; + } + value = value.toUpperCase().trim(); + + // Find the correct level + LogLevel level = defaultLevel; + if (value.equals("ALL")) { + LogLevel[] values = LogLevel.values(); + level = values[values.length - 1]; + } else { + try { + level = LogLevel.valueOf(value); + } catch (Exception e) { + // Ignore + } + } + return level; + } + +} diff --git a/core/src/net/sf/openrocket/logging/LogLevelBufferLogger.java b/core/src/net/sf/openrocket/logging/LogLevelBufferLogger.java new file mode 100644 index 00000000..d9021065 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/LogLevelBufferLogger.java @@ -0,0 +1,73 @@ +package net.sf.openrocket.logging; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; + +/** + * A logger that buffers a specific number of log lines from every log + * level. This prevents a multitude of lower-level lines from purging + * away important higher-level messages. The method {@link #getLogs()} + * combines these logs into their original (natural) order. A log line + * is also inserted stating the number of log lines of the particular + * level that have been purged from the buffer. + * + * @author Sampo Niskanen + */ +public class LogLevelBufferLogger extends LogHelper { + + private final EnumMap loggers = + new EnumMap(LogLevel.class); + + + /** + * Sole constructor. + * @param count the number of log lines from each level to buffer. + */ + public LogLevelBufferLogger(int count) { + for (LogLevel level : LogLevel.values()) { + loggers.put(level, new BufferLogger(count)); + } + } + + @Override + public void log(LogLine line) { + // Delegate to the buffered logger of this level + loggers.get(line.getLevel()).log(line); + } + + + /** + * Retrieve all buffered log lines in order. A log line is also added to indicate the number of + * log lines that have been purged for that log level. + * + * @return a list of log lines in order. + */ + public List getLogs() { + List result = new ArrayList(); + + for (LogLevel level : LogLevel.values()) { + BufferLogger logger = loggers.get(level); + List logs = logger.getLogs(); + int misses = logger.getOverwriteCount(); + + if (misses > 0) { + if (logs.isEmpty()) { + result.add(new LogLine(level, 0, 0, null, + "===== " + misses + " " + level + " lines removed but log is empty! =====", + null)); + } else { + result.add(new LogLine(level, logs.get(0).getLogCount(), 0, null, + "===== " + misses + " " + level + " lines removed =====", null)); + } + } + result.addAll(logs); + } + + Collections.sort(result); + return result; + } + + +} diff --git a/core/src/net/sf/openrocket/logging/LogLine.java b/core/src/net/sf/openrocket/logging/LogLine.java new file mode 100644 index 00000000..c2dc4fa6 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/LogLine.java @@ -0,0 +1,155 @@ +package net.sf.openrocket.logging; + +import java.io.PrintWriter; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Container object for a log line. A log line consists of the following elements: + *

    + *
  • a LogLevel + *
  • a TraceException + *
  • a message + *
  • a cause Throwable + *
  • an incremental log line counter (provided by LogLine) + *
  • a millisecond timestamp (provided by LogLine) + *
+ * Any one of the provided input values may be null. + * + * @author Sampo Niskanen + */ +public class LogLine implements Comparable { + + private static final AtomicInteger logCount = new AtomicInteger(1); + private static final long startTime = System.currentTimeMillis(); + + private final LogLevel level; + private final int count; + private final long timestamp; + private final TraceException trace; + private final String message; + private final Throwable cause; + + private volatile String formattedMessage = null; + + + /** + * Construct a LogLine at the current moment. The next log line count number is selected + * and the current run time set to the timestamp. + * + * @param level the logging level + * @param trace the trace exception for the log line, null permitted + * @param message the log message + * @param cause the causing throwable, null permitted + */ + public LogLine(LogLevel level, TraceException trace, String message, Throwable cause) { + this(level, logCount.getAndIncrement(), System.currentTimeMillis() - startTime, trace, message, cause); + } + + /** + * Construct a LogLine with all parameters. This should only be used in special conditions, + * for example to insert a log line at a specific point within normal logs. + * + * @param level the logging level + * @param count the log line count number + * @param timestamp the log line timestamp + * @param trace the trace exception for the log line, null permitted + * @param message the log message + * @param cause the causing throwable, null permitted + */ + public LogLine(LogLevel level, int count, long timestamp, + TraceException trace, String message, Throwable cause) { + this.level = level; + this.count = count; + this.timestamp = timestamp; + this.trace = trace; + this.message = message; + this.cause = cause; + } + + + + /** + * @return the level + */ + public LogLevel getLevel() { + return level; + } + + + /** + * @return the count + */ + public int getLogCount() { + return count; + } + + + /** + * @return the timestamp + */ + public long getTimestamp() { + return timestamp; + } + + + /** + * @return the trace + */ + public TraceException getTrace() { + return trace; + } + + + /** + * @return the message + */ + public String getMessage() { + return message; + } + + + /** + * @return the error + */ + public Throwable getCause() { + return cause; + } + + + + + /** + * Return a formatted string of the log line. The line contains the log + * line count, the time stamp, the log level, the trace position, the log + * message and, if provided, the stack trace of the error throwable. + */ + @Override + public String toString() { + if (formattedMessage == null) { + String str; + str = String.format("%4d %10.3f %-" + LogLevel.LENGTH + "s %s %s", + count, timestamp / 1000.0, (level != null) ? level.toString() : "NULL", + (trace != null) ? trace.getMessage() : "(-)", + message); + if (cause != null) { + StackTraceWriter stw = new StackTraceWriter(); + PrintWriter pw = new PrintWriter(stw); + cause.printStackTrace(pw); + pw.flush(); + str = str + "\n" + stw.toString(); + } + formattedMessage = str; + } + return formattedMessage; + } + + + /** + * Compare against another log line based on the log line count number. + */ + @Override + public int compareTo(LogLine o) { + return this.count - o.count; + } + +} diff --git a/core/src/net/sf/openrocket/logging/PrintStreamLogger.java b/core/src/net/sf/openrocket/logging/PrintStreamLogger.java new file mode 100644 index 00000000..d598783f --- /dev/null +++ b/core/src/net/sf/openrocket/logging/PrintStreamLogger.java @@ -0,0 +1,36 @@ +package net.sf.openrocket.logging; + +import java.io.PrintStream; +import java.util.EnumMap; + +/** + * A logger that output log lines to various print streams depending on the log level. + * By default output is logged nowhere. + * + * @author Sampo Niskanen + */ +public class PrintStreamLogger extends LogHelper { + + private final EnumMap output = new EnumMap(LogLevel.class); + + + @Override + public void log(LogLine line) { + PrintStream stream = output.get(line.getLevel()); + if (stream != null) { + stream.println(line.toString()); + } + } + + public PrintStream getOutput(LogLevel level) { + return output.get(level); + } + + public void setOutput(LogLevel level, PrintStream stream) { + if (level == null) { + throw new IllegalArgumentException("level=" + level + " stream=" + stream); + } + output.put(level, stream); + } + +} diff --git a/core/src/net/sf/openrocket/logging/StackTraceWriter.java b/core/src/net/sf/openrocket/logging/StackTraceWriter.java new file mode 100644 index 00000000..dbe2c5a9 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/StackTraceWriter.java @@ -0,0 +1,48 @@ +package net.sf.openrocket.logging; + +import java.io.IOException; +import java.io.Writer; + +public class StackTraceWriter extends Writer { + + public static final String PREFIX = " > "; + + private final StringBuilder buffer = new StringBuilder(); + private boolean addPrefix = true; + + @Override + public void write(char[] cbuf, int off, int len) throws IOException { + for (int i=0; i 0) { + return buffer.substring(0, buffer.length()-1); + } else { + return buffer.toString(); + } + } + + + @Override + public void close() throws IOException { + // no-op + } + + @Override + public void flush() throws IOException { + // no-op + } + +} diff --git a/core/src/net/sf/openrocket/logging/TraceException.java b/core/src/net/sf/openrocket/logging/TraceException.java new file mode 100644 index 00000000..f3fc1714 --- /dev/null +++ b/core/src/net/sf/openrocket/logging/TraceException.java @@ -0,0 +1,139 @@ +package net.sf.openrocket.logging; + + + +/** + * An exception that is used to store a stack trace. On modern computers + * instantiation of an exception takes on the order of one microsecond, while + * examining the trace typically takes several times longer. Therefore the + * exception should be stored and the stack trace examined only when necessary. + *

+ * The {@link #getMessage()} method returns a description of the position + * where this exception has been instantiated. The position is provided + * as many levels upwards from the instantiation position as provided to the + * constructor. + * + * @author Sampo Niskanen + */ +public class TraceException extends Exception { + + private static final String STANDARD_PACKAGE_PREFIX = "net.sf.openrocket."; + + private final int minLevel; + private final int maxLevel; + private volatile String message = null; + + + /** + * Instantiate exception that provides the line of instantiation as a message. + */ + public TraceException() { + this(0, 0); + } + + /** + * Instantiate exception that provides the provided number of levels upward + * from the instantiation location as a message. The level provided + * is how many levels upward should be examined to find the stack trace + * position for the exception message. + * + * @param level how many levels upward to examine the stack trace to find + * the correct message. + */ + public TraceException(int level) { + this(level, level); + } + + + /** + * Instantiate exception that provides a range of levels upward from the + * instantiation location as a message. This is useful to identify the + * next level of callers upward. + * + * @param minLevel the first level which to include. + * @param maxLevel the last level which to include. + */ + public TraceException(int minLevel, int maxLevel) { + if (minLevel > maxLevel || minLevel < 0) { + throw new IllegalArgumentException("minLevel=" + minLevel + " maxLevel=" + maxLevel); + } + this.minLevel = minLevel; + this.maxLevel = maxLevel; + } + + + /** + * Construct an exception with the specified message. + * + * @param message the message for the exception. + */ + public TraceException(String message) { + this(0, 0); + this.message = message; + } + + + /** + * Construct an exception with the specified message and cause. + * + * @param message the message for the exception. + * @param cause the cause for this exception. + */ + public TraceException(String message, Throwable cause) { + this(0, 0); + this.message = message; + this.initCause(cause); + } + + + /** + * Get the description of the code position as provided in the constructor. + */ + @Override + public String getMessage() { + if (message == null) { + StackTraceElement[] elements = this.getStackTrace(); + + StringBuilder sb = new StringBuilder(); + sb.append('('); + + if (elements == null || elements.length == 0) { + sb.append("no stack trace"); + } else { + + int levelCount = 0; + int position = minLevel; + while (levelCount <= (maxLevel - minLevel) && position < elements.length) { + + // Ignore synthetic "access$0" methods generated by the JRE + if (elements[position].getMethodName().contains("$")) { + position++; + continue; + } + + if (levelCount > 0) { + sb.append(' '); + } + sb.append(toString(elements[position])); + levelCount++; + position++; + } + + } + sb.append(')'); + + message = sb.toString(); + } + return message; + } + + + private static String toString(StackTraceElement element) { + if (element.getClassName().startsWith(STANDARD_PACKAGE_PREFIX)) { + return element.getFileName() + ":" + element.getLineNumber(); + } else { + return element.toString(); + } + } + +} diff --git a/core/src/net/sf/openrocket/masscalc/AbstractMassCalculator.java b/core/src/net/sf/openrocket/masscalc/AbstractMassCalculator.java new file mode 100644 index 00000000..30b53de6 --- /dev/null +++ b/core/src/net/sf/openrocket/masscalc/AbstractMassCalculator.java @@ -0,0 +1,53 @@ +package net.sf.openrocket.masscalc; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.startup.Application; + +/** + * Abstract base for mass calculators. Provides functionality for cacheing mass data. + * + * @author Sampo Niskanen + */ +public abstract class AbstractMassCalculator implements MassCalculator { + private static final LogHelper log = Application.getLogger(); + + private int rocketMassModID = -1; + private int rocketTreeModID = -1; + + + /** + * Check the current cache consistency. This method must be called by all + * methods that may use any cached data before any other operations are + * performed. If the rocket has changed since the previous call to + * checkCache(), then {@link #voidMassCache()} is called. + *

+ * This method performs the checking based on the rocket's modification IDs, + * so that these method may be called from listeners of the rocket itself. + * + * @param configuration the configuration of the current call + */ + protected final void checkCache(Configuration configuration) { + if (rocketMassModID != configuration.getRocket().getMassModID() || + rocketTreeModID != configuration.getRocket().getTreeModID()) { + rocketMassModID = configuration.getRocket().getMassModID(); + rocketTreeModID = configuration.getRocket().getTreeModID(); + log.debug("Voiding the mass cache"); + voidMassCache(); + } + } + + + + /** + * Void cached mass data. This method is called whenever a change occurs in + * the rocket structure that affects the mass of the rocket and when a new + * Rocket is used. This method must be overridden to void any cached data + * necessary. The method must call super.voidMassCache() during + * its execution. + */ + protected void voidMassCache() { + // No-op + } + +} diff --git a/core/src/net/sf/openrocket/masscalc/BasicMassCalculator.java b/core/src/net/sf/openrocket/masscalc/BasicMassCalculator.java new file mode 100644 index 00000000..4b5d8276 --- /dev/null +++ b/core/src/net/sf/openrocket/masscalc/BasicMassCalculator.java @@ -0,0 +1,350 @@ +package net.sf.openrocket.masscalc; + +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.motor.MotorInstanceConfiguration; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +public class BasicMassCalculator extends AbstractMassCalculator { + + private static final double MIN_MASS = 0.001 * MathUtil.EPSILON; + + + /* + * Cached data. All CG data is in absolute coordinates. All moments of inertia + * are relative to their respective CG. + */ + private Coordinate[] cgCache = null; + private double longitudinalInertiaCache[] = null; + private double rotationalInertiaCache[] = null; + + + + ////////////////// Mass property calculations /////////////////// + + + /** + * Return the CG of the rocket with the specified motor status (no motors, + * ignition, burnout). + */ + public Coordinate getCG(Configuration configuration, MassCalcType type) { + checkCache(configuration); + calculateStageCache(configuration); + + Coordinate totalCG = null; + + // Stage contribution + for (int stage : configuration.getActiveStages()) { + totalCG = cgCache[stage].average(totalCG); + } + + if (totalCG == null) + totalCG = Coordinate.NUL; + + // Add motor CGs + String motorId = configuration.getMotorConfigurationID(); + if (type != MassCalcType.NO_MOTORS && motorId != null) { + Iterator iterator = configuration.motorIterator(); + while (iterator.hasNext()) { + MotorMount mount = iterator.next(); + RocketComponent comp = (RocketComponent) mount; + Motor motor = mount.getMotor(motorId); + if (motor == null) + continue; + + Coordinate motorCG = type.getCG(motor).add(mount.getMotorPosition(motorId)); + Coordinate[] cgs = comp.toAbsolute(motorCG); + for (Coordinate cg : cgs) { + totalCG = totalCG.average(cg); + } + } + } + + return totalCG; + } + + + + + /** + * Return the CG of the rocket with the provided motor configuration. + */ + public Coordinate getCG(Configuration configuration, MotorInstanceConfiguration motors) { + checkCache(configuration); + calculateStageCache(configuration); + + Coordinate totalCG = getCG(configuration, MassCalcType.NO_MOTORS); + + // Add motor CGs + if (motors != null) { + for (MotorId id : motors.getMotorIDs()) { + int stage = ((RocketComponent) motors.getMotorMount(id)).getStageNumber(); + if (configuration.isStageActive(stage)) { + + MotorInstance motor = motors.getMotorInstance(id); + Coordinate position = motors.getMotorPosition(id); + Coordinate cg = motor.getCG().add(position); + totalCG = totalCG.average(cg); + + } + } + } + return totalCG; + } + + + /** + * Return the longitudinal inertia of the rocket with the specified motor instance + * configuration. + * + * @param configuration the current motor instance configuration + * @return the longitudinal inertia of the rocket + */ + public double getLongitudinalInertia(Configuration configuration, MotorInstanceConfiguration motors) { + checkCache(configuration); + calculateStageCache(configuration); + + final Coordinate totalCG = getCG(configuration, motors); + double totalInertia = 0; + + // Stages + for (int stage : configuration.getActiveStages()) { + Coordinate stageCG = cgCache[stage]; + + totalInertia += (longitudinalInertiaCache[stage] + + stageCG.weight * MathUtil.pow2(stageCG.x - totalCG.x)); + } + + + // Motors + if (motors != null) { + for (MotorId id : motors.getMotorIDs()) { + int stage = ((RocketComponent) motors.getMotorMount(id)).getStageNumber(); + if (configuration.isStageActive(stage)) { + MotorInstance motor = motors.getMotorInstance(id); + Coordinate position = motors.getMotorPosition(id); + Coordinate cg = motor.getCG().add(position); + + double inertia = motor.getLongitudinalInertia(); + totalInertia += inertia + cg.weight * MathUtil.pow2(cg.x - totalCG.x); + } + } + } + + return totalInertia; + } + + + + /** + * Return the rotational inertia of the rocket with the specified motor instance + * configuration. + * + * @param configuration the current motor instance configuration + * @return the rotational inertia of the rocket + */ + public double getRotationalInertia(Configuration configuration, MotorInstanceConfiguration motors) { + checkCache(configuration); + calculateStageCache(configuration); + + final Coordinate totalCG = getCG(configuration, motors); + double totalInertia = 0; + + // Stages + for (int stage : configuration.getActiveStages()) { + Coordinate stageCG = cgCache[stage]; + + totalInertia += (rotationalInertiaCache[stage] + + stageCG.weight * (MathUtil.pow2(stageCG.y - totalCG.y) + + MathUtil.pow2(stageCG.z - totalCG.z))); + } + + + // Motors + if (motors != null) { + for (MotorId id : motors.getMotorIDs()) { + int stage = ((RocketComponent) motors.getMotorMount(id)).getStageNumber(); + if (configuration.isStageActive(stage)) { + MotorInstance motor = motors.getMotorInstance(id); + Coordinate position = motors.getMotorPosition(id); + Coordinate cg = motor.getCG().add(position); + + double inertia = motor.getRotationalInertia(); + totalInertia += inertia + cg.weight * (MathUtil.pow2(cg.y - totalCG.y) + + MathUtil.pow2(cg.z - totalCG.z)); + } + } + } + + return totalInertia; + } + + + + @Override + public Map getCGAnalysis(Configuration configuration, MassCalcType type) { + checkCache(configuration); + calculateStageCache(configuration); + + Map map = new HashMap(); + + for (RocketComponent c : configuration) { + Coordinate[] cgs = c.toAbsolute(c.getCG()); + Coordinate totalCG = Coordinate.NUL; + for (Coordinate cg : cgs) { + totalCG = totalCG.average(cg); + } + map.put(c, totalCG); + } + + map.put(configuration.getRocket(), getCG(configuration, type)); + + return map; + } + + //////// Cache computations //////// + + private void calculateStageCache(Configuration config) { + if (cgCache == null) { + + int stages = config.getRocket().getStageCount(); + + cgCache = new Coordinate[stages]; + longitudinalInertiaCache = new double[stages]; + rotationalInertiaCache = new double[stages]; + + for (int i = 0; i < stages; i++) { + RocketComponent stage = config.getRocket().getChild(i); + MassData data = calculateAssemblyMassData(stage); + cgCache[i] = stage.toAbsolute(data.cg)[0]; + longitudinalInertiaCache[i] = data.longitudinalInertia; + rotationalInertiaCache[i] = data.rotationalInetria; + } + + } + } + + + + /** + * Returns the mass and inertia data for this component and all subcomponents. + * The inertia is returned relative to the CG, and the CG is in the coordinates + * of the specified component, not global coordinates. + */ + private MassData calculateAssemblyMassData(RocketComponent parent) { + MassData parentData = new MassData(); + + // Calculate data for this component + parentData.cg = parent.getComponentCG(); + if (parentData.cg.weight < MIN_MASS) + parentData.cg = parentData.cg.setWeight(MIN_MASS); + + + // Override only this component's data + if (!parent.getOverrideSubcomponents()) { + if (parent.isMassOverridden()) + parentData.cg = parentData.cg.setWeight(MathUtil.max(parent.getOverrideMass(), MIN_MASS)); + if (parent.isCGOverridden()) + parentData.cg = parentData.cg.setXYZ(parent.getOverrideCG()); + } + + parentData.longitudinalInertia = parent.getLongitudinalUnitInertia() * parentData.cg.weight; + parentData.rotationalInetria = parent.getRotationalUnitInertia() * parentData.cg.weight; + + + // Combine data for subcomponents + for (RocketComponent sibling : parent.getChildren()) { + Coordinate combinedCG; + double dx2, dr2; + + // Compute data of sibling + MassData siblingData = calculateAssemblyMassData(sibling); + Coordinate[] siblingCGs = sibling.toRelative(siblingData.cg, parent); + + for (Coordinate siblingCG : siblingCGs) { + + // Compute CG of this + sibling + combinedCG = parentData.cg.average(siblingCG); + + // Add effect of this CG change to parent inertia + dx2 = pow2(parentData.cg.x - combinedCG.x); + parentData.longitudinalInertia += parentData.cg.weight * dx2; + + dr2 = pow2(parentData.cg.y - combinedCG.y) + pow2(parentData.cg.z - combinedCG.z); + parentData.rotationalInetria += parentData.cg.weight * dr2; + + + // Add inertia of sibling + parentData.longitudinalInertia += siblingData.longitudinalInertia; + parentData.rotationalInetria += siblingData.rotationalInetria; + + // Add effect of sibling CG change + dx2 = pow2(siblingData.cg.x - combinedCG.x); + parentData.longitudinalInertia += siblingData.cg.weight * dx2; + + dr2 = pow2(siblingData.cg.y - combinedCG.y) + pow2(siblingData.cg.z - combinedCG.z); + parentData.rotationalInetria += siblingData.cg.weight * dr2; + + // Set combined CG + parentData.cg = combinedCG; + } + } + + // Override total data + if (parent.getOverrideSubcomponents()) { + if (parent.isMassOverridden()) { + double oldMass = parentData.cg.weight; + double newMass = MathUtil.max(parent.getOverrideMass(), MIN_MASS); + parentData.longitudinalInertia = parentData.longitudinalInertia * newMass / oldMass; + parentData.rotationalInetria = parentData.rotationalInetria * newMass / oldMass; + parentData.cg = parentData.cg.setWeight(newMass); + } + if (parent.isCGOverridden()) { + double oldx = parentData.cg.x; + double newx = parent.getOverrideCGX(); + parentData.longitudinalInertia += parentData.cg.weight * pow2(oldx - newx); + parentData.cg = parentData.cg.setX(newx); + } + } + + return parentData; + } + + + private static class MassData { + public Coordinate cg = Coordinate.NUL; + public double longitudinalInertia = 0; + public double rotationalInetria = 0; + } + + + @Override + protected void voidMassCache() { + super.voidMassCache(); + this.cgCache = null; + this.longitudinalInertiaCache = null; + this.rotationalInertiaCache = null; + } + + + + + @Override + public int getModID() { + return 0; + } + + + +} diff --git a/core/src/net/sf/openrocket/masscalc/MassCalculator.java b/core/src/net/sf/openrocket/masscalc/MassCalculator.java new file mode 100644 index 00000000..003650e1 --- /dev/null +++ b/core/src/net/sf/openrocket/masscalc/MassCalculator.java @@ -0,0 +1,88 @@ +package net.sf.openrocket.masscalc; + +import java.util.Map; + +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorInstanceConfiguration; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Monitorable; + +public interface MassCalculator extends Monitorable { + + public static enum MassCalcType { + NO_MOTORS { + @Override + public Coordinate getCG(Motor motor) { + return Coordinate.NUL; + } + }, + LAUNCH_MASS { + @Override + public Coordinate getCG(Motor motor) { + return motor.getLaunchCG(); + } + }, + BURNOUT_MASS { + @Override + public Coordinate getCG(Motor motor) { + return motor.getEmptyCG(); + } + }; + + public abstract Coordinate getCG(Motor motor); + } + + /** + * Compute the CG of the provided configuration. + * + * @param configuration the rocket configuration + * @param type the state of the motors (none, launch mass, burnout mass) + * @return the CG of the configuration + */ + public Coordinate getCG(Configuration configuration, MassCalcType type); + + /** + * Compute the CG of the provided configuration with specified motors. + * + * @param configuration the rocket configuration + * @param motors the motor configuration + * @return the CG of the configuration + */ + public Coordinate getCG(Configuration configuration, MotorInstanceConfiguration motors); + + /** + * Compute the longitudinal inertia of the provided configuration with specified motors. + * + * @param configuration the rocket configuration + * @param motors the motor configuration + * @return the longitudinal inertia of the configuration + */ + public double getLongitudinalInertia(Configuration configuration, MotorInstanceConfiguration motors); + + /** + * Compute the rotational inertia of the provided configuration with specified motors. + * + * @param configuration the rocket configuration + * @param motors the motor configuration + * @return the rotational inertia of the configuration + */ + public double getRotationalInertia(Configuration configuration, MotorInstanceConfiguration motors); + + + /** + * Compute an analysis of the per-component CG's of the provided configuration. + * The returned map will contain an entry for each physical rocket component (not stages) + * with its corresponding (best-effort) CG. Overriding of subcomponents is ignored. + * The CG of the entire configuration with motors is stored in the entry with the corresponding + * Rocket as the key. + * + * @param configuration the rocket configuration + * @param type the state of the motors (none, launch mass, burnout mass) + * @return a map from each rocket component to its corresponding CG. + */ + public Map getCGAnalysis(Configuration configuration, MassCalcType type); + + +} diff --git a/core/src/net/sf/openrocket/material/Material.java b/core/src/net/sf/openrocket/material/Material.java new file mode 100644 index 00000000..00be68b1 --- /dev/null +++ b/core/src/net/sf/openrocket/material/Material.java @@ -0,0 +1,236 @@ +package net.sf.openrocket.material; + +import net.sf.openrocket.unit.Unit; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.MathUtil; + +/** + * A class for different material types. Each material has a name and density. + * The interpretation of the density depends on the material type. For + * {@link Type#BULK} it is kg/m^3, for {@link Type#SURFACE} km/m^2. + *

+ * Objects of this type are immutable. + * + * @author Sampo Niskanen + */ + +public abstract class Material implements Comparable { + + public enum Type { + LINE("Line", UnitGroup.UNITS_DENSITY_LINE), + SURFACE("Surface", UnitGroup.UNITS_DENSITY_SURFACE), + BULK("Bulk", UnitGroup.UNITS_DENSITY_BULK); + + private final String name; + private final UnitGroup units; + + private Type(String name, UnitGroup units) { + this.name = name; + this.units = units; + } + + public UnitGroup getUnitGroup() { + return units; + } + + @Override + public String toString() { + return name; + } + } + + + ///// Definitions of different material types ///// + + public static class Line extends Material { + public Line(String name, double density, boolean userDefined) { + super(name, density, userDefined); + } + + @Override + public Type getType() { + return Type.LINE; + } + } + + public static class Surface extends Material { + + public Surface(String name, double density, boolean userDefined) { + super(name, density, userDefined); + } + + @Override + public Type getType() { + return Type.SURFACE; + } + + @Override + public String toStorableString() { + return super.toStorableString(); + } + } + + public static class Bulk extends Material { + public Bulk(String name, double density, boolean userDefined) { + super(name, density, userDefined); + } + + @Override + public Type getType() { + return Type.BULK; + } + } + + + + private final String name; + private final double density; + private final boolean userDefined; + + + public Material(String name, double density, boolean userDefined) { + this.name = name; + this.density = density; + this.userDefined = userDefined; + } + + + + public double getDensity() { + return density; + } + + public String getName() { + return name; + } + + public String getName(Unit u) { + return name + " (" + u.toStringUnit(density) + ")"; + } + + public boolean isUserDefined() { + return userDefined; + } + + public abstract Type getType(); + + @Override + public String toString() { + return this.getName(this.getType().getUnitGroup().getDefaultUnit()); + } + + + /** + * Compares this object to another object. Material objects are equal if and only if + * their types, names and densities are identical. + */ + @Override + public boolean equals(Object o) { + if (o == null) + return false; + if (this.getClass() != o.getClass()) + return false; + Material m = (Material) o; + return ((m.name.equals(this.name)) && MathUtil.equals(m.density, this.density)); + } + + + /** + * A hashCode() method giving a hash code compatible with the equals() method. + */ + @Override + public int hashCode() { + return name.hashCode() + (int) (density * 1000); + } + + + /** + * Order the materials according to their name, secondarily according to density. + */ + @Override + public int compareTo(Material o) { + int c = this.name.compareTo(o.name); + if (c != 0) { + return c; + } else { + return (int) ((this.density - o.density) * 1000); + } + } + + + /** + * Return a new material of the specified type. + */ + public static Material newMaterial(Type type, String name, double density, + boolean userDefined) { + switch (type) { + case LINE: + return new Material.Line(name, density, userDefined); + + case SURFACE: + return new Material.Surface(name, density, userDefined); + + case BULK: + return new Material.Bulk(name, density, userDefined); + + default: + throw new IllegalArgumentException("Unknown material type: " + type); + } + } + + + public String toStorableString() { + return getType().name() + "|" + name.replace('|', ' ') + '|' + density; + } + + + /** + * Return a material defined by the provided string. + * + * @param str the material storage string. + * @param userDefined whether the created material is user-defined. + * @return a new Material object. + * @throws IllegalArgumentException if str is invalid or null. + */ + public static Material fromStorableString(String str, boolean userDefined) { + if (str == null) + throw new IllegalArgumentException("Material string is null"); + + String[] split = str.split("\\|", 3); + if (split.length < 3) + throw new IllegalArgumentException("Illegal material string: " + str); + + Type type = null; + String name; + double density; + + try { + type = Type.valueOf(split[0]); + } catch (Exception e) { + throw new IllegalArgumentException("Illegal material string: " + str, e); + } + + name = split[1]; + + try { + density = Double.parseDouble(split[2]); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Illegal material string: " + str, e); + } + + switch (type) { + case BULK: + return new Material.Bulk(name, density, userDefined); + + case SURFACE: + return new Material.Surface(name, density, userDefined); + + case LINE: + return new Material.Line(name, density, userDefined); + + default: + throw new IllegalArgumentException("Illegal material string: " + str); + } + } + +} diff --git a/core/src/net/sf/openrocket/material/MaterialStorage.java b/core/src/net/sf/openrocket/material/MaterialStorage.java new file mode 100644 index 00000000..0d7f11b4 --- /dev/null +++ b/core/src/net/sf/openrocket/material/MaterialStorage.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.material; + +import net.sf.openrocket.database.Database; +import net.sf.openrocket.database.DatabaseListener; +import net.sf.openrocket.startup.Application; + +/** + * Class for storing changes to user-added materials. The materials are stored to + * the OpenRocket preferences. + * + * @author Sampo Niskanen + */ +public class MaterialStorage implements DatabaseListener { + + @Override + public void elementAdded(Material material, Database source) { + if (material.isUserDefined()) { + Application.getPreferences().addUserMaterial(material); + } + } + + @Override + public void elementRemoved(Material material, Database source) { + Application.getPreferences().removeUserMaterial(material); + } + +} diff --git a/core/src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java b/core/src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java new file mode 100644 index 00000000..a988ac8e --- /dev/null +++ b/core/src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java @@ -0,0 +1,151 @@ +package net.sf.openrocket.models.atmosphere; + +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.UniqueID; + +public class AtmosphericConditions implements Cloneable, Monitorable { + + /** Specific gas constant of dry air. */ + public static final double R = 287.053; + + /** Specific heat ratio of air. */ + public static final double GAMMA = 1.4; + + /** The standard air pressure (1.01325 bar). */ + public static final double STANDARD_PRESSURE = 101325.0; + + /** The standard air temperature (20 degrees Celcius). */ + public static final double STANDARD_TEMPERATURE = 293.15; + + + + /** Air pressure, in Pascals. */ + private double pressure; + + /** Air temperature, in Kelvins. */ + private double temperature; + + private int modID; + + + /** + * Construct standard atmospheric conditions. + */ + public AtmosphericConditions() { + this(STANDARD_TEMPERATURE, STANDARD_PRESSURE); + } + + /** + * Construct specified atmospheric conditions. + * + * @param temperature the temperature in Kelvins. + * @param pressure the pressure in Pascals. + */ + public AtmosphericConditions(double temperature, double pressure) { + this.setTemperature(temperature); + this.setPressure(pressure); + this.modID = UniqueID.next(); + } + + + + public double getPressure() { + return pressure; + } + + public void setPressure(double pressure) { + this.pressure = pressure; + this.modID = UniqueID.next(); + } + + public double getTemperature() { + return temperature; + } + + public void setTemperature(double temperature) { + this.temperature = temperature; + this.modID = UniqueID.next(); + } + + /** + * Return the current density of air for dry air. + * + * @return the current density of air. + */ + public double getDensity() { + return getPressure() / (R*getTemperature()); + } + + + /** + * Return the current speed of sound for dry air. + *

+ * The speed of sound is calculated using the expansion around the temperature 0 C + * c = 331.3 + 0.606*T where T is in Celcius. The result is accurate + * to about 0.5 m/s for temperatures between -30 and 30 C, and within 2 m/s + * for temperatures between -55 and 30 C. + * + * @return the current speed of sound. + */ + public double getMachSpeed() { + return 165.77 + 0.606 * getTemperature(); + } + + + /** + * Return the current kinematic viscosity of the air. + *

+ * The effect of temperature on the viscosity of a gas can be computed using + * Sutherland's formula. In the region of -40 ... 40 degrees Celcius the effect + * is highly linear, and thus a linear approximation is used in its stead. + * This is divided by the result of {@link #getDensity()} to achieve the + * kinematic viscosity. + * + * @return the current kinematic viscosity. + */ + public double getKinematicViscosity() { + double v = 3.7291e-06 + 4.9944e-08 * getTemperature(); + return v / getDensity(); + } + + + /** + * Return a copy of the atmospheric conditions. + */ + @Override + public AtmosphericConditions clone() { + try { + return (AtmosphericConditions) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException encountered!"); + } + } + + @Override + public boolean equals(Object other) { + if (this == other) + return true; + if (!(other instanceof AtmosphericConditions)) + return false; + AtmosphericConditions o = (AtmosphericConditions) other; + return MathUtil.equals(this.pressure, o.pressure) && MathUtil.equals(this.temperature, o.temperature); + } + + @Override + public int hashCode() { + return (int) (this.pressure + this.temperature*1000); + } + + @Override + public int getModID() { + return modID; + } + + @Override + public String toString() { + return String.format("AtmosphericConditions[T=%.2f,P=%.2f]", getTemperature(), getPressure()); + } + +} diff --git a/core/src/net/sf/openrocket/models/atmosphere/AtmosphericModel.java b/core/src/net/sf/openrocket/models/atmosphere/AtmosphericModel.java new file mode 100644 index 00000000..6a4eec84 --- /dev/null +++ b/core/src/net/sf/openrocket/models/atmosphere/AtmosphericModel.java @@ -0,0 +1,9 @@ +package net.sf.openrocket.models.atmosphere; + +import net.sf.openrocket.util.Monitorable; + +public interface AtmosphericModel extends Monitorable { + + public AtmosphericConditions getConditions(double altitude); + +} diff --git a/core/src/net/sf/openrocket/models/atmosphere/ExtendedISAModel.java b/core/src/net/sf/openrocket/models/atmosphere/ExtendedISAModel.java new file mode 100644 index 00000000..6b7eb2b7 --- /dev/null +++ b/core/src/net/sf/openrocket/models/atmosphere/ExtendedISAModel.java @@ -0,0 +1,128 @@ +package net.sf.openrocket.models.atmosphere; + +import static net.sf.openrocket.models.atmosphere.AtmosphericConditions.R; +import net.sf.openrocket.util.MathUtil; + + +/** + * An atmospheric temperature/pressure model based on the International Standard Atmosphere + * (ISA). The no-argument constructor creates an {@link AtmosphericModel} that corresponds + * to the ISA model. It is extended by the other constructors to allow defining a custom + * first layer. The base temperature and pressure are as given, and all other values + * are calculated based on these. + *

+ * TODO: LOW: Values at altitudes over 32km differ from standard results by ~5%. + * + * @author Sampo Niskanen + */ +public class ExtendedISAModel extends InterpolatingAtmosphericModel { + + public static final double STANDARD_TEMPERATURE = 288.15; + public static final double STANDARD_PRESSURE = 101325; + + private static final double G = 9.80665; + + private final double[] layer = { 0, 11000, 20000, 32000, 47000, 51000, 71000, 84852 }; + private final double[] baseTemperature = { + 288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.95 + }; + private final double[] basePressure = new double[layer.length]; + + + /** + * Construct the standard ISA model. + */ + public ExtendedISAModel() { + this(STANDARD_TEMPERATURE, STANDARD_PRESSURE); + } + + /** + * Construct an extended model with the given temperature and pressure at MSL. + * + * @param temperature the temperature at MSL. + * @param pressure the pressure at MSL. + */ + public ExtendedISAModel(double temperature, double pressure) { + this(0, temperature, pressure); + } + + + /** + * Construct an extended model with the given temperature and pressure at the + * specified altitude. Conditions below the given altitude cannot be calculated, + * and the values at the specified altitude will be returned instead. The altitude + * must be lower than the altitude of the next ISA standard layer (below 11km). + * + * @param altitude the altitude of the measurements. + * @param temperature the temperature. + * @param pressure the pressure. + * @throws IllegalArgumentException if the altitude exceeds the second layer boundary + * of the ISA model (over 11km). + */ + public ExtendedISAModel(double altitude, double temperature, double pressure) { + if (altitude >= layer[1]) { + throw new IllegalArgumentException("Too high first altitude: " + altitude); + } + + layer[0] = altitude; + baseTemperature[0] = temperature; + basePressure[0] = pressure; + + for (int i = 1; i < basePressure.length; i++) { + basePressure[i] = getExactConditions(layer[i] - 1).getPressure(); + } + } + + + @Override + protected AtmosphericConditions getExactConditions(double altitude) { + altitude = MathUtil.clamp(altitude, layer[0], layer[layer.length - 1]); + int n; + for (n = 0; n < layer.length - 1; n++) { + if (layer[n + 1] > altitude) + break; + } + + double rate = (baseTemperature[n + 1] - baseTemperature[n]) / (layer[n + 1] - layer[n]); + + double t = baseTemperature[n] + (altitude - layer[n]) * rate; + double p; + if (Math.abs(rate) > 0.001) { + p = basePressure[n] * + Math.pow(1 + (altitude - layer[n]) * rate / baseTemperature[n], -G / (rate * R)); + } else { + p = basePressure[n] * + Math.exp(-(altitude - layer[n]) * G / (R * baseTemperature[n])); + } + + return new AtmosphericConditions(t, p); + } + + @Override + protected double getMaxAltitude() { + return layer[layer.length - 1]; + } + + + public static void main(String foo[]) { + ExtendedISAModel model1 = new ExtendedISAModel(); + ExtendedISAModel model2 = new ExtendedISAModel(278.15, 100000); + + for (double alt = 0; alt < 80000; alt += 500) { + AtmosphericConditions cond1 = model1.getConditions(alt); + AtmosphericConditions cond2 = model2.getConditions(alt); + + AtmosphericConditions diff = new AtmosphericConditions(); + diff.setPressure((cond2.getPressure() - cond1.getPressure()) / cond1.getPressure() * 100); + diff.setTemperature((cond2.getTemperature() - cond1.getTemperature()) / cond1.getTemperature() * 100); + System.out.println("alt=" + alt + + ": std:" + cond1 + " mod:" + cond2 + " diff:" + diff); + } + } + + @Override + public int getModID() { + return 0; + } + +} diff --git a/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java b/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java new file mode 100644 index 00000000..6a416a9a --- /dev/null +++ b/core/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java @@ -0,0 +1,48 @@ +package net.sf.openrocket.models.atmosphere; + +/** + * An abstract atmospheric model that pre-computes the conditions on a number of layers + * and later linearly interpolates the values from between these layers. + * + * @author Sampo Niskanen + */ +public abstract class InterpolatingAtmosphericModel implements AtmosphericModel { + /** Layer thickness of interpolated altitude. */ + private static final double DELTA = 500; + + private AtmosphericConditions[] levels = null; + + + public AtmosphericConditions getConditions(double altitude) { + if (levels == null) + computeLayers(); + + if (altitude <= 0) + return levels[0]; + if (altitude >= DELTA * (levels.length - 1)) + return levels[levels.length - 1]; + + int n = (int) (altitude / DELTA); + double d = (altitude - n * DELTA) / DELTA; + AtmosphericConditions c = new AtmosphericConditions(); + c.setTemperature(levels[n].getTemperature() * (1 - d) + levels[n + 1].getTemperature() * d); + c.setPressure(levels[n].getPressure() * (1 - d) + levels[n + 1].getPressure() * d); + + return c; + } + + + private void computeLayers() { + double max = getMaxAltitude(); + int n = (int) (max / DELTA) + 1; + levels = new AtmosphericConditions[n]; + for (int i = 0; i < n; i++) { + levels[i] = getExactConditions(i * DELTA); + } + } + + + protected abstract double getMaxAltitude(); + + protected abstract AtmosphericConditions getExactConditions(double altitude); +} diff --git a/core/src/net/sf/openrocket/models/gravity/GravityModel.java b/core/src/net/sf/openrocket/models/gravity/GravityModel.java new file mode 100644 index 00000000..6bee52a7 --- /dev/null +++ b/core/src/net/sf/openrocket/models/gravity/GravityModel.java @@ -0,0 +1,21 @@ +package net.sf.openrocket.models.gravity; + +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.WorldCoordinate; + +/** + * An interface for modeling gravitational acceleration. + * + * @author Sampo Niskanen + */ +public interface GravityModel extends Monitorable { + + /** + * Compute the gravitational acceleration at a given world coordinate + * + * @param wc the world coordinate location + * @return gravitational acceleration in m/s/s + */ + public double getGravity(WorldCoordinate wc); + +} diff --git a/core/src/net/sf/openrocket/models/gravity/WGSGravityModel.java b/core/src/net/sf/openrocket/models/gravity/WGSGravityModel.java new file mode 100644 index 00000000..41fb5c23 --- /dev/null +++ b/core/src/net/sf/openrocket/models/gravity/WGSGravityModel.java @@ -0,0 +1,52 @@ +package net.sf.openrocket.models.gravity; + +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.WorldCoordinate; + +/** + * A gravity model based on the WGS84 ellipsoid. + * + * @author Richard Graham + */ +public class WGSGravityModel implements GravityModel { + + // Cache the previously computed value + private WorldCoordinate lastWorldCoordinate; + private double lastg; + + + @Override + public double getGravity(WorldCoordinate wc) { + + // This is a proxy method to calcGravity, to avoid repeated calculation + if (wc != this.lastWorldCoordinate) { + this.lastg = calcGravity(wc); + this.lastWorldCoordinate = wc; + } + + return this.lastg; + + } + + + @Override + public int getModID() { + // The model is immutable, so it can return a constant mod ID + return 0; + } + + + private double calcGravity(WorldCoordinate wc) { + + double sin2lat = MathUtil.pow2(Math.sin(wc.getLatitudeRad())); + double g_0 = 9.7803267714 * ((1.0 + 0.00193185138639 * sin2lat) / Math.sqrt(1.0 - 0.00669437999013 * sin2lat)); + + // Apply correction due to altitude. Note this assumes a spherical earth, but it is a small correction + // so it probably doesn't really matter. Also does not take into account gravity of the atmosphere, again + // correction could be done but not really necessary. + double g_alt = g_0 * MathUtil.pow2(WorldCoordinate.REARTH / (WorldCoordinate.REARTH + wc.getAltitude())); + + return g_alt; + } + +} diff --git a/core/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java b/core/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java new file mode 100644 index 00000000..960325d5 --- /dev/null +++ b/core/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java @@ -0,0 +1,169 @@ +package net.sf.openrocket.models.wind; + +import java.util.Random; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.PinkNoise; + +/** + * A wind simulator that generates wind speed as pink noise from a specified average wind speed + * and standard deviance. Currently the wind is always directed in the direction of the negative + * X-axis. The simulated wind is unaffected by the altitude. + * + * @author Sampo Niskanen + */ +public class PinkNoiseWindModel implements WindModel { + + /** Random value with which to XOR the random seed value */ + private static final int SEED_RANDOMIZATION = 0x7343AA03; + + + + /** Pink noise alpha parameter. */ + private static final double ALPHA = 5.0 / 3.0; + + /** Number of poles to use in the pink noise IIR filter. */ + private static final int POLES = 2; + + /** The standard deviation of the generated pink noise with the specified number of poles. */ + private static final double STDDEV = 2.252; + + /** Time difference between random samples. */ + private static final double DELTA_T = 0.05; + + + private double average = 0; + private double standardDeviation = 0; + + private final int seed; + + private PinkNoise randomSource = null; + private double time1; + private double value1, value2; + + + /** + * Construct a new wind simulation with a specific seed value. + * @param seed the seed value. + */ + public PinkNoiseWindModel(int seed) { + this.seed = seed ^ SEED_RANDOMIZATION; + } + + + + /** + * Return the average wind speed. + * + * @return the average wind speed. + */ + public double getAverage() { + return average; + } + + /** + * Set the average wind speed. This method will also modify the + * standard deviation such that the turbulence intensity remains constant. + * + * @param average the average wind speed to set + */ + public void setAverage(double average) { + double intensity = getTurbulenceIntensity(); + this.average = Math.max(average, 0); + setTurbulenceIntensity(intensity); + } + + + + /** + * Return the standard deviation from the average wind speed. + * + * @return the standard deviation of the wind speed + */ + public double getStandardDeviation() { + return standardDeviation; + } + + /** + * Set the standard deviation of the average wind speed. + * + * @param standardDeviation the standardDeviation to set + */ + public void setStandardDeviation(double standardDeviation) { + this.standardDeviation = Math.max(standardDeviation, 0); + } + + + /** + * Return the turbulence intensity (standard deviation / average). + * + * @return the turbulence intensity + */ + public double getTurbulenceIntensity() { + if (MathUtil.equals(average, 0)) { + if (MathUtil.equals(standardDeviation, 0)) + return 0; + else + return 1000; + } + return standardDeviation / average; + } + + /** + * Set the standard deviation to match the turbulence intensity. + * + * @param intensity the turbulence intensity + */ + public void setTurbulenceIntensity(double intensity) { + setStandardDeviation(intensity * average); + } + + + + + + @Override + public Coordinate getWindVelocity(double time, double altitude) { + if (time < 0) { + throw new IllegalArgumentException("Requesting wind speed at t=" + time); + } + + if (randomSource == null) { + randomSource = new PinkNoise(ALPHA, POLES, new Random(seed)); + time1 = 0; + value1 = randomSource.nextValue(); + value2 = randomSource.nextValue(); + } + + if (time < time1) { + reset(); + return getWindVelocity(time, altitude); + } + + while (time1 + DELTA_T < time) { + value1 = value2; + value2 = randomSource.nextValue(); + time1 += DELTA_T; + } + + double a = (time - time1) / DELTA_T; + + double speed = average + (value1 * (1 - a) + value2 * a) * standardDeviation / STDDEV; + // TODO: MEDIUM: Make wind direction configurable + return new Coordinate(speed, 0, 0); + } + + + private void reset() { + randomSource = null; + } + + + + @Override + public int getModID() { + return (int) (average * 1000 + standardDeviation); + } + +} diff --git a/core/src/net/sf/openrocket/models/wind/WindModel.java b/core/src/net/sf/openrocket/models/wind/WindModel.java new file mode 100644 index 00000000..3f78f102 --- /dev/null +++ b/core/src/net/sf/openrocket/models/wind/WindModel.java @@ -0,0 +1,10 @@ +package net.sf.openrocket.models.wind; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Monitorable; + +public interface WindModel extends Monitorable { + + public Coordinate getWindVelocity(double time, double altitude); + +} diff --git a/core/src/net/sf/openrocket/motor/DesignationComparator.java b/core/src/net/sf/openrocket/motor/DesignationComparator.java new file mode 100644 index 00000000..21489cb7 --- /dev/null +++ b/core/src/net/sf/openrocket/motor/DesignationComparator.java @@ -0,0 +1,77 @@ +package net.sf.openrocket.motor; + +import java.text.Collator; +import java.util.Comparator; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Compares two motors designations. The motors are ordered first + * by their motor class, second by their average thrust and lastly by any + * extra modifiers at the end of the designation. + * + * @author Sampo Niskanen + */ +public class DesignationComparator implements Comparator { + private static final Collator COLLATOR; + static { + COLLATOR = Collator.getInstance(Locale.US); + COLLATOR.setStrength(Collator.PRIMARY); + } + + private Pattern pattern = + Pattern.compile("^([0-9][0-9]+|1/([1-8]))?([a-zA-Z])([0-9]+)(.*?)$"); + + @Override + public int compare(String o1, String o2) { + int value; + Matcher m1, m2; + + m1 = pattern.matcher(o1); + m2 = pattern.matcher(o2); + + if (m1.find() && m2.find()) { + + String o1Class = m1.group(3); + int o1Thrust = Integer.parseInt(m1.group(4)); + String o1Extra = m1.group(5); + + String o2Class = m2.group(3); + int o2Thrust = Integer.parseInt(m2.group(4)); + String o2Extra = m2.group(5); + + // 1. Motor class + if (o1Class.equalsIgnoreCase("A") && o2Class.equalsIgnoreCase("A")) { + // 1/2A and 1/4A comparison + String sub1 = m1.group(2); + String sub2 = m2.group(2); + + if (sub1 != null || sub2 != null) { + if (sub1 == null) + sub1 = "1"; + if (sub2 == null) + sub2 = "1"; + value = -COLLATOR.compare(sub1,sub2); + if (value != 0) + return value; + } + } + value = COLLATOR.compare(o1Class,o2Class); + if (value != 0) + return value; + + // 2. Average thrust + if (o1Thrust != o2Thrust) + return o1Thrust - o2Thrust; + + // 3. Extra modifier + return COLLATOR.compare(o1Extra, o2Extra); + + } else { + + // Not understandable designation, simply compare strings + return COLLATOR.compare(o1, o2); + } + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/motor/Manufacturer.java b/core/src/net/sf/openrocket/motor/Manufacturer.java new file mode 100644 index 00000000..831181bb --- /dev/null +++ b/core/src/net/sf/openrocket/motor/Manufacturer.java @@ -0,0 +1,249 @@ +package net.sf.openrocket.motor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Class containing information about motor manufacturers. + * + * @author Sampo Niskanen + */ +public class Manufacturer { + + private static Set manufacturers = new HashSet(); + static { + + // AeroTech has many name combinations... + List names = new ArrayList(); + for (String s : new String[] { "A", "AT", "AERO", "AEROT", "AEROTECH" }) { + names.add(s); + names.add(s + "-RMS"); + names.add(s + "-RCS"); + names.add("RCS-" + s); + names.add(s + "-APOGEE"); + } + names.add("ISP"); + + // Aerotech has single-use, reload and hybrid motors + manufacturers.add(new Manufacturer("AeroTech", "AeroTech", Motor.Type.UNKNOWN, + names.toArray(new String[0]))); + + manufacturers.add(new Manufacturer("Alpha Hybrid Rocketry LLC", "Alpha Hybrid Rocketry", Motor.Type.HYBRID, + "AHR", "ALPHA", "ALPHA HYBRID", "ALPHA HYBRIDS", "ALPHA HYBRIDS ROCKETRY")); + + // TODO: HIGH: AMW/ProX - how to classify? + + manufacturers.add(new Manufacturer("Animal Motor Works", "Animal Motor Works", Motor.Type.RELOAD, + "AMW", "AW", "ANIMAL")); + + manufacturers.add(new Manufacturer("Apogee", "Apogee", Motor.Type.SINGLE, + "AP", "APOG", "P")); + + manufacturers.add(new Manufacturer("Cesaroni Technology Inc.", "Cesaroni Technology", Motor.Type.RELOAD, + "CES", "CESARONI", "CESARONI TECHNOLOGY INCORPORATED", "CTI", + "CS", "CSR", "PRO38", "ABC")); + + manufacturers.add(new Manufacturer("Contrail Rockets", "Contrail Rockets", Motor.Type.HYBRID, + "CR", "CONTR", "CONTRAIL", "CONTRAIL ROCKET")); + + manufacturers.add(new Manufacturer("Estes", "Estes", Motor.Type.SINGLE, + "E", "ES")); + + // Ellis Mountain has both single-use and reload motors + manufacturers.add(new Manufacturer("Ellis Mountain", "Ellis Mountain", Motor.Type.UNKNOWN, + "EM", "ELLIS", "ELLIS MOUNTAIN ROCKET", "ELLIS MOUNTAIN ROCKETS")); + + manufacturers.add(new Manufacturer("Gorilla Rocket Motors", "Gorilla Rocket Motors", Motor.Type.RELOAD, + "GR", "GORILLA", "GORILLA ROCKET", "GORILLA ROCKETS", "GORILLA MOTOR", + "GORILLA MOTORS", "GORILLA ROCKET MOTOR")); + + manufacturers.add(new Manufacturer("HyperTEK", "HyperTEK", Motor.Type.HYBRID, + "H", "HT", "HYPER")); + + manufacturers.add(new Manufacturer("Kosdon by AeroTech", "Kosdon by AeroTech", Motor.Type.RELOAD, + "K", "KBA", "K-AT", "KOS", "KOSDON", "KOSDON/AT", "KOSDON/AEROTECH")); + + manufacturers.add(new Manufacturer("Loki Research", "Loki Research", Motor.Type.RELOAD, + "LOKI", "LR")); + + manufacturers.add(new Manufacturer("Public Missiles, Ltd.", "Public Missiles", Motor.Type.SINGLE, + "PM", "PML", "PUBLIC MISSILES LIMITED")); + + manufacturers.add(new Manufacturer("Propulsion Polymers", "Propulsion Polymers", Motor.Type.HYBRID, + "PP", "PROP", "PROPULSION")); + + manufacturers.add(new Manufacturer("Quest", "Quest", Motor.Type.SINGLE, + "Q", "QU")); + + manufacturers.add(new Manufacturer("RATT Works", "RATT Works", Motor.Type.HYBRID, + "RATT", "RT", "RTW")); + + manufacturers.add(new Manufacturer("Roadrunner Rocketry", "Roadrunner Rocketry", Motor.Type.SINGLE, + "RR", "ROADRUNNER")); + + manufacturers.add(new Manufacturer("Rocketvision", "Rocketvision", Motor.Type.SINGLE, + "RV", "ROCKET VISION")); + + manufacturers.add(new Manufacturer("Sky Ripper Systems", "Sky Ripper Systems", Motor.Type.HYBRID, + "SR", "SRS", "SKYR", "SKYRIPPER", "SKY RIPPER", "SKYRIPPER SYSTEMS")); + + manufacturers.add(new Manufacturer("West Coast Hybrids", "West Coast Hybrids", Motor.Type.HYBRID, + "WCH", "WCR", "WEST COAST", "WEST COAST HYBRID")); + + // German WECO Feuerwerk, previously Sachsen Feuerwerk + manufacturers.add(new Manufacturer("WECO Feuerwerk", "WECO Feuerwerk", Motor.Type.SINGLE, + "WECO", "WECO FEUERWERKS", "SF", "SACHSEN", "SACHSEN FEUERWERK", + "SACHSEN FEUERWERKS")); + + + // Check that no duplicates have appeared + for (Manufacturer m1 : manufacturers) { + for (Manufacturer m2 : manufacturers) { + if (m1 == m2) + continue; + for (String name : m1.getAllNames()) { + if (m2.matches(name)) { + throw new IllegalStateException("Manufacturer name clash between " + + "manufacturers " + m1 + " and " + m2 + " name " + name); + } + } + } + } + } + + + + private final String displayName; + private final String simpleName; + private final Set allNames; + private final Set searchNames; + private final Motor.Type motorType; + + + private Manufacturer(String displayName, String simpleName, Motor.Type motorType, String... alternateNames) { + this.displayName = displayName; + this.simpleName = simpleName; + this.motorType = motorType; + if (motorType == null) { + throw new IllegalArgumentException("motorType cannot be null"); + } + + Set all = new HashSet(); + Set search = new HashSet(); + + all.add(displayName); + all.add(simpleName); + search.add(generateSearchString(displayName)); + search.add(generateSearchString(simpleName)); + + for (String name : alternateNames) { + all.add(name); + search.add(generateSearchString(name)); + } + + this.allNames = Collections.unmodifiableSet(all); + this.searchNames = Collections.unmodifiableSet(search); + } + + + /** + * Returns the display name of the manufacturer. This is the value that + * should be presented in the UI to the user. + * + * @return the display name + */ + public String getDisplayName() { + return displayName; + } + + + /** + * Returns the simple name of the manufacturer. This should be used for example + * when saving the manufacturer for compatibility. + * + * @return the simple name. + */ + public String getSimpleName() { + return simpleName; + } + + + /** + * Return all names of the manufacturer. This includes all kinds of + * codes that correspond to the manufacturer (for example "A" for AeroTech). + * + * @return an unmodifiable set of the alternative names. + */ + public Set getAllNames() { + return allNames; + } + + + /** + * Return the motor type that this manufacturer produces if it produces only one motor type. + * If the manufacturer manufactures multiple motor types or the type is unknown, + * type UNKNOWN is returned. + * + * @return the manufactured motor type, or UNKNOWN. + */ + public Motor.Type getMotorType() { + return motorType; + } + + + /** + * Check whether the display, simple or any of the alternative names matches the + * specified name. Matching is performed case insensitively and ignoring any + * non-letter and non-number characters. + * + * @param name the name to search for. + * @return whether this manufacturer matches the request. + */ + public boolean matches(String name) { + if (name == null) + return false; + return this.searchNames.contains(generateSearchString(name)); + } + + + /** + * Return the display name of the manufacturer. + */ + @Override + public String toString() { + return displayName; + } + + + /** + * Returns a manufacturer for the given name. The manufacturer is searched for + * within the manufacturers and if a match is found the corresponding + * object is returned. If not, a new manufacturer is returned with display and + * simple name the name specified. Subsequent requests for the same (or corresponding) + * manufacturer name will return the same object. + * + * @param name the manufacturer name to search for. + * @return the Manufacturer object corresponding the name. + */ + public static synchronized Manufacturer getManufacturer(String name) { + for (Manufacturer m : manufacturers) { + if (m.matches(name)) + return m; + } + + Manufacturer m = new Manufacturer(name.trim(), name.trim(), Motor.Type.UNKNOWN); + manufacturers.add(m); + return m; + } + + + + + private String generateSearchString(String str) { + return str.toLowerCase().replaceAll("[^a-zA-Z0-9]+", " ").trim(); + } + +} diff --git a/core/src/net/sf/openrocket/motor/Motor.java b/core/src/net/sf/openrocket/motor/Motor.java new file mode 100644 index 00000000..fda0eebb --- /dev/null +++ b/core/src/net/sf/openrocket/motor/Motor.java @@ -0,0 +1,133 @@ +package net.sf.openrocket.motor; + +import net.sf.openrocket.util.Coordinate; + +public interface Motor { + + /** + * Enum of rocket motor types. + * + * @author Sampo Niskanen + */ + public enum Type { + SINGLE("Single-use", "Single-use solid propellant motor"), + RELOAD("Reloadable", "Reloadable solid propellant motor"), + HYBRID("Hybrid", "Hybrid rocket motor engine"), + UNKNOWN("Unknown", "Unknown motor type"); + + private final String name; + private final String description; + + Type(String name, String description) { + this.name = name; + this.description = description; + } + + /** + * Return a short name of this motor type. + * @return a short name of the motor type. + */ + public String getName() { + return name; + } + + /** + * Return a long description of this motor type. + * @return a description of the motor type. + */ + public String getDescription() { + return description; + } + + @Override + public String toString() { + return name; + } + } + + + /** + * Ejection charge delay value signifying a "plugged" motor with no ejection charge. + * The value is that of Double.POSITIVE_INFINITY. + */ + public static final double PLUGGED = Double.POSITIVE_INFINITY; + + + /** + * Below what portion of maximum thrust is the motor chosen to be off when + * calculating average thrust and burn time. NFPA 1125 defines the "official" + * burn time to be the time which the motor produces over 5% of its maximum thrust. + */ + public static final double MARGINAL_THRUST = 0.05; + + + + /** + * Return the motor type. + * + * @return the motorType + */ + public Type getMotorType(); + + + /** + * Return the designation of the motor. + * + * @return the designation + */ + public String getDesignation(); + + /** + * Return the designation of the motor, including a delay. + * + * @param delay the delay of the motor. + * @return designation with delay. + */ + public String getDesignation(double delay); + + + /** + * Return extra description for the motor. This may include for example + * comments on the source of the thrust curve. The returned String + * may include new-lines. + * + * @return the description + */ + public String getDescription(); + + + /** + * Return the maximum diameter of the motor. + * + * @return the diameter + */ + public double getDiameter(); + + /** + * Return the length of the motor. This should be a "characteristic" length, + * and the exact definition may depend on the motor type. Typically this should + * be the length from the bottom of the motor to the end of the maximum diameter + * portion, ignoring any smaller ejection charge compartments. + * + * @return the length + */ + public double getLength(); + + + public MotorInstance getInstance(); + + + public Coordinate getLaunchCG(); + public Coordinate getEmptyCG(); + + + public double getBurnTimeEstimate(); + public double getAverageThrustEstimate(); + public double getMaxThrustEstimate(); + public double getTotalImpulseEstimate(); + + public double[] getTimePoints(); + public double[] getThrustPoints(); + public Coordinate[] getCGPoints(); + +} diff --git a/core/src/net/sf/openrocket/motor/MotorDigest.java b/core/src/net/sf/openrocket/motor/MotorDigest.java new file mode 100644 index 00000000..e50cbf9f --- /dev/null +++ b/core/src/net/sf/openrocket/motor/MotorDigest.java @@ -0,0 +1,181 @@ +package net.sf.openrocket.motor; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.TextUtil; + +/** + * A class that generated a "digest" of a motor. A digest is a string value that + * uniquely identifies a motor (like a hash code or checksum). Two motors that have + * the same digest behave similarly with a very high probability. The digest can + * therefore be used to identify motors that otherwise have the same specifications. + *

+ * The digest only uses a limited amount of precision, so that rounding errors won't + * cause differing digest results. + * + * @author Sampo Niskanen + */ +public class MotorDigest { + + private static final double EPSILON = 0.00000000001; + + public enum DataType { + /** An array of time points at which data is available (in ms) */ + TIME_ARRAY(0, 1000), + /** Mass data for a few specific points (normally initial and empty mass) (in 0.1g) */ + MASS_SPECIFIC(1, 10000), + /** Mass per time (in 0.1g) */ + MASS_PER_TIME(2, 10000), + /** CG position for a few specific points (normally initial and final CG) (in mm) */ + CG_SPECIFIC(3, 1000), + /** CG position per time (in mm) */ + CG_PER_TIME(4, 1000), + /** Thrust force per time (in mN) */ + FORCE_PER_TIME(5, 1000); + + private final int order; + private final int multiplier; + + DataType(int order, int multiplier) { + this.order = order; + this.multiplier = multiplier; + } + + public int getOrder() { + return order; + } + + public int getMultiplier() { + return multiplier; + } + } + + + private final MessageDigest digest; + private boolean used = false; + private int lastOrder = -1; + + + public MotorDigest() { + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MD5 digest not supported by JRE", e); + } + } + + + public void update(DataType type, int... values) { + + // Check for correct order + if (lastOrder >= type.getOrder()) { + throw new IllegalArgumentException("Called with type=" + type + " order=" + type.getOrder() + + " while lastOrder=" + lastOrder); + } + lastOrder = type.getOrder(); + + // Digest the type + digest.update(bytes(type.getOrder())); + + // Digest the data length + digest.update(bytes(values.length)); + + // Digest the values + for (int v : values) { + digest.update(bytes(v)); + } + + } + + + private void update(DataType type, int multiplier, double... values) { + + int[] intValues = new int[values.length]; + for (int i = 0; i < values.length; i++) { + double v = values[i]; + v = next(v); + v *= multiplier; + v = next(v); + intValues[i] = (int) Math.round(v); + } + update(type, intValues); + } + + public void update(DataType type, double... values) { + update(type, type.getMultiplier(), values); + } + + private static double next(double v) { + return v + Math.signum(v) * EPSILON; + } + + + public String getDigest() { + if (used) { + throw new IllegalStateException("MotorDigest already used"); + } + used = true; + byte[] result = digest.digest(); + return TextUtil.hexString(result); + } + + + + private byte[] bytes(int value) { + return new byte[] { + (byte) ((value >>> 24) & 0xFF), (byte) ((value >>> 16) & 0xFF), + (byte) ((value >>> 8) & 0xFF), (byte) (value & 0xFF) }; + } + + + /** + * Digest the contents of a thrust curve motor. The result is a string uniquely + * defining the functional aspects of the motor. + * + * @param m the motor to digest + * @return the digest + */ + public static String digestMotor(Motor m) { + + // Create the motor digest from data available in RASP files + MotorDigest motorDigest = new MotorDigest(); + motorDigest.update(DataType.TIME_ARRAY, m.getTimePoints()); + + Coordinate[] cg = m.getCGPoints(); + double[] cgx = new double[cg.length]; + double[] mass = new double[cg.length]; + for (int i = 0; i < cg.length; i++) { + cgx[i] = cg[i].x; + mass[i] = cg[i].weight; + } + + motorDigest.update(DataType.MASS_PER_TIME, mass); + motorDigest.update(DataType.CG_PER_TIME, cgx); + motorDigest.update(DataType.FORCE_PER_TIME, m.getThrustPoints()); + return motorDigest.getDigest(); + + } + + public static String digestComment(String comment) { + comment = comment.replaceAll("\\s+", " ").trim(); + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("MD5 digest not supported by JRE", e); + } + + try { + digest.update(comment.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 encoding not supported by JRE", e); + } + + return TextUtil.hexString(digest.digest()); + } + +} diff --git a/core/src/net/sf/openrocket/motor/MotorId.java b/core/src/net/sf/openrocket/motor/MotorId.java new file mode 100644 index 00000000..126fff1c --- /dev/null +++ b/core/src/net/sf/openrocket/motor/MotorId.java @@ -0,0 +1,67 @@ +package net.sf.openrocket.motor; + +/** + * An immutable identifier for a motor instance in a MotorInstanceConfiguration. + * The motor is identified by the ID of its mounting component and a + * positive motor count number. + * + * @author Sampo Niskanen + */ +public final class MotorId { + + private final String componentId; + private final int number; + + + /** + * Sole constructor. + * + * @param componentId the component ID, must not be null + * @param number a positive motor doun5 number + */ + public MotorId(String componentId, int number) { + super(); + + if (componentId == null) { + throw new IllegalArgumentException("Component ID was null"); + } + if (number <= 0) { + throw new IllegalArgumentException("Number must be positive, n=" + number); + } + + // Use intern so comparison can be done using == instead of equals() + this.componentId = componentId.intern(); + this.number = number; + } + + + public String getComponentId() { + return componentId; + } + + public int getNumber() { + return number; + } + + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + + if (!(o instanceof MotorId)) + return false; + + MotorId other = (MotorId)o; + // Comparison with == ok since string is intern()'ed + return this.componentId == other.componentId && this.number == other.number; + } + + + @Override + public int hashCode() { + return componentId.hashCode() + (number << 12); + } + + // TODO: toString() +} diff --git a/core/src/net/sf/openrocket/motor/MotorInstance.java b/core/src/net/sf/openrocket/motor/MotorInstance.java new file mode 100644 index 00000000..6b48e3a4 --- /dev/null +++ b/core/src/net/sf/openrocket/motor/MotorInstance.java @@ -0,0 +1,60 @@ +package net.sf.openrocket.motor; + +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Monitorable; + +public interface MotorInstance extends Cloneable, Monitorable { + + /** + * Step the motor instance forward in time. + * + * @param time the time to step to, from motor ignition. + * @param acceleration the average acceleration during the step. + * @param cond the average atmospheric conditions during the step. + */ + public void step(double time, double acceleration, AtmosphericConditions cond); + + + /** + * Return the time to which this motor has been stepped. + * @return the current step time. + */ + public double getTime(); + + /** + * Return the average thrust during the last step. + */ + public double getThrust(); + + /** + * Return the average CG location during the last step. + */ + public Coordinate getCG(); + + /** + * Return the average longitudinal moment of inertia during the last step. + * This is the actual inertia, not the unit inertia! + */ + public double getLongitudinalInertia(); + + /** + * Return the average rotational moment of inertia during the last step. + * This is the actual inertia, not the unit inertia! + */ + public double getRotationalInertia(); + + /** + * Return whether this motor still produces thrust. If this method returns false + * the motor has burnt out, and will not produce any significant thrust anymore. + */ + public boolean isActive(); + + + /** + * Create a new instance of this motor instance. The state of the motor is + * identical to this instance and can be used independently from this one. + */ + public MotorInstance clone(); + +} diff --git a/core/src/net/sf/openrocket/motor/MotorInstanceConfiguration.java b/core/src/net/sf/openrocket/motor/MotorInstanceConfiguration.java new file mode 100644 index 00000000..a3142028 --- /dev/null +++ b/core/src/net/sf/openrocket/motor/MotorInstanceConfiguration.java @@ -0,0 +1,141 @@ +package net.sf.openrocket.motor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Monitorable; + +/** + * A configuration of motor instances identified by a string id. Each motor instance has + * an individual position, ingition time etc. + * + * @author Sampo Niskanen + */ +public final class MotorInstanceConfiguration implements Monitorable, Cloneable { + + private final List ids = new ArrayList(); + private final List unmodifiableIds = Collections.unmodifiableList(ids); + private final List motors = new ArrayList(); + private final List mounts = new ArrayList(); + private final List positions = new ArrayList(); + private final List ignitionTimes = new ArrayList(); + + + private int modID = 0; + + + /** + * Add a motor instance to this configuration. The motor is placed at + * the specified position and with an infinite ignition time (never ignited). + * + * @param id the ID of this motor instance. + * @param motor the motor instance. + * @param mount the motor mount containing this motor + * @param position the position of the motor in absolute coordinates. + * @throws IllegalArgumentException if a motor with the specified ID already exists. + */ + public void addMotor(MotorId id, MotorInstance motor, MotorMount mount, Coordinate position) { + if (this.ids.contains(id)) { + throw new IllegalArgumentException("MotorInstanceConfiguration already " + + "contains a motor with id " + id); + } + this.ids.add(id); + this.motors.add(motor); + this.mounts.add(mount); + this.positions.add(position); + this.ignitionTimes.add(Double.POSITIVE_INFINITY); + modID++; + } + + /** + * Return a list of all motor IDs in this configuration (not only ones in active stages). + */ + public List getMotorIDs() { + return unmodifiableIds; + } + + public MotorInstance getMotorInstance(MotorId id) { + return motors.get(indexOf(id)); + } + + public MotorMount getMotorMount(MotorId id) { + return mounts.get(indexOf(id)); + } + + public Coordinate getMotorPosition(MotorId id) { + return positions.get(indexOf(id)); + } + + public void setMotorPosition(MotorId id, Coordinate position) { + positions.set(indexOf(id), position); + modID++; + } + + public double getMotorIgnitionTime(MotorId id) { + return ignitionTimes.get(indexOf(id)); + } + + public void setMotorIgnitionTime(MotorId id, double time) { + this.ignitionTimes.set(indexOf(id), time); + modID++; + } + + + + private int indexOf(MotorId id) { + int index = ids.indexOf(id); + if (index < 0) { + throw new IllegalArgumentException("MotorInstanceConfiguration does not " + + "contain a motor with id " + id); + } + return index; + } + + + + /** + * Step all of the motor instances to the specified time minus their ignition time. + * @param time the "global" time + */ + public void step(double time, double acceleration, AtmosphericConditions cond) { + for (int i = 0; i < motors.size(); i++) { + double t = time - ignitionTimes.get(i); + if (t >= 0) { + motors.get(i).step(t, acceleration, cond); + } + } + modID++; + } + + @Override + public int getModID() { + int id = modID; + for (MotorInstance motor : motors) { + id += motor.getModID(); + } + return id; + } + + /** + * Return a copy of this motor instance configuration with independent motor instances + * from this instance. + */ + @Override + public MotorInstanceConfiguration clone() { + MotorInstanceConfiguration clone = new MotorInstanceConfiguration(); + clone.ids.addAll(this.ids); + clone.mounts.addAll(this.mounts); + clone.positions.addAll(this.positions); + clone.ignitionTimes.addAll(this.ignitionTimes); + for (MotorInstance motor : this.motors) { + clone.motors.add(motor.clone()); + } + clone.modID = this.modID; + return clone; + } + +} diff --git a/core/src/net/sf/openrocket/motor/ThrustCurveMotor.java b/core/src/net/sf/openrocket/motor/ThrustCurveMotor.java new file mode 100644 index 00000000..1f8acd2a --- /dev/null +++ b/core/src/net/sf/openrocket/motor/ThrustCurveMotor.java @@ -0,0 +1,564 @@ +package net.sf.openrocket.motor; + +import java.io.Serializable; +import java.text.Collator; +import java.util.Arrays; +import java.util.Locale; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Inertia; +import net.sf.openrocket.util.MathUtil; + + +public class ThrustCurveMotor implements Motor, Comparable { + private static final LogHelper log = Application.getLogger(); + + public static final double MAX_THRUST = 10e6; + + // Comparators: + private static final Collator COLLATOR = Collator.getInstance(Locale.US); + static { + COLLATOR.setStrength(Collator.PRIMARY); + } + private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator(); + + + + private final Manufacturer manufacturer; + private final String designation; + private final String description; + private final Motor.Type type; + private final double[] delays; + private final double diameter; + private final double length; + private final double[] time; + private final double[] thrust; + private final Coordinate[] cg; + + private double maxThrust; + private double burnTime; + private double averageThrust; + private double totalImpulse; + + + /** + * Sole constructor. Sets all the properties of the motor. + * + * @param manufacturer the manufacturer of the motor. + * @param designation the designation of the motor. + * @param description extra description of the motor. + * @param type the motor type + * @param delays the delays defined for this thrust curve + * @param diameter diameter of the motor. + * @param length length of the motor. + * @param time the time points for the thrust curve. + * @param thrust thrust at the time points. + * @param cg cg at the time points. + */ + public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description, + Motor.Type type, double[] delays, double diameter, double length, + double[] time, double[] thrust, Coordinate[] cg) { + + // Check argument validity + if ((time.length != thrust.length) || (time.length != cg.length)) { + throw new IllegalArgumentException("Array lengths do not match, " + + "time:" + time.length + " thrust:" + thrust.length + + " cg:" + cg.length); + } + if (time.length < 2) { + throw new IllegalArgumentException("Too short thrust-curve, length=" + + time.length); + } + for (int i = 0; i < time.length - 1; i++) { + if (time[i + 1] < time[i]) { + throw new IllegalArgumentException("Time goes backwards, " + + "time[" + i + "]=" + time[i] + " " + + "time[" + (i + 1) + "]=" + time[i + 1]); + } + } + if (!MathUtil.equals(time[0], 0)) { + throw new IllegalArgumentException("Curve starts at time " + time[0]); + } + if (!MathUtil.equals(thrust[0], 0)) { + throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]); + } + if (!MathUtil.equals(thrust[thrust.length - 1], 0)) { + throw new IllegalArgumentException("Curve ends at thrust " + + thrust[thrust.length - 1]); + } + for (double t : thrust) { + if (t < 0) { + throw new IllegalArgumentException("Negative thrust."); + } + if (t > MAX_THRUST || Double.isNaN(t)) { + throw new IllegalArgumentException("Invalid thrust " + t); + } + } + for (Coordinate c : cg) { + if (c.isNaN()) { + throw new IllegalArgumentException("Invalid CG " + c); + } + if (c.x < 0 || c.x > length) { + throw new IllegalArgumentException("Invalid CG position " + c.x); + } + if (c.weight < 0) { + throw new IllegalArgumentException("Negative mass " + c.weight); + } + } + + if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD && + type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) { + throw new IllegalArgumentException("Illegal motor type=" + type); + } + + + this.manufacturer = manufacturer; + this.designation = designation; + this.description = description; + this.type = type; + this.delays = delays.clone(); + this.diameter = diameter; + this.length = length; + this.time = time.clone(); + this.thrust = thrust.clone(); + this.cg = cg.clone(); + + computeStatistics(); + } + + + + /** + * Get the manufacturer of this motor. + * + * @return the manufacturer + */ + public Manufacturer getManufacturer() { + return manufacturer; + } + + + /** + * Return the array of time points for this thrust curve. + * @return an array of time points where the thrust is sampled + */ + public double[] getTimePoints() { + return time.clone(); + } + + /** + * Returns the array of thrust points for this thrust curve. + * @return an array of thrust samples + */ + public double[] getThrustPoints() { + return thrust.clone(); + } + + /** + * Returns the array of CG points for this thrust curve. + * @return an array of CG samples + */ + public Coordinate[] getCGPoints() { + return cg.clone(); + } + + /** + * Return a list of standard delays defined for this motor. + * @return a list of standard delays + */ + public double[] getStandardDelays() { + return delays.clone(); + } + + + /** + * {@inheritDoc} + *

+ * NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet, + * not the ThrustCurveMotor itself. + */ + @Override + public Type getMotorType() { + return type; + } + + + @Override + public String getDesignation() { + return designation; + } + + @Override + public String getDesignation(double delay) { + return designation + "-" + getDelayString(delay); + } + + + @Override + public String getDescription() { + return description; + } + + @Override + public double getDiameter() { + return diameter; + } + + @Override + public double getLength() { + return length; + } + + + @Override + public MotorInstance getInstance() { + return new ThrustCurveMotorInstance(); + } + + + @Override + public Coordinate getLaunchCG() { + return cg[0]; + } + + @Override + public Coordinate getEmptyCG() { + return cg[cg.length - 1]; + } + + + + + @Override + public double getBurnTimeEstimate() { + return burnTime; + } + + @Override + public double getAverageThrustEstimate() { + return averageThrust; + } + + @Override + public double getMaxThrustEstimate() { + return maxThrust; + } + + @Override + public double getTotalImpulseEstimate() { + return totalImpulse; + } + + + + /** + * Compute the general statistics of this motor. + */ + private void computeStatistics() { + + // Maximum thrust + maxThrust = 0; + for (double t : thrust) { + if (t > maxThrust) + maxThrust = t; + } + + + // Burn start time + double thrustLimit = maxThrust * MARGINAL_THRUST; + double burnStart, burnEnd; + + int pos; + for (pos = 1; pos < thrust.length; pos++) { + if (thrust[pos] >= thrustLimit) + break; + } + if (pos >= thrust.length) { + throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust + + " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust)); + } + if (MathUtil.equals(thrust[pos - 1], thrust[pos])) { + // For safety + burnStart = (time[pos - 1] + time[pos]) / 2; + } else { + burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]); + } + + + // Burn end time + for (pos = thrust.length - 2; pos >= 0; pos--) { + if (thrust[pos] >= thrustLimit) + break; + } + if (pos < 0) { + throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust + + " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust)); + } + if (MathUtil.equals(thrust[pos], thrust[pos + 1])) { + // For safety + burnEnd = (time[pos] + time[pos + 1]) / 2; + } else { + burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1], + time[pos], time[pos + 1]); + } + + + // Burn time + burnTime = Math.max(burnEnd - burnStart, 0); + + + // Total impulse and average thrust + totalImpulse = 0; + averageThrust = 0; + + for (pos = 0; pos < time.length - 1; pos++) { + double t0 = time[pos]; + double t1 = time[pos + 1]; + double f0 = thrust[pos]; + double f1 = thrust[pos + 1]; + + totalImpulse += (t1 - t0) * (f0 + f1) / 2; + + if (t0 < burnStart && t1 > burnStart) { + double fStart = MathUtil.map(burnStart, t0, t1, f0, f1); + averageThrust += (fStart + f1) / 2 * (t1 - burnStart); + } else if (t0 >= burnStart && t1 <= burnEnd) { + averageThrust += (f0 + f1) / 2 * (t1 - t0); + } else if (t0 < burnEnd && t1 > burnEnd) { + double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1); + averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0); + } + } + + if (burnTime > 0) { + averageThrust /= burnTime; + } else { + averageThrust = 0; + } + + } + + + ////////// Static methods + + /** + * Return a String representation of a delay time. If the delay is {@link #PLUGGED}, + * returns "P". + * + * @param delay the delay time. + * @return the String representation. + */ + public static String getDelayString(double delay) { + return getDelayString(delay, "P"); + } + + /** + * Return a String representation of a delay time. If the delay is {@link #PLUGGED}, + * plugged is returned. + * + * @param delay the delay time. + * @param plugged the return value if there is no ejection charge. + * @return the String representation. + */ + public static String getDelayString(double delay, String plugged) { + if (delay == PLUGGED) + return plugged; + delay = Math.rint(delay * 10) / 10; + if (MathUtil.equals(delay, Math.rint(delay))) + return "" + ((int) delay); + return "" + delay; + } + + + + //////// Motor instance implementation //////// + private class ThrustCurveMotorInstance implements MotorInstance { + + private int position; + + // Previous time step value + private double prevTime; + + // Average thrust during previous step + private double stepThrust; + // Instantaneous thrust at current time point + private double instThrust; + + // Average CG during previous step + private Coordinate stepCG; + // Instantaneous CG at current time point + private Coordinate instCG; + + private final double unitRotationalInertia; + private final double unitLongitudinalInertia; + + private int modID = 0; + + public ThrustCurveMotorInstance() { + log.debug("ThrustCurveMotor: Creating motor instance of " + ThrustCurveMotor.this); + position = 0; + prevTime = 0; + instThrust = 0; + stepThrust = 0; + instCG = cg[0]; + stepCG = cg[0]; + unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2); + unitLongitudinalInertia = Inertia.filledCylinderLongitudinal(getDiameter() / 2, getLength()); + } + + @Override + public double getTime() { + return prevTime; + } + + @Override + public Coordinate getCG() { + return stepCG; + } + + @Override + public double getLongitudinalInertia() { + return unitLongitudinalInertia * stepCG.weight; + } + + @Override + public double getRotationalInertia() { + return unitRotationalInertia * stepCG.weight; + } + + @Override + public double getThrust() { + return stepThrust; + } + + @Override + public boolean isActive() { + return prevTime < time[time.length - 1]; + } + + @Override + public void step(double nextTime, double acceleration, AtmosphericConditions cond) { + + if (!(nextTime >= prevTime)) { + // Also catches NaN + throw new IllegalArgumentException("Stepping backwards in time, current=" + + prevTime + " new=" + nextTime); + } + if (MathUtil.equals(prevTime, nextTime)) { + return; + } + + modID++; + + if (position >= time.length - 1) { + // Thrust has ended + prevTime = nextTime; + stepThrust = 0; + instThrust = 0; + stepCG = cg[cg.length - 1]; + return; + } + + + // Compute average & instantaneous thrust + if (nextTime < time[position + 1]) { + + // Time step between time points + double nextF = MathUtil.map(nextTime, time[position], time[position + 1], + thrust[position], thrust[position + 1]); + stepThrust = (instThrust + nextF) / 2; + instThrust = nextF; + + } else { + + // Portion of previous step + stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime); + + // Whole steps + position++; + while ((position < time.length - 1) && (nextTime >= time[position + 1])) { + stepThrust += (thrust[position] + thrust[position + 1]) / 2 * + (time[position + 1] - time[position]); + position++; + } + + // End step + if (position < time.length - 1) { + instThrust = MathUtil.map(nextTime, time[position], time[position + 1], + thrust[position], thrust[position + 1]); + stepThrust += (thrust[position] + instThrust) / 2 * + (nextTime - time[position]); + } else { + // Thrust ended during this step + instThrust = 0; + } + + stepThrust /= (nextTime - prevTime); + + } + + // Compute average and instantaneous CG (simple average between points) + Coordinate nextCG; + if (position < time.length - 1) { + nextCG = MathUtil.map(nextTime, time[position], time[position + 1], + cg[position], cg[position + 1]); + } else { + nextCG = cg[cg.length - 1]; + } + stepCG = instCG.add(nextCG).multiply(0.5); + instCG = nextCG; + + // Update time + prevTime = nextTime; + } + + @Override + public MotorInstance clone() { + try { + return (MotorInstance) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException", e); + } + } + + @Override + public int getModID() { + return modID; + } + } + + + + @Override + public int compareTo(ThrustCurveMotor other) { + + int value; + + // 1. Manufacturer + value = COLLATOR.compare(this.manufacturer.getDisplayName(), + ((ThrustCurveMotor) other).manufacturer.getDisplayName()); + if (value != 0) + return value; + + // 2. Designation + value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation()); + if (value != 0) + return value; + + // 3. Diameter + value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000); + if (value != 0) + return value; + + // 4. Length + value = (int) ((this.getLength() - other.getLength()) * 1000000); + return value; + + } + + +} diff --git a/core/src/net/sf/openrocket/optimization/general/Function.java b/core/src/net/sf/openrocket/optimization/general/Function.java new file mode 100644 index 00000000..a7be5668 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/Function.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.optimization.general; + +/** + * An interface defining an optimizable function. + *

+ * Some function optimizers require that the function is thread-safe. + * + * @author Sampo Niskanen + */ +public interface Function { + + /** + * Evaluate the function at the specified point. + *

+ * If the function evaluation is slow, then this method should abort the computation if + * the thread is interrupted. + * + * @param point the point at which to evaluate the function. + * @return the function value. + * @throws InterruptedException if the thread was interrupted before function evaluation was completed. + * @throws OptimizationException if an error occurs that prevents the optimization + */ + public double evaluate(Point point) throws InterruptedException, OptimizationException; + +} diff --git a/core/src/net/sf/openrocket/optimization/general/FunctionCache.java b/core/src/net/sf/openrocket/optimization/general/FunctionCache.java new file mode 100644 index 00000000..69c42d79 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/FunctionCache.java @@ -0,0 +1,39 @@ +package net.sf.openrocket.optimization.general; + +/** + * A storage of cached values of a function. The purpose of this class is to + * cache function values between optimization runs. Subinterfaces may provide + * additional functionality. + * + * @author Sampo Niskanen + */ +public interface FunctionCache { + + /** + * Compute and return the value of the function at the specified point. + * + * @param point the point at which to evaluate. + * @return the value of the function at that point. + */ + public double getValue(Point point); + + /** + * Clear the cache. + */ + public void clearCache(); + + /** + * Return the function that is evaluated by this cache implementation. + * + * @return the function that is being evaluated. + */ + public Function getFunction(); + + /** + * Set the function that is evaluated by this cache implementation. + * + * @param function the function that is being evaluated. + */ + public void setFunction(Function function); + +} diff --git a/core/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java b/core/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java new file mode 100644 index 00000000..485843f9 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.optimization.general; + +/** + * An interface for a function optimization algorithm. The function is evaluated + * via a function cache. + * + * @author Sampo Niskanen + */ +public interface FunctionOptimizer { + + /** + * Perform optimization on the function. The optimization control is called to control + * when optimization is stopped. + * + * @param initial the initial start point of the optimization. + * @param control the optimization control. + * @throws OptimizationException if an error occurs that prevents optimization + */ + public void optimize(Point initial, OptimizationController control) throws OptimizationException; + + + /** + * Return the optimum point computed by {@link #optimize(Point, OptimizationController)}. + * + * @return the optimum point value. + * @throws IllegalStateException if {@link #optimize(Point, OptimizationController)} has not been called. + */ + public Point getOptimumPoint(); + + /** + * Return the function value at the optimum point. + * + * @return the value at the optimum point. + * @throws IllegalStateException if {@link #optimize(Point, OptimizationController)} has not been called. + */ + public double getOptimumValue(); + + + /** + * Return the function cache used by this optimization algorithm. + * + * @return the function cache. + */ + public FunctionCache getFunctionCache(); + + /** + * Set the function cache that provides the function values for this algorithm. + * Some algorithms may require the function cache to be an instance of + * ParallelFunctionCache. + * + * @param functionCache the function cache. + */ + public void setFunctionCache(FunctionCache functionCache); + + +} diff --git a/core/src/net/sf/openrocket/optimization/general/OptimizationController.java b/core/src/net/sf/openrocket/optimization/general/OptimizationController.java new file mode 100644 index 00000000..c91689cb --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/OptimizationController.java @@ -0,0 +1,19 @@ +package net.sf.openrocket.optimization.general; + +public interface OptimizationController { + + /** + * Control for whether to continue the optimization. This method is called after + * every full step taken by the optimization algorithm. + * + * @param oldPoint the old position. + * @param oldValue the value of the function at the old position. + * @param newPoint the new position. + * @param newValue the value of the function at the new position. + * @param stepSize the step length that is used to search for smaller function values when applicable, or NaN. + * @return true to continue optimization, false to stop. + */ + public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, + double stepSize); + +} diff --git a/core/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java b/core/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java new file mode 100644 index 00000000..a0a455e9 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.optimization.general; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * An OptimizationController that delegates control actions to multiple other controllers. + * The optimization is stopped if any of the controllers stops it. + * + * @author Sampo Niskanen + */ +public class OptimizationControllerDelegator implements OptimizationController { + + private final List controllers = new ArrayList(); + + /** + * Construct the controlled based on an array of controllers. + * + * @param controllers the controllers to use. + */ + public OptimizationControllerDelegator(OptimizationController... controllers) { + for (OptimizationController c : controllers) { + this.controllers.add(c); + } + } + + /** + * Construct the controller based on a collection of controllers. + * + * @param controllers the controllers to use. + */ + public OptimizationControllerDelegator(Collection controllers) { + this.controllers.addAll(controllers); + } + + + /** + * Control whether to continue optimization. This method returns false if any of the + * used controllers returns false. However, all controllers will be called even if + * an earlier one stops the optimization. + */ + @Override + public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { + boolean ret = true; + + for (OptimizationController c : controllers) { + if (!c.stepTaken(oldPoint, oldValue, newPoint, newValue, stepSize)) { + ret = false; + } + } + + return ret; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/general/OptimizationException.java b/core/src/net/sf/openrocket/optimization/general/OptimizationException.java new file mode 100644 index 00000000..7c0c0674 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/OptimizationException.java @@ -0,0 +1,22 @@ +package net.sf.openrocket.optimization.general; + +/** + * An exception that prevents optimization from continuing. + * + * @author Sampo Niskanen + */ +public class OptimizationException extends Exception { + + public OptimizationException(String message) { + super(message); + } + + public OptimizationException(Throwable cause) { + super(cause); + } + + public OptimizationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java b/core/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java new file mode 100644 index 00000000..67154da5 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java @@ -0,0 +1,303 @@ +package net.sf.openrocket.optimization.general; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import net.sf.openrocket.util.BugException; + +/** + * An implementation of a ParallelFunctionCache that evaluates function values + * in parallel and caches them. This allows pre-calculating possibly required + * function values beforehand. If values are not required after all, the + * computation can be aborted assuming the function evaluation supports it. + *

+ * Note that while this class handles threads and abstracts background execution, + * the public methods themselves are NOT thread-safe and should be called from + * only one thread at a time. + * + * @author Sampo Niskanen + */ +public class ParallelExecutorCache implements ParallelFunctionCache { + + private final Map functionCache = new HashMap(); + private final Map> futureMap = new HashMap>(); + + private ExecutorService executor; + + private Function function; + + + /** + * Construct a cache that uses the same number of computational threads as there are + * processors available. + */ + public ParallelExecutorCache() { + this(Runtime.getRuntime().availableProcessors()); + } + + /** + * Construct a cache that uses the specified number of computational threads for background + * computation. The threads that are created are marked as daemon threads. + * + * @param threadCount the number of threads to use in the executor. + */ + public ParallelExecutorCache(int threadCount) { + this(new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS, + new LinkedBlockingQueue(), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setDaemon(true); + return t; + } + })); + } + + /** + * Construct a cache that uses the specified ExecutorService for managing + * computational threads. + * + * @param executor the executor to use for function evaluations. + */ + public ParallelExecutorCache(ExecutorService executor) { + this.executor = executor; + } + + + + @Override + public void compute(Collection points) { + for (Point p : points) { + compute(p); + } + } + + + @Override + public void compute(Point point) { + + if (isOutsideRange(point)) { + // Point is outside of range + return; + } + + if (functionCache.containsKey(point)) { + // Function has already been evaluated at the point + return; + } + + if (futureMap.containsKey(point)) { + // Function is being evaluated at the point + return; + } + + // Submit point for evaluation + FunctionCallable callable = new FunctionCallable(function, point); + Future future = executor.submit(callable); + futureMap.put(point, future); + } + + + @Override + public void waitFor(Collection points) throws InterruptedException, OptimizationException { + for (Point p : points) { + waitFor(p); + } + } + + + @Override + public void waitFor(Point point) throws InterruptedException, OptimizationException { + if (isOutsideRange(point)) { + return; + } + + if (functionCache.containsKey(point)) { + return; + } + + Future future = futureMap.get(point); + if (future == null) { + throw new IllegalStateException("waitFor called for " + point + " but it is not being computed"); + } + + try { + double value = future.get(); + functionCache.put(point, value); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (cause instanceof InterruptedException) { + throw (InterruptedException) cause; + } + if (cause instanceof OptimizationException) { + throw (OptimizationException) cause; + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + + throw new BugException("Function threw unknown exception while processing", e); + } + } + + + + @Override + public List abort(Collection points) { + List computed = new ArrayList(Math.min(points.size(), 10)); + + for (Point p : points) { + if (abort(p)) { + computed.add(p); + } + } + + return computed; + } + + + + @Override + public boolean abort(Point point) { + if (isOutsideRange(point)) { + return false; + } + + if (functionCache.containsKey(point)) { + return true; + } + + Future future = futureMap.remove(point); + if (future == null) { + throw new IllegalStateException("abort called for " + point + " but it is not being computed"); + } + + if (future.isDone()) { + // Evaluation has been completed, store value in cache + try { + double value = future.get(); + functionCache.put(point, value); + return true; + } catch (Exception e) { + return false; + } + } else { + // Cancel the evaluation + future.cancel(true); + return false; + } + } + + + @Override + public void abortAll() { + Iterator iterator = futureMap.keySet().iterator(); + while (iterator.hasNext()) { + Point point = iterator.next(); + Future future = futureMap.get(point); + iterator.remove(); + + if (future.isDone()) { + // Evaluation has been completed, store value in cache + try { + double value = future.get(); + functionCache.put(point, value); + } catch (Exception e) { + // Ignore + } + } else { + // Cancel the evaluation + future.cancel(true); + } + } + } + + + @Override + public double getValue(Point point) { + if (isOutsideRange(point)) { + return Double.MAX_VALUE; + } + + Double d = functionCache.get(point); + if (d == null) { + throw new IllegalStateException(point + " is not in function cache. " + + "functionCache=" + functionCache + " futureMap=" + futureMap); + } + return d; + } + + + @Override + public Function getFunction() { + return function; + } + + @Override + public void setFunction(Function function) { + this.function = function; + clearCache(); + } + + @Override + public void clearCache() { + List list = new ArrayList(futureMap.keySet()); + abort(list); + functionCache.clear(); + } + + + public ExecutorService getExecutor() { + return executor; + } + + + /** + * Check whether a point is outside of the valid optimization range. + */ + private boolean isOutsideRange(Point p) { + int n = p.dim(); + for (int i = 0; i < n; i++) { + double d = p.get(i); + // Include NaN in disallowed range + if (!(d >= 0.0 && d <= 1.0)) { + return true; + } + } + return false; + } + + + /** + * A Callable that evaluates a function at a specific point and returns the result. + */ + private class FunctionCallable implements Callable { + private final Function calledFunction; + private final Point point; + + public FunctionCallable(Function function, Point point) { + this.calledFunction = function; + this.point = point; + } + + @Override + public Double call() throws InterruptedException, OptimizationException { + return calledFunction.evaluate(point); + } + } + + +} diff --git a/core/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java b/core/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java new file mode 100644 index 00000000..1c8a35f6 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java @@ -0,0 +1,77 @@ +package net.sf.openrocket.optimization.general; + +import java.util.Collection; +import java.util.List; + +/** + * A FunctionCache that allows scheduling points to be computed in the background, + * waiting for specific points to become computed or aborting the computation of + * points. + * + * @author Sampo Niskanen + */ +public interface ParallelFunctionCache extends FunctionCache { + + /** + * Schedule a list of function evaluations at the specified points. + * The points are added to the end of the computation queue in the order + * they are returned by the iterator. + * + * @param points the points at which to evaluate the function. + */ + public void compute(Collection points); + + /** + * Schedule function evaluation for the specified point. The point is + * added to the end of the computation queue. + * + * @param point the point at which to evaluate the function. + */ + public void compute(Point point); + + /** + * Wait for a collection of points to be computed. After calling this method + * the function values are available by calling {@link #getValue(Point)}. + * + * @param points the points to wait for. + * @throws InterruptedException if this thread or the computing thread was interrupted while waiting. + * @throws OptimizationException if an error preventing continuing the optimization occurs. + */ + public void waitFor(Collection points) throws InterruptedException, OptimizationException; + + /** + * Wait for a point to be computed. After calling this method + * the function value is available by calling {@link #getValue(Point)}. + * + * @param point the point to wait for. + * @throws InterruptedException if this thread or the computing thread was interrupted while waiting. + * @throws OptimizationException if an error preventing continuing the optimization occurs. + */ + public void waitFor(Point point) throws InterruptedException, OptimizationException; + + + /** + * Abort the computation of the specified points. If computation has ended, + * the result is stored in the function cache anyway. + * + * @param points the points to abort. + * @return a list of the points that have been computed anyway + */ + public List abort(Collection points); + + + /** + * Abort the computation of the specified point. If computation has ended, + * the result is stored in the function cache anyway. + * + * @param point the point to abort. + * @return true if the point has been computed anyway, false if not. + */ + public boolean abort(Point point); + + + /** + * Abort the computation of all still unexecuted points. + */ + public void abortAll(); +} diff --git a/core/src/net/sf/openrocket/optimization/general/Point.java b/core/src/net/sf/openrocket/optimization/general/Point.java new file mode 100644 index 00000000..9b2bece8 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/Point.java @@ -0,0 +1,207 @@ +package net.sf.openrocket.optimization.general; + +import java.util.Arrays; + +import net.sf.openrocket.util.MathUtil; + +/** + * An immutable n-dimensional coordinate point. + * + * @author Sampo Niskanen + */ +public final class Point { + + private final double[] point; + private double length = -1; + private double length2 = -1; + + + /** + * Create a new point with all values zero. + * + * @param dim the dimensionality of the point + */ + public Point(int dim) { + if (dim <= 0) { + throw new IllegalArgumentException("Invalid dimensionality " + dim); + } + point = new double[dim]; + } + + + /** + * Create a new point filled with a specific value. + * + * @param dim the dimensionality of the point + * @param value the value for all dimensions + */ + public Point(int dim, double value) { + this(dim); + Arrays.fill(point, value); + } + + + /** + * Create a new point with specific values. + * + * @param value the values of the dimensions. + */ + public Point(double... value) { + if (value.length == 0) { + throw new IllegalArgumentException("Zero-dimensional point not allowed"); + } + point = value.clone(); + } + + + /** + * Create a copy of a point. Used locally to create copies. + * + * @param p the point to copy. + */ + private Point(Point p) { + point = p.point.clone(); + } + + + + /** + * Return the point dimensionality. + * + * @return the point dimensionality + */ + public int dim() { + return point.length; + } + + + + public double get(int i) { + return point[i]; + } + + public Point set(int i, double v) { + Point p = new Point(this); + p.point[i] = v; + return p; + } + + + /** + * Return a new point that is the sum of two points. + * + * @param other the point to add to this point. + * @return the sum of these points. + */ + public Point add(Point other) { + Point p = new Point(this); + for (int i = 0; i < point.length; i++) { + p.point[i] += other.point[i]; + } + return p; + } + + + /** + * Return a new point that is the subtraction of two points. + * + * @param other the point to subtract from this point. + * @return the value of this - other. + */ + public Point sub(Point other) { + Point p = new Point(this); + for (int i = 0; i < point.length; i++) { + p.point[i] -= other.point[i]; + } + return p; + } + + + /** + * Return this point multiplied by a scalar value. + * + * @param v the scalar to multiply with + * @return the scaled point + */ + public Point mul(double v) { + Point p = new Point(this); + for (int i = 0; i < point.length; i++) { + p.point[i] *= v; + } + return p; + } + + + /** + * Return the length of this coordinate. + * + * @return the length. + */ + public double length() { + if (length < 0) { + length = MathUtil.safeSqrt(length2()); + } + return length; + } + + + /** + * Return the squared length of this coordinate. + * + * @return the square of the length of thie coordinate. + */ + public double length2() { + if (length2 < 0) { + length2 = 0; + for (double p : point) { + length2 += p * p; + } + } + return length2; + } + + + + /** + * Return the point as an array. + * + * @return the point as an array. + */ + public double[] asArray() { + return point.clone(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + + if (!(obj instanceof Point)) + return false; + + Point other = (Point) obj; + if (this.point.length != other.point.length) + return false; + + for (int i = 0; i < point.length; i++) { + if (!MathUtil.equals(this.point[i], other.point[i])) + return false; + } + return true; + } + + @Override + public int hashCode() { + int n = 0; + for (double d : point) { + n *= 37; + n += (int) (d * 1000); + } + return n; + } + + @Override + public String toString() { + return "Point" + Arrays.toString(point); + } +} diff --git a/core/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java b/core/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java new file mode 100644 index 00000000..16a2fbcc --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.optimization.general.multidim; + +import java.util.Comparator; + +import net.sf.openrocket.optimization.general.FunctionCache; +import net.sf.openrocket.optimization.general.Point; + +/** + * A comparator that orders Points in a function value order, smallest first. + * + * @author Sampo Niskanen + */ +public class FunctionCacheComparator implements Comparator { + + private final FunctionCache cache; + + public FunctionCacheComparator(FunctionCache cache) { + this.cache = cache; + } + + @Override + public int compare(Point o1, Point o2) { + double v1 = cache.getValue(o1); + double v2 = cache.getValue(o2); + + return Double.compare(v1, v2); + } + +} diff --git a/core/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java b/core/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java new file mode 100644 index 00000000..d4d04fc9 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java @@ -0,0 +1,324 @@ +package net.sf.openrocket.optimization.general.multidim; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.FunctionCache; +import net.sf.openrocket.optimization.general.FunctionOptimizer; +import net.sf.openrocket.optimization.general.OptimizationController; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.ParallelFunctionCache; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Statistics; + +/** + * A customized implementation of the parallel multidirectional search algorithm by Dennis and Torczon. + *

+ * This is a parallel pattern search optimization algorithm. The function evaluations are performed + * using an ExecutorService. By default a ThreadPoolExecutor is used that has as many thread defined + * as the system has processors. + *

+ * The optimization can be aborted by interrupting the current thread. + */ +public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Statistics { + private static final LogHelper log = Application.getLogger(); + + private List simplex = new ArrayList(); + + private ParallelFunctionCache functionExecutor; + + private boolean useExpansion = false; + private boolean useCoordinateSearch = false; + + private int stepCount = 0; + private int reflectionAcceptance = 0; + private int expansionAcceptance = 0; + private int coordinateAcceptance = 0; + private int reductionFallback = 0; + + + public MultidirectionalSearchOptimizer() { + // No-op + } + + public MultidirectionalSearchOptimizer(ParallelFunctionCache functionCache) { + this.functionExecutor = functionCache; + } + + + + @Override + public void optimize(Point initial, OptimizationController control) throws OptimizationException { + FunctionCacheComparator comparator = new FunctionCacheComparator(functionExecutor); + + final List pattern = SearchPattern.square(initial.dim()); + log.info("Starting optimization at " + initial + " with pattern " + pattern); + + try { + + boolean simplexComputed = false; + double step = 0.5; + + // Set up the current simplex + simplex.clear(); + simplex.add(initial); + for (Point p : pattern) { + simplex.add(initial.add(p.mul(step))); + } + + // Normal iterations + List reflection = new ArrayList(simplex.size()); + List expansion = new ArrayList(simplex.size()); + List coordinateSearch = new ArrayList(simplex.size()); + Point current; + double currentValue; + boolean continueOptimization = true; + while (continueOptimization) { + + log.debug("Starting optimization step with simplex " + simplex + + (simplexComputed ? "" : " (not computed)")); + stepCount++; + + if (!simplexComputed) { + // TODO: Could something be computed in parallel? + functionExecutor.compute(simplex); + functionExecutor.waitFor(simplex); + Collections.sort(simplex, comparator); + simplexComputed = true; + } + + current = simplex.get(0); + currentValue = functionExecutor.getValue(current); + + /* + * Compute and queue the next points in likely order of usefulness. + * Expansion is unlikely as we're mainly dealing with bounded optimization. + */ + createReflection(simplex, reflection); + if (useCoordinateSearch) + createCoordinateSearch(current, step, coordinateSearch); + if (useExpansion) + createExpansion(simplex, expansion); + + functionExecutor.compute(reflection); + if (useCoordinateSearch) + functionExecutor.compute(coordinateSearch); + if (useExpansion) + functionExecutor.compute(expansion); + + // Check reflection acceptance + log.debug("Computing reflection"); + functionExecutor.waitFor(reflection); + + if (accept(reflection, currentValue)) { + + log.debug("Reflection was successful, aborting coordinate search, " + + (useExpansion ? "computing" : "skipping") + " expansion"); + + if (useCoordinateSearch) + functionExecutor.abort(coordinateSearch); + + simplex.clear(); + simplex.add(current); + simplex.addAll(reflection); + Collections.sort(simplex, comparator); + + if (useExpansion) { + + /* + * Assume expansion to be unsuccessful, queue next reflection while computing expansion. + */ + createReflection(simplex, reflection); + + functionExecutor.compute(reflection); + functionExecutor.waitFor(expansion); + + if (accept(expansion, currentValue)) { + log.debug("Expansion was successful, aborting reflection"); + functionExecutor.abort(reflection); + + simplex.clear(); + simplex.add(current); + simplex.addAll(expansion); + step *= 2; + Collections.sort(simplex, comparator); + expansionAcceptance++; + } else { + log.debug("Expansion failed"); + reflectionAcceptance++; + } + + } else { + reflectionAcceptance++; + } + + } else { + + log.debug("Reflection was unsuccessful, aborting expansion, computing coordinate search"); + + functionExecutor.abort(expansion); + + /* + * Assume coordinate search to be unsuccessful, queue contraction step while computing. + */ + halveStep(simplex); + functionExecutor.compute(simplex); + + if (useCoordinateSearch) { + functionExecutor.waitFor(coordinateSearch); + + if (accept(coordinateSearch, currentValue)) { + + log.debug("Coordinate search successful, reseting simplex"); + List toAbort = new LinkedList(simplex); + simplex.clear(); + simplex.add(current); + for (Point p : pattern) { + simplex.add(current.add(p.mul(step))); + } + toAbort.removeAll(simplex); + functionExecutor.abort(toAbort); + simplexComputed = false; + coordinateAcceptance++; + + } else { + log.debug("Coordinate search unsuccessful, halving step."); + step /= 2; + simplexComputed = false; + reductionFallback++; + } + } else { + log.debug("Coordinate search not used, halving step."); + step /= 2; + simplexComputed = false; + reductionFallback++; + } + + } + + log.debug("Ending optimization step with simplex " + simplex); + + continueOptimization = control.stepTaken(current, currentValue, simplex.get(0), + functionExecutor.getValue(simplex.get(0)), step); + + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + } + + } catch (InterruptedException e) { + log.info("Optimization was interrupted with InterruptedException"); + } + + log.info("Finishing optimization at point " + simplex.get(0) + " value = " + + functionExecutor.getValue(simplex.get(0))); + log.info("Optimization statistics: " + getStatistics()); + } + + + + private void createReflection(List base, List reflection) { + Point current = base.get(0); + reflection.clear(); + + /* new = - (old - current) + current = 2*current - old */ + for (int i = 1; i < base.size(); i++) { + Point p = base.get(i); + p = current.mul(2).sub(p); + reflection.add(p); + } + } + + private void createExpansion(List base, List expansion) { + Point current = base.get(0); + expansion.clear(); + for (int i = 1; i < base.size(); i++) { + Point p = current.mul(3).sub(base.get(i).mul(2)); + expansion.add(p); + } + } + + private void halveStep(List base) { + Point current = base.get(0); + for (int i = 1; i < base.size(); i++) { + Point p = base.get(i); + + /* new = (old - current)*0.5 + current = old*0.5 + current*0.5 = (old + current)*0.5 */ + + p = p.add(current).mul(0.5); + base.set(i, p); + } + } + + private void createCoordinateSearch(Point current, double step, List coordinateDirections) { + coordinateDirections.clear(); + for (int i = 0; i < current.dim(); i++) { + Point p = new Point(current.dim()); + p = p.set(i, step); + coordinateDirections.add(current.add(p)); + coordinateDirections.add(current.sub(p)); + } + } + + + private boolean accept(List points, double currentValue) { + for (Point p : points) { + if (functionExecutor.getValue(p) < currentValue) { + return true; + } + } + return false; + } + + + + @Override + public Point getOptimumPoint() { + if (simplex.size() == 0) { + throw new IllegalStateException("Optimization has not been called, simplex is empty"); + } + return simplex.get(0); + } + + @Override + public double getOptimumValue() { + return functionExecutor.getValue(getOptimumPoint()); + } + + @Override + public FunctionCache getFunctionCache() { + return functionExecutor; + } + + @Override + public void setFunctionCache(FunctionCache functionCache) { + if (!(functionCache instanceof ParallelFunctionCache)) { + throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache); + } + this.functionExecutor = (ParallelFunctionCache) functionCache; + } + + @Override + public String getStatistics() { + return "MultidirectionalSearchOptimizer[stepCount=" + stepCount + + ", reflectionAcceptance=" + reflectionAcceptance + + ", expansionAcceptance=" + expansionAcceptance + + ", coordinateAcceptance=" + coordinateAcceptance + + ", reductionFallback=" + reductionFallback; + } + + @Override + public void resetStatistics() { + stepCount = 0; + reflectionAcceptance = 0; + expansionAcceptance = 0; + coordinateAcceptance = 0; + reductionFallback = 0; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java b/core/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java new file mode 100644 index 00000000..1a7df333 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java @@ -0,0 +1,95 @@ +package net.sf.openrocket.optimization.general.multidim; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.util.MathUtil; + +/** + * A helper class to create search patterns. + * + * @author Sampo Niskanen + */ +public class SearchPattern { + + /** + * Create a square search pattern with the specified dimensionality. + * + * @param dimensionality the dimensionality + */ + public static List square(int dimensionality) { + List pattern = new ArrayList(dimensionality); + + for (int i = 0; i < dimensionality; i++) { + double[] p = new double[dimensionality]; + p[i] = 1.0; + pattern.add(new Point(p)); + } + return pattern; + } + + + + /** + * Create a regular simplex search pattern with the specified dimensionality. + * + * @param dimensionality the dimensionality + */ + public static List regularSimplex(int dimensionality) { + if (dimensionality <= 0) { + throw new IllegalArgumentException("Illegal dimensionality " + dimensionality); + } + + List pattern = new ArrayList(dimensionality); + + double[] coordinates = new double[dimensionality]; + double dot = -1.0 / dimensionality; + + /* + * First construct an origin-centered regular simplex. + * http://en.wikipedia.org/wiki/Simplex#Cartesian_coordinates_for_regular_n-dimensional_simplex_in_Rn + */ + + for (int i = 0; i < dimensionality; i++) { + // Compute the next point coordinate + double value = 1; + + for (int j = 0; j < i; j++) { + value -= MathUtil.pow2(coordinates[j]); + } + value = MathUtil.safeSqrt(value); + + coordinates[i] = value; + pattern.add(new Point(coordinates)); + + // Compute the i-coordinate for all next points + value = dot; + for (int j = 0; j < i; j++) { + value -= MathUtil.pow2(coordinates[j]); + } + value = value / coordinates[i]; + + coordinates[i] = value; + } + + // Minimum point + Point min = pattern.get(dimensionality - 1); + min = min.set(dimensionality - 1, -min.get(dimensionality - 1)); + + + /* + * Shift simplex to have a corner at the origin and scale to unit length. + */ + if (dimensionality > 1) { + double scale = 1.0 / (pattern.get(1).sub(pattern.get(0)).length()); + for (int i = 0; i < dimensionality; i++) { + Point p = pattern.get(i); + p = p.sub(min).mul(scale); + pattern.set(i, p); + } + } + + return pattern; + } +} diff --git a/core/src/net/sf/openrocket/optimization/general/onedim/GoldenSectionSearchOptimizer.java b/core/src/net/sf/openrocket/optimization/general/onedim/GoldenSectionSearchOptimizer.java new file mode 100644 index 00000000..906eea57 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/general/onedim/GoldenSectionSearchOptimizer.java @@ -0,0 +1,272 @@ +package net.sf.openrocket.optimization.general.onedim; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.FunctionCache; +import net.sf.openrocket.optimization.general.FunctionOptimizer; +import net.sf.openrocket.optimization.general.OptimizationController; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.ParallelFunctionCache; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Statistics; + +/** + * An implementation of a one-dimensional golden section search method + * (see e.g. Nonlinear programming, Bazaraa, Sherall, Shetty, 2nd edition, p. 270) + *

+ * This implementation attempts to guess future evaluations and computes them in parallel + * with the next point. + *

+ * The optimization can be aborted by interrupting the current thread. + */ +public class GoldenSectionSearchOptimizer implements FunctionOptimizer, Statistics { + private static final LogHelper log = Application.getLogger(); + + private static final double ALPHA = (Math.sqrt(5) - 1) / 2.0; + + + private ParallelFunctionCache functionExecutor; + + private Point current = null; + + private int guessSuccess = 0; + private int guessFailure = 0; + + + /** + * Construct an optimizer with no function executor. + */ + public GoldenSectionSearchOptimizer() { + // No-op + } + + /** + * Construct an optimizer. + * + * @param functionExecutor the function executor. + */ + public GoldenSectionSearchOptimizer(ParallelFunctionCache functionExecutor) { + super(); + this.functionExecutor = functionExecutor; + } + + @Override + public void optimize(Point initial, OptimizationController control) throws OptimizationException { + + if (initial.dim() != 1) { + throw new IllegalArgumentException("Only single-dimensional optimization supported, dim=" + + initial.dim()); + } + + log.info("Starting golden section search for optimization"); + + Point guessAC = null; + Point guessBD = null; + + try { + boolean guessedAC; + + Point previous = p(0); + double previousValue = Double.NaN; + current = previous; + double currentValue = Double.NaN; + + /* + * Initialize the points + computation. + */ + Point a = p(0); + Point d = p(1.0); + Point b = section1(a, d); + Point c = section2(a, d); + + functionExecutor.compute(a); + functionExecutor.compute(d); + functionExecutor.compute(b); + functionExecutor.compute(c); + + // Wait for points a and d, which normally are already precomputed + functionExecutor.waitFor(a); + functionExecutor.waitFor(d); + + boolean continueOptimization = true; + while (continueOptimization) { + + /* + * Get values at A & D for guessing. + * These are pre-calculated except during the first step. + */ + double fa, fd; + fa = functionExecutor.getValue(a); + fd = functionExecutor.getValue(d); + + + /* + * Start calculating possible two next points. The order of evaluation + * is selected based on the function values at A and D. + */ + guessAC = section1(a, c); + guessBD = section2(b, d); + if (Double.isNaN(fd) || fa < fd) { + guessedAC = true; + functionExecutor.compute(guessAC); + functionExecutor.compute(guessBD); + } else { + guessedAC = false; + functionExecutor.compute(guessBD); + functionExecutor.compute(guessAC); + } + + + /* + * Get values at B and C. + */ + double fb, fc; + functionExecutor.waitFor(b); + functionExecutor.waitFor(c); + fb = functionExecutor.getValue(b); + fc = functionExecutor.getValue(c); + + double min = MathUtil.min(fa, fb, fc, fd); + if (Double.isNaN(min)) { + throw new OptimizationException("Unable to compute initial function values"); + } + + + /* + * Update previous and current values for step control. + */ + previousValue = currentValue; + currentValue = min; + previous = current; + if (min == fa) { + current = a; + } else if (min == fb) { + current = b; + } else if (min == fc) { + current = c; + } else { + current = d; + } + + + /* + * Select next positions. These are already being calculated in the background + * as guessAC and guessBD. + */ + if (min == fa || min == fb) { + d = c; + c = b; + b = guessAC; + functionExecutor.abort(guessBD); + guessBD = null; + log.debug("Selecting A-C region, a=" + a.get(0) + " c=" + c.get(0)); + if (guessedAC) { + guessSuccess++; + } else { + guessFailure++; + } + } else { + a = b; + b = c; + c = guessBD; + functionExecutor.abort(guessAC); + guessAC = null; + log.debug("Selecting B-D region, b=" + b.get(0) + " d=" + d.get(0)); + if (!guessedAC) { + guessSuccess++; + } else { + guessFailure++; + } + } + + + /* + * Check optimization control. + */ + continueOptimization = control.stepTaken(previous, previousValue, + current, currentValue, c.get(0) - a.get(0)); + + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + } + + + } catch (InterruptedException e) { + log.info("Optimization was interrupted with InterruptedException"); + } + + if (guessAC != null) { + functionExecutor.abort(guessAC); + } + if (guessBD != null) { + functionExecutor.abort(guessBD); + } + + + log.info("Finishing optimization at point " + getOptimumPoint() + " value " + getOptimumValue()); + log.info("Optimization statistics: " + getStatistics()); + } + + + private Point p(double v) { + return new Point(v); + } + + + private Point section1(Point a, Point b) { + double va = a.get(0); + double vb = b.get(0); + return p(va + (1 - ALPHA) * (vb - va)); + } + + private Point section2(Point a, Point b) { + double va = a.get(0); + double vb = b.get(0); + return p(va + ALPHA * (vb - va)); + } + + + + @Override + public Point getOptimumPoint() { + return current; + } + + + @Override + public double getOptimumValue() { + if (getOptimumPoint() == null) { + return Double.NaN; + } + return functionExecutor.getValue(getOptimumPoint()); + } + + @Override + public FunctionCache getFunctionCache() { + return functionExecutor; + } + + @Override + public void setFunctionCache(FunctionCache functionCache) { + if (!(functionCache instanceof ParallelFunctionCache)) { + throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache); + } + this.functionExecutor = (ParallelFunctionCache) functionCache; + } + + @Override + public String getStatistics() { + return String.format("Guess hit rate %d/%d = %.3f", guessSuccess, guessSuccess + guessFailure, + ((double) guessSuccess) / (guessSuccess + guessFailure)); + } + + @Override + public void resetStatistics() { + guessSuccess = 0; + guessFailure = 0; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java new file mode 100644 index 00000000..2acdb049 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java @@ -0,0 +1,41 @@ +package net.sf.openrocket.optimization.rocketoptimization; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.unit.UnitGroup; + +/** + * A parameter of a rocket or simulation that can be optimized + * (for example max. altitude or velocity). + * + * @author Sampo Niskanen + */ +public interface OptimizableParameter { + + /** + * Return the label name for this optimization parameter. + * + * @return the name for the optimization parameter (e.g. "Flight altitude") + */ + public String getName(); + + /** + * Compute the value for this optimization parameter for the simulation. + * The return value can be any double value. + *

+ * This method can return NaN in case of a problem computing + * + * @param simulation the simulation + * @return the parameter value (any double value) + * @throws OptimizationException if an error occurs preventing the optimization from continuing + */ + public double computeValue(Simulation simulation) throws OptimizationException, InterruptedException; + + + /** + * Return the unit group associated with the computed value. + * @return the unit group of the computed value. + */ + public UnitGroup getUnitGroup(); + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java new file mode 100644 index 00000000..f7884046 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.optimization.rocketoptimization; + +/** + * A goal for an optimization process, for example minimizing, maximizing or seeking + * a specific parameter value. + * + * @author Sampo Niskanen + */ +public interface OptimizationGoal { + + /** + * Compute a value which, when minimized, yields the desired goal of the + * optimization problem. + * + * @param value the function actual value + * @return the value to minimize to reach the goal + */ + double getMinimizationParameter(double value); + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java new file mode 100644 index 00000000..2f224269 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java @@ -0,0 +1,186 @@ +package net.sf.openrocket.optimization.rocketoptimization; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.Function; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.Pair; + +/** + * A Function that optimizes a specific RocketOptimizationParameter to some goal + * by modifying a base simulation using SimulationModifiers. + * + * @author Sampo Niskanen + */ +public class RocketOptimizationFunction implements Function { + private static final LogHelper log = Application.getLogger(); + + private static final double OUTSIDE_DOMAIN_SCALE = 1.0e200; + + /* + * NOTE: This class must be thread-safe!!! + */ + + private final Simulation baseSimulation; + private final OptimizableParameter parameter; + private final OptimizationGoal goal; + private final SimulationDomain domain; + private final SimulationModifier[] modifiers; + + + private final List listeners = new ArrayList(); + + + /** + * Sole constructor. + *

+ * The dimensionality of the resulting function is the same as the length of the + * modifiers array. + * + * @param baseSimulation the base simulation to modify + * @param parameter the rocket parameter to optimize + * @param goal the goal of the rocket parameter + * @param modifiers the modifiers that modify the simulation + */ + public RocketOptimizationFunction(Simulation baseSimulation, OptimizableParameter parameter, + OptimizationGoal goal, SimulationDomain domain, SimulationModifier... modifiers) { + this.baseSimulation = baseSimulation; + this.parameter = parameter; + this.goal = goal; + this.domain = domain; + this.modifiers = modifiers.clone(); + if (modifiers.length == 0) { + throw new IllegalArgumentException("No SimulationModifiers specified"); + } + } + + + @Override + public double evaluate(Point point) throws InterruptedException, OptimizationException { + + /* + * parameterValue is the computed parameter value (e.g. altitude) + * goalValue is the value that needs to be minimized + */ + double goalValue, parameterValue; + + log.debug("Computing optimization function value at point " + point); + + // Create the new simulation based on the point + double[] p = point.asArray(); + if (p.length != modifiers.length) { + throw new IllegalArgumentException("Point has length " + p.length + " while function has " + + modifiers.length + " simulation modifiers"); + } + + Simulation simulation = newSimulationInstance(baseSimulation); + for (int i = 0; i < modifiers.length; i++) { + modifiers[i].modify(simulation, p[i]); + } + + + // Check whether the point is within the simulation domain + Pair d = domain.getDistanceToDomain(simulation); + double distance = d.getU(); + Value referenceValue = d.getV(); + if (distance > 0 || Double.isNaN(distance)) { + if (Double.isNaN(distance)) { + goalValue = Double.MAX_VALUE; + } else { + goalValue = (distance + 1) * OUTSIDE_DOMAIN_SCALE; + } + log.debug("Optimization point is outside of domain, distance=" + distance + " goal function value=" + goalValue); + + fireEvent(simulation, point, referenceValue, null, goalValue); + + return goalValue; + } + + + // Compute the optimization value + parameterValue = parameter.computeValue(simulation); + goalValue = goal.getMinimizationParameter(parameterValue); + + if (Double.isNaN(goalValue)) { + log.warn("Computed goal value was NaN, baseSimulation=" + baseSimulation + " parameter=" + parameter + + " goal=" + goal + " modifiers=" + Arrays.toString(modifiers) + " simulation=" + simulation + + " parameter value=" + parameterValue); + goalValue = Double.MAX_VALUE; + } + + log.verbose("Parameter value at point " + point + " is " + parameterValue + ", goal function value=" + goalValue); + + fireEvent(simulation, point, referenceValue, new Value(parameterValue, parameter.getUnitGroup().getDefaultUnit()), + goalValue); + + return goalValue; + } + + + + + + /** + * Returns a new deep copy of the simulation and rocket. This methods performs + * synchronization on the simulation for thread protection. + *

+ * Note: This method is package-private for unit testing purposes. + * + * @return a new deep copy of the simulation and rocket + */ + Simulation newSimulationInstance(Simulation simulation) { + synchronized (baseSimulation) { + Rocket newRocket = simulation.getRocket().copyWithOriginalID(); + Simulation newSimulation = simulation.duplicateSimulation(newRocket); + return newSimulation; + } + } + + + /** + * Add a listener to this function. The listener will be notified each time the + * function is successfully evaluated. + *

+ * Note that the listener may be called from other threads and must be thread-safe! + * + * @param listener the listener to add. + */ + public void addRocketOptimizationListener(RocketOptimizationListener listener) { + listeners.add(listener); + } + + public void removeRocketOptimizationListener(RocketOptimizationListener listener) { + listeners.remove(listener); + } + + + + private void fireEvent(Simulation simulation, Point p, Value domainReference, Value parameterValue, double goalValue) + throws OptimizationException { + + if (listeners.isEmpty()) { + return; + } + + + Value[] values = new Value[p.dim()]; + for (int i = 0; i < values.length; i++) { + double value = modifiers[i].getCurrentSIValue(simulation); + UnitGroup unit = modifiers[i].getUnitGroup(); + values[i] = new Value(value, unit.getDefaultUnit()); + } + + for (RocketOptimizationListener l : listeners) { + l.evaluated(p, values, domainReference, parameterValue, goalValue); + } + } +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java new file mode 100644 index 00000000..05fc429b --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java @@ -0,0 +1,24 @@ +package net.sf.openrocket.optimization.rocketoptimization; + +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.unit.Value; + +/** + * A listener for the progress of rocket optimization. + * + * @author Sampo Niskanen + */ +public interface RocketOptimizationListener { + + /** + * Called after successful function evaluation. + * + * @param point the optimization point. + * @param state the values to which the rocket has been modified in SI units, in the order of "point". + * @param domainReference the domain reference description (or null if unavailable) + * @param parameterValue the parameter value (or NaN if unavailable) + * @param goalValue the goal value (return value of the function) + */ + public void evaluated(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue); + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java new file mode 100644 index 00000000..d2a204ec --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java @@ -0,0 +1,30 @@ +package net.sf.openrocket.optimization.rocketoptimization; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.Pair; + +/** + * An interface defining a function domain which limits allowed function values. + * + * @author Sampo Niskanen + */ +public interface SimulationDomain { + + /** + * Return a value determining whether the simulation is within the domain limits + * of an optimization process. If the returned value is negative or zero, the + * simulation is within the domain; if the value is positive, the returned value + * is an indication of how far from the domain the value is; if the returned value + * is NaN, the simulation is outside of the domain. + * + * @param simulation the simulation to check. + * @return A pair indicating the status. The first element is the reference value, + * a negative value or zero if the simulation is in the domain, + * a positive value or NaN if not. The second is the value + * indication of the domain (may be null). + */ + public Pair getDistanceToDomain(Simulation simulation); + + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java new file mode 100644 index 00000000..6ed85a6e --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java @@ -0,0 +1,110 @@ +package net.sf.openrocket.optimization.rocketoptimization; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.ChangeSource; + +/** + * An interface what modifies a single parameter in a rocket simulation + * based on a double value in the range [0...1]. + *

+ * The implementation must fire change events when the minimum and maximum ranges + * are modified, NOT when the actual modified value changes. + * + * @author Sampo Niskanen + */ +public interface SimulationModifier extends ChangeSource { + + /** + * Return a short name describing this modifier. + * @return a name describing this modifier. + */ + public String getName(); + + /** + * Return a longer description describing this modifiers. + * @return a description of the modifier. + */ + public String getDescription(); + + /** + * Return the object this modifier is related to. This is for example the + * rocket component this modifier is modifying. This object can be used by a + * UI to group related modifiers. + * + * @return the object this modifier is related to, or null. + */ + public Object getRelatedObject(); + + + /** + * Return the current value of the modifier in SI units. + * @return the current value of this parameter in SI units. + * @throws OptimizationException if fetching the current value fails + */ + public double getCurrentSIValue(Simulation simulation) throws OptimizationException; + + + /** + * Return the minimum value (corresponding to scaled value 0) in SI units. + * @return the value corresponding to scaled value 0. + */ + public double getMinValue(); + + /** + * Set the minimum value (corresponding to scaled value 0) in SI units. + * @param value the value corresponding to scaled value 0. + */ + public void setMinValue(double value); + + + /** + * Return the maximum value (corresponding to scaled value 1) in SI units. + * @return the value corresponding to scaled value 1. + */ + public double getMaxValue(); + + /** + * Set the maximum value (corresponding to scaled value 1) in SI units. + * @param value the value corresponding to scaled value 1. + */ + public void setMaxValue(double value); + + + /** + * Return the unit group used for the values returned by {@link #getCurrentSIValue(Simulation)} etc. + * @return the unit group + */ + public UnitGroup getUnitGroup(); + + + /** + * Return the current scaled value. This is normally within the range [0...1], but + * can be outside the range if the current value is outside of the min and max values. + * + * @return the current value of this parameter (normally between [0 ... 1]) + * @throws OptimizationException if fetching the current value fails + */ + public double getCurrentScaledValue(Simulation simulation) throws OptimizationException; + + + + /** + * Modify the specified simulation to the corresponding parameter value. + * + * @param simulation the simulation to modify + * @param scaledValue the scaled value in the range [0...1] + * @throws OptimizationException if the modification fails + */ + public void modify(Simulation simulation, double scaledValue) throws OptimizationException; + + + /** + * Compare whether this SimulationModifier is equivalent to another simulation modifier. + * "Equivalent" means that the simulation modifier corresponds to the same modification in + * another rocket instance (e.g. the same modification on another rocket component that + * has the same component ID). + */ + public boolean equals(Object obj); +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java new file mode 100644 index 00000000..fac678ea --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java @@ -0,0 +1,20 @@ +package net.sf.openrocket.optimization.rocketoptimization.domains; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.Pair; + +/** + * A simulation domain that includes all points in the domain. + * + * @author Sampo Niskanen + */ +public class IdentitySimulationDomain implements SimulationDomain { + + @Override + public Pair getDistanceToDomain(Simulation simulation) { + return new Pair(-1.0, null); + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java new file mode 100644 index 00000000..631b45c9 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java @@ -0,0 +1,139 @@ +package net.sf.openrocket.optimization.rocketoptimization.domains; + +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.unit.Value; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Pair; + +/** + * A simulation domain that limits the required stability of the rocket. + * + * @author Sampo Niskanen + */ +public class StabilityDomain implements SimulationDomain { + + private final double minimum; + private final boolean minAbsolute; + private final double maximum; + private final boolean maxAbsolute; + + + /** + * Sole constructor. + * + * @param minimum minimum stability requirement (or NaN for no limit) + * @param minAbsolute true if minimum is an absolute SI measurement, + * false if it is relative to the rocket caliber + * @param maximum maximum stability requirement (or NaN for no limit) + * @param maxAbsolute true if maximum is an absolute SI measurement, + * false if it is relative to the rocket caliber + */ + public StabilityDomain(double minimum, boolean minAbsolute, double maximum, boolean maxAbsolute) { + super(); + this.minimum = minimum; + this.minAbsolute = minAbsolute; + this.maximum = maximum; + this.maxAbsolute = maxAbsolute; + } + + + + + @Override + public Pair getDistanceToDomain(Simulation simulation) { + Coordinate cp, cg; + double cpx, cgx; + double absolute; + double relative; + + /* + * These are instantiated each time because this class must be thread-safe. + * Caching would in any case be inefficient since the rocket changes all the time. + */ + AerodynamicCalculator aerodynamicCalculator = new BarrowmanCalculator(); + MassCalculator massCalculator = new BasicMassCalculator(); + + + Configuration configuration = simulation.getConfiguration(); + FlightConditions conditions = new FlightConditions(configuration); + conditions.setMach(Application.getPreferences().getDefaultMach()); + conditions.setAOA(0); + conditions.setRollRate(0); + + // TODO: HIGH: This re-calculates the worst theta value every time + cp = aerodynamicCalculator.getWorstCP(configuration, conditions, null); + cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); + + if (cp.weight > 0.000001) + cpx = cp.x; + else + cpx = Double.NaN; + + if (cg.weight > 0.000001) + cgx = cg.x; + else + cgx = Double.NaN; + + + // Calculate the reference (absolute or relative) + absolute = cpx - cgx; + + double diameter = 0; + for (RocketComponent c : configuration) { + if (c instanceof SymmetricComponent) { + double d1 = ((SymmetricComponent) c).getForeRadius() * 2; + double d2 = ((SymmetricComponent) c).getAftRadius() * 2; + diameter = MathUtil.max(diameter, d1, d2); + } + } + relative = absolute / diameter; + + + Value desc; + if (minAbsolute && maxAbsolute) { + desc = new Value(absolute, UnitGroup.UNITS_LENGTH); + } else { + desc = new Value(relative, UnitGroup.UNITS_STABILITY_CALIBERS); + } + + double ref; + if (minAbsolute) { + ref = minimum - absolute; + if (ref > 0) { + return new Pair(ref, desc); + } + } else { + ref = minimum - relative; + if (ref > 0) { + return new Pair(ref, desc); + } + } + + if (maxAbsolute) { + ref = absolute - maximum; + if (ref > 0) { + return new Pair(ref, desc); + } + } else { + ref = relative - maximum; + if (ref > 0) { + return new Pair(ref, desc); + } + } + + return new Pair(0.0, desc); + } +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java new file mode 100644 index 00000000..e9576eea --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java @@ -0,0 +1,18 @@ +package net.sf.openrocket.optimization.rocketoptimization.goals; + +import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; + +/** + * An optimization goal that maximizes a function value. The method simply + * returns the opposite of the function value. + * + * @author Sampo Niskanen + */ +public class MaximizationGoal implements OptimizationGoal { + + @Override + public double getMinimizationParameter(double value) { + return -value; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java new file mode 100644 index 00000000..c6f8fc60 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java @@ -0,0 +1,18 @@ +package net.sf.openrocket.optimization.rocketoptimization.goals; + +import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; + +/** + * An optimization goal that minimizes a function value. The method simply + * returns the function value. + * + * @author Sampo Niskanen + */ +public class MinimizationGoal implements OptimizationGoal { + + @Override + public double getMinimizationParameter(double value) { + return value; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java new file mode 100644 index 00000000..34d433ab --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java @@ -0,0 +1,30 @@ +package net.sf.openrocket.optimization.rocketoptimization.goals; + +import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; + + +/** + * An optimization goal that seeks for a specific function value. + * The method returns the Euclidic distance from the desired value. + * + * @author Sampo Niskanen + */ +public class ValueSeekGoal implements OptimizationGoal { + + private final double goal; + + /** + * Sole constructor. + * + * @param goal the function value to optimize towards. + */ + public ValueSeekGoal(double goal) { + this.goal = goal; + } + + @Override + public double getMinimizationParameter(double value) { + return Math.abs(value - goal); + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java new file mode 100644 index 00000000..1d8c0fd1 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java @@ -0,0 +1,211 @@ +package net.sf.openrocket.optimization.rocketoptimization.modifiers; + +import java.util.ArrayList; +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.StateChangeListener; + +/** + * An abstract implementation of the SimulationModifier interface. An implementation + * needs only to implement the {@link #getCurrentSIValue(Simulation)} and + * {@link #modify(net.sf.openrocket.document.Simulation, double)} methods. + * + * @author Sampo Niskanen + */ +public abstract class AbstractSimulationModifier implements SimulationModifier { + + private final String name; + private final String description; + private final Object relatedObject; + private final UnitGroup unitGroup; + + private double minValue = 0.0; + private double maxValue = 1.0; + + private final List listeners = new ArrayList(); + + + /** + * Sole constructor. + * + * @param modifierName the name of this modifier (returned by {@link #getName()}) + * @param modifierDescription the description of this modifier (returned by {@link #getDescription()}) + * @param relatedObject the related object (returned by {@link #getRelatedObject()}) + * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) + */ + public AbstractSimulationModifier(String modifierName, String modifierDescription, Object relatedObject, + UnitGroup unitGroup) { + this.name = modifierName; + this.description = modifierDescription; + this.relatedObject = relatedObject; + this.unitGroup = unitGroup; + + if (this.name == null || this.description == null || this.relatedObject == null || this.unitGroup == null) { + throw new IllegalArgumentException("null value provided:" + + " name=" + this.name + " description=" + description + " relatedObject=" + relatedObject + + " unitGroup=" + unitGroup); + } + } + + + @Override + public String getName() { + return name; + } + + @Override + public String getDescription() { + return description; + } + + @Override + public Object getRelatedObject() { + return relatedObject; + } + + @Override + public double getCurrentScaledValue(Simulation simulation) throws OptimizationException { + double value = getCurrentSIValue(simulation); + return toScaledValue(value); + } + + + + /** + * Returns the scaled value (normally within [0...1]). If the min...max range is singular, + * this method returns 0.0, 1.0 or 0.5 depending on whether the value is less than, + * greater than or equal to the limit. + * + * @param value the value in SI units + * @return the value in scaled range (normally within [0...1]) + */ + protected double toScaledValue(double value) { + if (MathUtil.equals(minValue, maxValue)) { + if (value > maxValue) + return 1.0; + if (value < minValue) + return 0.0; + return 0.5; + } + + return MathUtil.map(value, minValue, maxValue, 0.0, 1.0); + } + + + /** + * Returns the base value (in SI units). + * + * @param value the value in scaled range (normally within [0...1]) + * @return the value in SI units + */ + protected double toBaseValue(double value) { + return MathUtil.map(value, 0.0, 1.0, minValue, maxValue); + } + + + + @Override + public double getMinValue() { + return minValue; + } + + @Override + public void setMinValue(double value) { + if (MathUtil.equals(minValue, value)) + return; + this.minValue = value; + if (maxValue < minValue) + maxValue = minValue; + fireChangeEvent(); + } + + @Override + public double getMaxValue() { + return maxValue; + } + + @Override + public void setMaxValue(double value) { + if (MathUtil.equals(maxValue, value)) + return; + this.maxValue = value; + if (minValue > maxValue) + minValue = maxValue; + fireChangeEvent(); + } + + @Override + public UnitGroup getUnitGroup() { + return unitGroup; + } + + + @Override + public void addChangeListener(EventListener listener) { + listeners.add(listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listeners.remove(listener); + } + + + /** + * Fire a change event to the listeners. + */ + protected void fireChangeEvent() { + EventObject event = new EventObject(this); + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] list = listeners.toArray(new EventListener[0]); + for (EventListener l : list) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(event); + } + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + AbstractSimulationModifier other = (AbstractSimulationModifier) obj; + if (!this.description.equals(other.description)) + return false; + if (!this.name.equals(other.name)) + return false; + if (!this.relatedObject.equals(other.relatedObject)) + return false; + if (!this.unitGroup.equals(other.unitGroup)) + return false; + + return true; + } + + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (description.hashCode()); + result = prime * result + (name.hashCode()); + result = prime * result + (relatedObject.hashCode()); + result = prime * result + (unitGroup.hashCode()); + return result; + } + + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java new file mode 100644 index 00000000..bd80889a --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java @@ -0,0 +1,50 @@ +package net.sf.openrocket.optimization.rocketoptimization.modifiers; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.unit.UnitGroup; + +/** + * A generic simulation modifier that modifies a value of a certain RocketComponent + * based on the component's ID and the value name. + * + * @author Sampo Niskanen + */ +public class GenericComponentModifier extends GenericModifier { + + private final Class componentClass; + private final String componentId; + + /** + * Sole constructor. + * + * @param modifierName the name of this modifier (returned by {@link #getName()}) + * @param modifierDescription the description of this modifier (returned by {@link #getDescription()}) + * @param relatedObject the related object (returned by {@link #getRelatedObject()}) + * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) + * @param multiplier the multiplier by which the value returned by the getter is multiplied + * to obtain the desired value + * @param componentClass the RocketComponent class type that is being modified + * @param componentId the ID of the component to modify + * @param methodName the base name of the getter/setter methods (without "get"/"set") + */ + public GenericComponentModifier(String modifierName, String modifierDescription, Object relatedObject, UnitGroup unitGroup, + double multiplier, Class componentClass, String componentId, String methodName) { + super(modifierName, modifierDescription, relatedObject, unitGroup, multiplier, componentClass, methodName); + + this.componentClass = componentClass; + this.componentId = componentId; + } + + @Override + protected RocketComponent getModifiedObject(Simulation simulation) throws OptimizationException { + RocketComponent c = simulation.getRocket().findComponent(componentId); + if (c == null) { + throw new OptimizationException("Could not find component of type " + componentClass.getSimpleName() + + " with correct ID"); + } + return c; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java new file mode 100644 index 00000000..07724c8f --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java @@ -0,0 +1,106 @@ +package net.sf.openrocket.optimization.rocketoptimization.modifiers; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Reflection.Method; + +/** + * A generic SimulationModifier that uses reflection to get and set a double value. + * Implementations need to implement the {@link #getModifiedObject(Simulation)} method + * to return which object is modified. + * + * @author Sampo Niskanen + */ +public abstract class GenericModifier extends AbstractSimulationModifier { + private static final LogHelper log = Application.getLogger(); + + private final double multiplier; + + private final Class modifiedClass; + private final String methodName; + + private final Method getter; + private final Method setter; + + + /** + * Sole constructor. + * + * @param modifierName the name of this modifier (returned by {@link #getName()}) + * @param modifierDescription the description of this modifier (returned by {@link #getDescription()}) + * @param relatedObject the related object (returned by {@link #getRelatedObject()}) + * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) + * @param multiplier the multiplier by which the value returned by the getter is multiplied + * to obtain the desired value + * @param modifiedClass the class type that {@link #getModifiedObject(Simulation)} returns + * @param methodName the base name of the getter/setter methods (without "get"/"set") + */ + public GenericModifier(String modifierName, String modifierDescription, Object relatedObject, UnitGroup unitGroup, + double multiplier, Class modifiedClass, String methodName) { + super(modifierName, modifierDescription, relatedObject, unitGroup); + this.multiplier = multiplier; + + this.modifiedClass = modifiedClass; + this.methodName = methodName; + + if (MathUtil.equals(multiplier, 0)) { + throw new IllegalArgumentException("multiplier is zero"); + } + + try { + methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1); + getter = new Method(modifiedClass.getMethod("get" + methodName)); + setter = new Method(modifiedClass.getMethod("set" + methodName, double.class)); + } catch (SecurityException e) { + throw new BugException("Trying to find method get/set" + methodName + " in class " + modifiedClass, e); + } catch (NoSuchMethodException e) { + throw new BugException("Trying to find method get/set" + methodName + " in class " + modifiedClass, e); + } + } + + + + @Override + public double getCurrentSIValue(Simulation simulation) throws OptimizationException { + T modifiable = getModifiedObject(simulation); + if (modifiable == null) { + throw new OptimizationException("BUG: getModifiedObject() returned null"); + } + return ((Double) getter.invoke(modifiable)) * multiplier; + } + + + @Override + public void modify(Simulation simulation, double scaledValue) throws OptimizationException { + T modifiable = getModifiedObject(simulation); + if (modifiable == null) { + throw new OptimizationException("BUG: getModifiedObject() returned null"); + } + double siValue = toBaseValue(scaledValue) / multiplier; + log.verbose("Setting setter=" + setter + " modifiable=" + modifiable + " siValue=" + siValue + "scaledValue=" + scaledValue); + setter.invoke(modifiable, siValue); + } + + + /** + * Return the object from the simulation that will be modified. + * @param simulation the simulation + * @return the object to modify + * + * @throws OptimizationException if the object cannot be found + */ + protected abstract T getModifiedObject(Simulation simulation) throws OptimizationException; + + + + @Override + public String toString() { + return "GenericModifier[modifiedClass=" + modifiedClass.getCanonicalName() + ", methodName=" + methodName + ", multiplier=" + multiplier + "]"; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/DeploymentVelocityParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/DeploymentVelocityParameter.java new file mode 100644 index 00000000..0add6648 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/DeploymentVelocityParameter.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.RecoveryDeviceDeploymentEndListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the total flight time. + * + * @author Sampo Niskanen + */ +public class DeploymentVelocityParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected SimulationListener[] getSimulationListeners() { + return new SimulationListener[] { new RecoveryDeviceDeploymentEndListener() }; + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_VELOCITY_TOTAL); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_VELOCITY; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java new file mode 100644 index 00000000..4effe72b --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the speed the rocket hits the ground. + * + * @author Sampo Niskanen + */ +public class GroundHitVelocityParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_VELOCITY_TOTAL); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_VELOCITY; + } + + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java new file mode 100644 index 00000000..cb387d71 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the distance where a rocket lands. + * + * @author Sampo Niskanen + */ +public class LandingDistanceParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_POSITION_XY); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_DISTANCE; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java new file mode 100644 index 00000000..eb6e7a61 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the maximum acceleration during a simulated flight. + * + * @author Sampo Niskanen + */ +public class MaximumAccelerationParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected SimulationListener[] getSimulationListeners() { + return new SimulationListener[] { new ApogeeEndListener() }; + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getMaximum(FlightDataType.TYPE_ACCELERATION_TOTAL); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_ACCELERATION; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java new file mode 100644 index 00000000..f427d7fc --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the maximum altitude of a simulated flight. + * + * @author Sampo Niskanen + */ +public class MaximumAltitudeParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected SimulationListener[] getSimulationListeners() { + return new SimulationListener[] { new ApogeeEndListener() }; + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_DISTANCE; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java new file mode 100644 index 00000000..3062e4b8 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java @@ -0,0 +1,40 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the maximum velocity during a simulated flight. + * + * @author Sampo Niskanen + */ +public class MaximumVelocityParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected SimulationListener[] getSimulationListeners() { + return new SimulationListener[] { new ApogeeEndListener() }; + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getMaximum(FlightDataType.TYPE_VELOCITY_TOTAL); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_VELOCITY; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/SimulationBasedParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/SimulationBasedParameter.java new file mode 100644 index 00000000..e814dd30 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/SimulationBasedParameter.java @@ -0,0 +1,82 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import java.util.Arrays; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.exception.MotorIgnitionException; +import net.sf.openrocket.simulation.exception.SimulationCalculationException; +import net.sf.openrocket.simulation.exception.SimulationCancelledException; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationLaunchException; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.simulation.listeners.system.InterruptListener; +import net.sf.openrocket.startup.Application; + +/** + * An abstract optimization parameter that simulates a rocket flight and obtains + * a value from the simulation result. + * + * @author Sampo Niskanen + */ +public abstract class SimulationBasedParameter implements OptimizableParameter { + + private static final LogHelper log = Application.getLogger(); + + @Override + public double computeValue(Simulation simulation) throws OptimizationException, InterruptedException { + try { + log.debug("Running simulation for " + getName()); + + SimulationListener[] listeners = getSimulationListeners(); + listeners = Arrays.copyOf(listeners, listeners.length + 1); + listeners[listeners.length - 1] = new InterruptListener(); + simulation.simulate(listeners); + + double value = getResultValue(simulation.getSimulatedData()); + log.debug("Parameter '" + getName() + " was " + value); + return value; + } catch (MotorIgnitionException e) { + // A problem with motor ignition will cause optimization to fail + throw new OptimizationException(e); + } catch (SimulationLaunchException e) { + // Other launch exceptions result in illegal value + return Double.NaN; + } catch (SimulationCalculationException e) { + // Calculation errors result in illegal value + return Double.NaN; + } catch (SimulationCancelledException e) { + // Simulation cancellation stops the optimization + throw (InterruptedException) new InterruptedException("Optimization was interrupted").initCause(e); + } catch (SimulationException e) { + // Other exceptions fail + throw new OptimizationException(e); + } + } + + + /** + * Return the optimization parameter from the simulation flight data. + * + * @param simulatedData the simulated data. + * @return the optimization parameter. + */ + protected abstract double getResultValue(FlightData simulatedData); + + /** + * Return an array of simulation listeners to provide to the simulation. + * This may include a listener that stops the simulation after the necessary value + * has been computed. + *

+ * This array should NOT contain InterruptListener, it will be added implicitly. + * + * @return an array of simulation listeners to include. + */ + protected SimulationListener[] getSimulationListeners() { + return new SimulationListener[0]; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java new file mode 100644 index 00000000..1d46f753 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java @@ -0,0 +1,110 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +/** + * An optimization parameter that computes either the absolute or relative stability of a rocket. + * + * @author Sampo Niskanen + */ +public class StabilityParameter implements OptimizableParameter { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + private final boolean absolute; + + public StabilityParameter(boolean absolute) { + this.absolute = absolute; + } + + + @Override + public String getName() { + return trans.get("name") + " (" + getUnitGroup().getDefaultUnit().getUnit() + ")"; + } + + @Override + public double computeValue(Simulation simulation) throws OptimizationException { + Coordinate cp, cg; + double cpx, cgx; + double stability; + + log.debug("Calculating stability of simulation, absolute=" + absolute); + + /* + * These are instantiated each time because this class must be thread-safe. + * Caching would in any case be inefficient since the rocket changes all the time. + */ + AerodynamicCalculator aerodynamicCalculator = new BarrowmanCalculator(); + MassCalculator massCalculator = new BasicMassCalculator(); + + + Configuration configuration = simulation.getConfiguration(); + FlightConditions conditions = new FlightConditions(configuration); + conditions.setMach(Application.getPreferences().getDefaultMach()); + conditions.setAOA(0); + conditions.setRollRate(0); + + cp = aerodynamicCalculator.getWorstCP(configuration, conditions, null); + cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); + + if (cp.weight > 0.000001) + cpx = cp.x; + else + cpx = Double.NaN; + + if (cg.weight > 0.000001) + cgx = cg.x; + else + cgx = Double.NaN; + + + // Calculate the reference (absolute or relative) + stability = cpx - cgx; + + if (!absolute) { + double diameter = 0; + for (RocketComponent c : configuration) { + if (c instanceof SymmetricComponent) { + double d1 = ((SymmetricComponent) c).getForeRadius() * 2; + double d2 = ((SymmetricComponent) c).getAftRadius() * 2; + diameter = MathUtil.max(diameter, d1, d2); + } + } + stability = stability / diameter; + } + + log.debug("Resulting stability is " + stability + ", absolute=" + absolute); + + return stability; + } + + @Override + public UnitGroup getUnitGroup() { + if (absolute) { + return UnitGroup.UNITS_LENGTH; + } else { + return UnitGroup.UNITS_STABILITY_CALIBERS; + } + } + +} diff --git a/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/TotalFlightTimeParameter.java b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/TotalFlightTimeParameter.java new file mode 100644 index 00000000..560227a2 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/rocketoptimization/parameters/TotalFlightTimeParameter.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.simulation.FlightData; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * An optimization parameter that computes the total flight time. + * + * @author Sampo Niskanen + */ +public class TotalFlightTimeParameter extends SimulationBasedParameter { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + protected double getResultValue(FlightData simulatedData) { + return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_TIME); + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_FLIGHT_TIME; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java b/core/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java new file mode 100644 index 00000000..7eadfb6a --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java @@ -0,0 +1,42 @@ +package net.sf.openrocket.optimization.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.DeploymentVelocityParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.GroundHitVelocityParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.LandingDistanceParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumAccelerationParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumAltitudeParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumVelocityParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.StabilityParameter; +import net.sf.openrocket.optimization.rocketoptimization.parameters.TotalFlightTimeParameter; + +/** + * Default implementation for optimization parameter service. + * + * @author Sampo Niskanen + */ +public class DefaultOptimizableParameterService implements OptimizableParameterService { + + @Override + public Collection getParameters(OpenRocketDocument document) { + List list = new ArrayList(); + + list.add(new MaximumAltitudeParameter()); + list.add(new MaximumVelocityParameter()); + list.add(new MaximumAccelerationParameter()); + list.add(new StabilityParameter(false)); + list.add(new StabilityParameter(true)); + list.add(new GroundHitVelocityParameter()); + list.add(new LandingDistanceParameter()); + list.add(new TotalFlightTimeParameter()); + list.add(new DeploymentVelocityParameter()); + + return list; + } + +} diff --git a/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java b/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java new file mode 100644 index 00000000..565330aa --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java @@ -0,0 +1,329 @@ +package net.sf.openrocket.optimization.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.optimization.rocketoptimization.modifiers.GenericComponentModifier; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.EllipticalFinSet; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.InternalComponent; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.Parachute; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.RecoveryDevice.DeployEvent; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.Streamer; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Reflection; +import net.sf.openrocket.util.Reflection.Method; + +public class DefaultSimulationModifierService implements SimulationModifierService { + + private static final Translator trans = Application.getTranslator(); + + private static final double DEFAULT_RANGE_MULTIPLIER = 2.0; + + + private static final Map, List> definitions = new HashMap, List>(); + static { + //addModifier("optimization.modifier.", unitGroup, multiplier, componentClass, methodName); + + /* + * Note: Each component type must contain only mutually exclusive variables. + * For example, body tube does not have inner diameter definition because it is + * defined by the outer diameter and thickness. + */ + + addModifier("optimization.modifier.nosecone.length", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Length"); + addModifier("optimization.modifier.nosecone.diameter", UnitGroup.UNITS_LENGTH, 2.0, NoseCone.class, "AftRadius", "isAftRadiusAutomatic"); + addModifier("optimization.modifier.nosecone.thickness", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Thickness", "isFilled"); + + addModifier("optimization.modifier.transition.length", UnitGroup.UNITS_LENGTH, 1.0, Transition.class, "Length"); + addModifier("optimization.modifier.transition.forediameter", UnitGroup.UNITS_LENGTH, 2.0, Transition.class, "ForeRadius", "isForeRadiusAutomatic"); + addModifier("optimization.modifier.transition.aftdiameter", UnitGroup.UNITS_LENGTH, 2.0, Transition.class, "AftRadius", "isAftRadiusAutomatic"); + addModifier("optimization.modifier.transition.thickness", UnitGroup.UNITS_LENGTH, 1.0, Transition.class, "Thickness", "isFilled"); + + addModifier("optimization.modifier.bodytube.length", UnitGroup.UNITS_LENGTH, 1.0, BodyTube.class, "Length"); + addModifier("optimization.modifier.bodytube.outerDiameter", UnitGroup.UNITS_LENGTH, 2.0, BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic"); + addModifier("optimization.modifier.bodytube.thickness", UnitGroup.UNITS_LENGTH, 1.0, BodyTube.class, "Thickness", "isFilled"); + + addModifier("optimization.modifier.trapezoidfinset.rootChord", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "RootChord"); + addModifier("optimization.modifier.trapezoidfinset.tipChord", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "TipChord"); + addModifier("optimization.modifier.trapezoidfinset.sweep", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "Sweep"); + addModifier("optimization.modifier.trapezoidfinset.height", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "Height"); + addModifier("optimization.modifier.finset.cant", UnitGroup.UNITS_ANGLE, 1.0, TrapezoidFinSet.class, "CantAngle"); + + addModifier("optimization.modifier.ellipticalfinset.length", UnitGroup.UNITS_LENGTH, 1.0, EllipticalFinSet.class, "Length"); + addModifier("optimization.modifier.ellipticalfinset.height", UnitGroup.UNITS_LENGTH, 1.0, EllipticalFinSet.class, "Height"); + addModifier("optimization.modifier.finset.cant", UnitGroup.UNITS_ANGLE, 1.0, EllipticalFinSet.class, "CantAngle"); + + addModifier("optimization.modifier.finset.cant", UnitGroup.UNITS_ANGLE, 1.0, FreeformFinSet.class, "CantAngle"); + + addModifier("optimization.modifier.launchlug.length", UnitGroup.UNITS_LENGTH, 1.0, LaunchLug.class, "Length"); + addModifier("optimization.modifier.launchlug.outerDiameter", UnitGroup.UNITS_LENGTH, 2.0, LaunchLug.class, "OuterRadius"); + addModifier("optimization.modifier.launchlug.thickness", UnitGroup.UNITS_LENGTH, 1.0, LaunchLug.class, "Thickness"); + + + addModifier("optimization.modifier.masscomponent.mass", UnitGroup.UNITS_MASS, 1.0, MassComponent.class, "ComponentMass"); + + addModifier("optimization.modifier.parachute.diameter", UnitGroup.UNITS_LENGTH, 1.0, Parachute.class, "Diameter"); + addModifier("optimization.modifier.parachute.coefficient", UnitGroup.UNITS_NONE, 1.0, Parachute.class, "CD"); + + addModifier("optimization.modifier.streamer.length", UnitGroup.UNITS_LENGTH, 1.0, Streamer.class, "StripLength"); + addModifier("optimization.modifier.streamer.width", UnitGroup.UNITS_LENGTH, 1.0, Streamer.class, "StripWidth"); + addModifier("optimization.modifier.streamer.aspectRatio", UnitGroup.UNITS_NONE, 1.0, Streamer.class, "AspectRatio"); + addModifier("optimization.modifier.streamer.coefficient", UnitGroup.UNITS_NONE, 1.0, Streamer.class, "CD", "isCDAutomatic"); + + } + + private static void addModifier(String modifierNameKey, UnitGroup unitGroup, double multiplier, + Class componentClass, String methodName) { + addModifier(modifierNameKey, unitGroup, multiplier, componentClass, methodName, null); + } + + private static void addModifier(String modifierNameKey, UnitGroup unitGroup, double multiplier, + Class componentClass, String methodName, String autoMethod) { + + String modifierDescriptionKey = modifierNameKey + ".desc"; + + List list = definitions.get(componentClass); + if (list == null) { + list = new ArrayList(); + definitions.put(componentClass, list); + } + + ModifierDefinition definition = new ModifierDefinition(modifierNameKey, modifierDescriptionKey, unitGroup, + multiplier, componentClass, methodName, autoMethod); + list.add(definition); + } + + + + + @Override + public Collection getModifiers(OpenRocketDocument document) { + List modifiers = new ArrayList(); + + Rocket rocket = document.getRocket(); + + // Simulation is used to calculate default min/max values + Simulation simulation = new Simulation(rocket); + simulation.getConfiguration().setMotorConfigurationID(null); + + for (RocketComponent c : rocket) { + + // Attribute modifiers + List list = definitions.get(c.getClass()); + if (list != null) { + for (ModifierDefinition def : list) { + + // Ignore modifier if value is set to automatic + if (def.autoMethod != null) { + Method m = Reflection.findMethod(c.getClass(), def.autoMethod); + if ((Boolean) m.invoke(c)) { + continue; + } + } + + SimulationModifier mod = new GenericComponentModifier( + trans.get(def.modifierNameKey), trans.get(def.modifierDescriptionKey), c, def.unitGroup, + def.multiplier, def.componentClass, c.getID(), def.methodName); + setDefaultMinMax(mod, simulation); + modifiers.add(mod); + } + } + + + // Add override modifiers if mass/CG is overridden + if (c.isMassOverridden()) { + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.rocketcomponent.overrideMass"), + trans.get("optimization.modifier.rocketcomponent.overrideMass.desc"), + c, UnitGroup.UNITS_MASS, + 1.0, c.getClass(), c.getID(), "OverrideMass"); + setDefaultMinMax(mod, simulation); + modifiers.add(mod); + } + if (c.isCGOverridden()) { + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.rocketcomponent.overrideCG"), + trans.get("optimization.modifier.rocketcomponent.overrideCG.desc"), + c, UnitGroup.UNITS_LENGTH, + 1.0, c.getClass(), c.getID(), "OverrideCGX"); + mod.setMinValue(0); + mod.setMaxValue(c.getLength()); + modifiers.add(mod); + } + + + // Conditional motor mount parameters + if (c instanceof MotorMount) { + MotorMount mount = (MotorMount) c; + if (mount.isMotorMount()) { + + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.motormount.overhang"), + trans.get("optimization.modifier.motormount.overhang.desc"), + c, UnitGroup.UNITS_LENGTH, + 1.0, c.getClass(), c.getID(), "MotorOverhang"); + setDefaultMinMax(mod, simulation); + modifiers.add(mod); + + mod = new GenericComponentModifier( + trans.get("optimization.modifier.motormount.delay"), + trans.get("optimization.modifier.motormount.delay.desc"), + c, UnitGroup.UNITS_SHORT_TIME, + 1.0, c.getClass(), c.getID(), "IgnitionDelay"); + mod.setMinValue(0); + mod.setMaxValue(5); + modifiers.add(mod); + + } + } + + + // Inner component positioning + if (c instanceof InternalComponent) { + RocketComponent parent = c.getParent(); + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.internalcomponent.position"), + trans.get("optimization.modifier.internalcomponent.position.desc"), + c, UnitGroup.UNITS_LENGTH, + 1.0, c.getClass(), c.getID(), "PositionValue"); + mod.setMinValue(0); + mod.setMaxValue(parent.getLength()); + modifiers.add(mod); + } + + + // Custom min/max for fin set position + if (c instanceof FinSet) { + RocketComponent parent = c.getParent(); + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.finset.position"), + trans.get("optimization.modifier.finset.position.desc"), + c, UnitGroup.UNITS_LENGTH, + 1.0, c.getClass(), c.getID(), "PositionValue"); + mod.setMinValue(0); + mod.setMaxValue(parent.getLength()); + modifiers.add(mod); + } + + + // Custom min/max for launch lug position + if (c instanceof LaunchLug) { + RocketComponent parent = c.getParent(); + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.launchlug.position"), + trans.get("optimization.modifier.launchlug.position.desc"), + c, UnitGroup.UNITS_LENGTH, + 1.0, c.getClass(), c.getID(), "PositionValue"); + mod.setMinValue(0); + mod.setMaxValue(parent.getLength()); + modifiers.add(mod); + } + + + // Recovery device deployment altitude and delay + if (c instanceof RecoveryDevice) { + RecoveryDevice device = (RecoveryDevice) c; + + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier.recoverydevice.deployDelay"), + trans.get("optimization.modifier.recoverydevice.deployDelay.desc"), + c, UnitGroup.UNITS_SHORT_TIME, + 1.0, c.getClass(), c.getID(), "DeployDelay"); + mod.setMinValue(0); + mod.setMaxValue(10); + modifiers.add(mod); + + if (device.getDeployEvent() == DeployEvent.ALTITUDE) { + mod = new GenericComponentModifier( + trans.get("optimization.modifier.recoverydevice.deployAltitude"), + trans.get("optimization.modifier.recoverydevice.deployAltitude.desc"), + c, UnitGroup.UNITS_DISTANCE, + 1.0, c.getClass(), c.getID(), "DeployAltitude"); + setDefaultMinMax(mod, simulation); + modifiers.add(mod); + } + } + + + // Conditional shape parameter of Transition + if (c instanceof Transition) { + Transition transition = (Transition) c; + Transition.Shape shape = transition.getType(); + if (shape.usesParameter()) { + SimulationModifier mod = new GenericComponentModifier( + trans.get("optimization.modifier." + c.getClass().getSimpleName().toLowerCase() + ".shapeparameter"), + trans.get("optimization.modifier." + c.getClass().getSimpleName().toLowerCase() + ".shapeparameter.desc"), + c, UnitGroup.UNITS_NONE, + 1.0, c.getClass(), c.getID(), "ShapeParameter"); + mod.setMinValue(shape.minParameter()); + mod.setMaxValue(shape.maxParameter()); + modifiers.add(mod); + } + } + } + + return modifiers; + } + + + private void setDefaultMinMax(SimulationModifier mod, Simulation simulation) { + try { + double current = mod.getCurrentSIValue(simulation); + mod.setMinValue(current / DEFAULT_RANGE_MULTIPLIER); + mod.setMaxValue(current * DEFAULT_RANGE_MULTIPLIER); + } catch (OptimizationException e) { + throw new BugException("Simulation modifier threw exception", e); + } + } + + + /* + * String modifierName, Object relatedObject, UnitGroup unitGroup, + double multiplier, Class componentClass, String componentId, String methodName + */ + + private static class ModifierDefinition { + private final String modifierNameKey; + private final String modifierDescriptionKey; + private final UnitGroup unitGroup; + private final double multiplier; + private final Class componentClass; + private final String methodName; + private final String autoMethod; + + + public ModifierDefinition(String modifierNameKey, String modifierDescriptionKey, UnitGroup unitGroup, + double multiplier, Class componentClass, String methodName, String autoMethod) { + this.modifierNameKey = modifierNameKey; + this.modifierDescriptionKey = modifierDescriptionKey; + this.unitGroup = unitGroup; + this.multiplier = multiplier; + this.componentClass = componentClass; + this.methodName = methodName; + this.autoMethod = autoMethod; + } + + } +} diff --git a/core/src/net/sf/openrocket/optimization/services/OptimizableParameterService.java b/core/src/net/sf/openrocket/optimization/services/OptimizableParameterService.java new file mode 100644 index 00000000..d7842e4d --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/services/OptimizableParameterService.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.optimization.services; + +import java.util.Collection; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; + +/** + * A service for generating rocket optimization parameters. + * + * @author Sampo Niskanen + */ +public interface OptimizableParameterService { + + /** + * Return all available rocket optimization parameters for this document. + * These should be new instances unless the parameter implementation is stateless. + * + * @param document the design document + * @return a collection of the rocket optimization parameters. + */ + public Collection getParameters(OpenRocketDocument document); + + +} diff --git a/core/src/net/sf/openrocket/optimization/services/OptimizationServiceHelper.java b/core/src/net/sf/openrocket/optimization/services/OptimizationServiceHelper.java new file mode 100644 index 00000000..152ccaf7 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/services/OptimizationServiceHelper.java @@ -0,0 +1,67 @@ +package net.sf.openrocket.optimization.services; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.ServiceLoader; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.util.TestRockets; + +public final class OptimizationServiceHelper { + + private OptimizationServiceHelper() { + // Prevent instantiation + } + + /** + * Return the simulation modifiers for an OpenRocketDocument. This queries the + * getModifiers() method from all available services and returns a collection of all + * the modifiers. + * + * @param document the OpenRocketDocument. + * @return a collection of all simulation modifiers applicable to the document. + */ + public static Collection getSimulationModifiers(OpenRocketDocument document) { + List list = new ArrayList(); + + ServiceLoader loader = ServiceLoader.load(SimulationModifierService.class); + for (SimulationModifierService service : loader) { + list.addAll(service.getModifiers(document)); + } + + return list; + } + + + + /** + * Return the optimization parameters for an OpenRocketDocument. This queries the + * getParameters() method from all available services and returns a collection of all + * the modifiers. + * + * @param document the OpenRocketDocument. + * @return a collection of all optimization parameters applicable to the document. + */ + public static Collection getOptimizableParameters(OpenRocketDocument document) { + List list = new ArrayList(); + + ServiceLoader loader = ServiceLoader.load(OptimizableParameterService.class); + for (OptimizableParameterService service : loader) { + list.addAll(service.getParameters(document)); + } + + return list; + } + + + public static void main(String[] args) { + Rocket r = TestRockets.makeBigBlue(); + OpenRocketDocument document = new OpenRocketDocument(r); + System.out.println("Simulation modifiers: " + getSimulationModifiers(document)); + System.out.println("Optimization parameters: " + getOptimizableParameters(document)); + } +} diff --git a/core/src/net/sf/openrocket/optimization/services/SimulationModifierService.java b/core/src/net/sf/openrocket/optimization/services/SimulationModifierService.java new file mode 100644 index 00000000..3f386379 --- /dev/null +++ b/core/src/net/sf/openrocket/optimization/services/SimulationModifierService.java @@ -0,0 +1,24 @@ +package net.sf.openrocket.optimization.services; + +import java.util.Collection; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; + +/** + * A service for generating simulation modifiers. + * + * @author Sampo Niskanen + */ +public interface SimulationModifierService { + + /** + * Return all available simulation modifiers for this document. + * + * @param document the design document + * @return a collection of the rocket optimization parameters. + */ + public Collection getModifiers(OpenRocketDocument document); + + +} diff --git a/core/src/net/sf/openrocket/preset/ComponentPreset.java b/core/src/net/sf/openrocket/preset/ComponentPreset.java new file mode 100644 index 00000000..87343e93 --- /dev/null +++ b/core/src/net/sf/openrocket/preset/ComponentPreset.java @@ -0,0 +1,73 @@ +package net.sf.openrocket.preset; + +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.rocketcomponent.RocketComponent; + +/** + * A model for a preset component. + *

+ * A preset component contains a component class type, manufacturer information, + * part information, and a method that returns a prototype of the preset component. + * + * @author Sampo Niskanen + */ +public abstract class ComponentPreset { + + private final Manufacturer manufacturer; + private final String partNo; + private final String partDescription; + private final RocketComponent prototype; + + + public ComponentPreset(Manufacturer manufacturer, String partNo, String partDescription, + RocketComponent prototype) { + this.manufacturer = manufacturer; + this.partNo = partNo; + this.partDescription = partDescription; + this.prototype = prototype.copy(); + + if (prototype.getParent() != null) { + throw new IllegalArgumentException("Prototype component cannot have a parent"); + } + if (prototype.getChildCount() > 0) { + throw new IllegalArgumentException("Prototype component cannot have children"); + } + } + + + /** + * Return the component class that this preset defines. + */ + public Class getComponentClass() { + return prototype.getClass(); + } + + /** + * Return the manufacturer of this preset component. + */ + public Manufacturer getManufacturer() { + return manufacturer; + } + + /** + * Return the part number. This is the part identifier (e.g. "BT-50"). + */ + public String getPartNo() { + return partNo; + } + + /** + * Return the part description. This is a longer description of the component. + */ + public String getPartDescription() { + return partDescription; + } + + /** + * Return a prototype component. This component may be modified freely. + */ + public RocketComponent getPrototype() { + return prototype.copy(); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/BodyComponent.java b/core/src/net/sf/openrocket/rocketcomponent/BodyComponent.java new file mode 100644 index 00000000..a102e87f --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/BodyComponent.java @@ -0,0 +1,79 @@ +package net.sf.openrocket.rocketcomponent; + + + +/** + * Class to represent a body object. The object can be described as a function of + * the cylindrical coordinates x and angle theta as r = f(x,theta). The component + * need not be symmetrical in any way (e.g. square tube, slanted cone etc). + * + * It defines the methods getRadius(x,theta) and getInnerRadius(x,theta), as well + * as get/setLength(). + * + * @author Sampo Niskanen + */ + +public abstract class BodyComponent extends ExternalComponent { + + /** + * Default constructor. Sets the relative position to POSITION_RELATIVE_AFTER, + * i.e. body components come after one another. + */ + public BodyComponent() { + super(RocketComponent.Position.AFTER); + } + + + /** + * Get the outer radius of the component at cylindrical coordinate (x,theta). + * + * Note that the return value may be negative for a slanted object. + * + * @param x Distance in x direction + * @param theta Angle about the x-axis + * @return Distance to the outer edge of the object + */ + public abstract double getRadius(double x, double theta); + + + /** + * Get the inner radius of the component at cylindrical coordinate (x,theta). + * + * Note that the return value may be negative for a slanted object. + * + * @param x Distance in x direction + * @param theta Angle about the x-axis + * @return Distance to the inner edge of the object + */ + public abstract double getInnerRadius(double x, double theta); + + + @Override + protected void loadFromPreset(RocketComponent preset) { + BodyComponent c = (BodyComponent) preset; + this.setLength(c.getLength()); + + super.loadFromPreset(preset); + } + + + + /** + * Sets the length of the body component. + *

+ * Note: This should be overridden by the subcomponents which need to call + * clearPreset(). (BodyTube allows changing length without resetting the preset.) + */ + public void setLength(double length) { + if (this.length == length) + return; + this.length = Math.max(length, 0); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + @Override + public boolean allowsChildren() { + return true; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java new file mode 100644 index 00000000..64c1b4a9 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/BodyTube.java @@ -0,0 +1,463 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +/** + * Rocket body tube component. Has only two parameters, a radius and length. + * + * @author Sampo Niskanen + */ + +public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial { + private static final Translator trans = Application.getTranslator(); + + private double outerRadius = 0; + private boolean autoRadius = false; // Radius chosen automatically based on parent component + + // When changing the inner radius, thickness is modified + + private boolean motorMount = false; + private HashMap ejectionDelays = new HashMap(); + private HashMap motors = new HashMap(); + private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC; + private double ignitionDelay = 0; + private double overhang = 0; + + + + public BodyTube() { + super(); + this.length = 8 * DEFAULT_RADIUS; + this.outerRadius = DEFAULT_RADIUS; + this.autoRadius = true; + } + + public BodyTube(double length, double radius) { + super(); + this.outerRadius = Math.max(radius, 0); + this.length = Math.max(length, 0); + } + + + public BodyTube(double length, double radius, boolean filled) { + this(length, radius); + this.filled = filled; + } + + public BodyTube(double length, double radius, double thickness) { + this(length, radius); + this.filled = false; + this.thickness = thickness; + } + + + /************ Get/set component parameter methods ************/ + + /** + * Return the outer radius of the body tube. + * + * @return the outside radius of the tube + */ + @Override + public double getOuterRadius() { + if (autoRadius) { + // Return auto radius from front or rear + double r = -1; + SymmetricComponent c = this.getPreviousSymmetricComponent(); + if (c != null) { + r = c.getFrontAutoRadius(); + } + if (r < 0) { + c = this.getNextSymmetricComponent(); + if (c != null) { + r = c.getRearAutoRadius(); + } + } + if (r < 0) + r = DEFAULT_RADIUS; + return r; + } + return outerRadius; + } + + + /** + * Set the outer radius of the body tube. If the radius is less than the wall thickness, + * the wall thickness is decreased accordingly of the value of the radius. + * This method sets the automatic radius off. + * + * @param radius the outside radius in standard units + */ + @Override + public void setOuterRadius(double radius) { + if ((this.outerRadius == radius) && (autoRadius == false)) + return; + + this.autoRadius = false; + this.outerRadius = Math.max(radius, 0); + + if (this.thickness > this.outerRadius) + this.thickness = this.outerRadius; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + clearPreset(); + } + + + /** + * Returns whether the radius is selected automatically or not. + * Returns false also in case automatic radius selection is not possible. + */ + public boolean isOuterRadiusAutomatic() { + return autoRadius; + } + + /** + * Sets whether the radius is selected automatically or not. + */ + public void setOuterRadiusAutomatic(boolean auto) { + if (autoRadius == auto) + return; + + autoRadius = auto; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + clearPreset(); + } + + + @Override + protected void loadFromPreset(RocketComponent preset) { + BodyTube c = (BodyTube) preset; + this.setOuterRadius(c.getOuterRadius()); + + super.loadFromPreset(preset); + } + + + @Override + public double getAftRadius() { + return getOuterRadius(); + } + + @Override + public double getForeRadius() { + return getOuterRadius(); + } + + @Override + public boolean isAftRadiusAutomatic() { + return isOuterRadiusAutomatic(); + } + + @Override + public boolean isForeRadiusAutomatic() { + return isOuterRadiusAutomatic(); + } + + + + @Override + protected double getFrontAutoRadius() { + if (isOuterRadiusAutomatic()) { + // Search for previous SymmetricComponent + SymmetricComponent c = this.getPreviousSymmetricComponent(); + if (c != null) { + return c.getFrontAutoRadius(); + } else { + return -1; + } + } + return getOuterRadius(); + } + + @Override + protected double getRearAutoRadius() { + if (isOuterRadiusAutomatic()) { + // Search for next SymmetricComponent + SymmetricComponent c = this.getNextSymmetricComponent(); + if (c != null) { + return c.getRearAutoRadius(); + } else { + return -1; + } + } + return getOuterRadius(); + } + + + + + + @Override + public double getInnerRadius() { + if (filled) + return 0; + return Math.max(getOuterRadius() - thickness, 0); + } + + @Override + public void setInnerRadius(double r) { + setThickness(getOuterRadius() - r); + } + + + + + /** + * Return the component name. + */ + @Override + public String getComponentName() { + //// Body tube + return trans.get("BodyTube.BodyTube"); + } + + + /************ Component calculations ***********/ + + // From SymmetricComponent + /** + * Returns the outer radius at the position x. This returns the same value as getOuterRadius(). + */ + @Override + public double getRadius(double x) { + return getOuterRadius(); + } + + /** + * Returns the inner radius at the position x. If the tube is filled, returns always zero. + */ + @Override + public double getInnerRadius(double x) { + if (filled) + return 0.0; + else + return Math.max(getOuterRadius() - thickness, 0); + } + + + /** + * Returns the body tube's center of gravity. + */ + @Override + public Coordinate getComponentCG() { + return new Coordinate(length / 2, 0, 0, getComponentMass()); + } + + /** + * Returns the body tube's volume. + */ + @Override + public double getComponentVolume() { + double r = getOuterRadius(); + if (filled) + return getFilledVolume(r, length); + else + return getFilledVolume(r, length) - getFilledVolume(getInnerRadius(0), length); + } + + + @Override + public double getLongitudinalUnitInertia() { + // 1/12 * (3 * (r1^2 + r2^2) + h^2) + return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getLength())) / 12; + } + + @Override + public double getRotationalUnitInertia() { + // 1/2 * (r1^2 + r2^2) + return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius())) / 2; + } + + + + + /** + * Helper function for cylinder volume. + */ + private static double getFilledVolume(double r, double l) { + return Math.PI * r * r * l; + } + + + /** + * Adds bounding coordinates to the given set. The body tube will fit within the + * convex hull of the points. + * + * Currently the points are simply a rectangular box around the body tube. + */ + @Override + public Collection getComponentBounds() { + Collection bounds = new ArrayList(8); + double r = getOuterRadius(); + addBound(bounds, 0, r); + addBound(bounds, length, r); + return bounds; + } + + + + /** + * Check whether the given type can be added to this component. BodyTubes allow any + * InternalComponents or ExternalComponents, excluding BodyComponents, to be added. + * + * @param type The RocketComponent class type to add. + * @return Whether such a component can be added. + */ + @Override + public boolean isCompatible(Class type) { + if (InternalComponent.class.isAssignableFrom(type)) + return true; + if (ExternalComponent.class.isAssignableFrom(type) && + !BodyComponent.class.isAssignableFrom(type)) + return true; + return false; + } + + //////////////// Motor mount ///////////////// + + @Override + public boolean isMotorMount() { + return motorMount; + } + + @Override + public void setMotorMount(boolean mount) { + if (motorMount == mount) + return; + motorMount = mount; + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + @Override + public Motor getMotor(String id) { + if (id == null) + return null; + + // Check whether the id is valid for the current rocket + RocketComponent root = this.getRoot(); + if (!(root instanceof Rocket)) + return null; + if (!((Rocket) root).isMotorConfigurationID(id)) + return null; + + return motors.get(id); + } + + @Override + public void setMotor(String id, Motor motor) { + if (id == null) { + if (motor != null) { + throw new IllegalArgumentException("Cannot set non-null motor for id null"); + } + } + Motor current = motors.get(id); + if ((motor == null && current == null) || + (motor != null && motor.equals(current))) + return; + motors.put(id, motor); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + @Override + public double getMotorDelay(String id) { + Double delay = ejectionDelays.get(id); + if (delay == null) + return Motor.PLUGGED; + return delay; + } + + @Override + public void setMotorDelay(String id, double delay) { + ejectionDelays.put(id, delay); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + @Override + public int getMotorCount() { + return 1; + } + + @Override + public double getMotorMountDiameter() { + return getInnerRadius() * 2; + } + + @Override + public IgnitionEvent getIgnitionEvent() { + return ignitionEvent; + } + + @Override + public void setIgnitionEvent(IgnitionEvent event) { + if (ignitionEvent == event) + return; + ignitionEvent = event; + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + } + + + @Override + public double getIgnitionDelay() { + return ignitionDelay; + } + + @Override + public void setIgnitionDelay(double delay) { + if (MathUtil.equals(delay, ignitionDelay)) + return; + ignitionDelay = delay; + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + } + + + @Override + public double getMotorOverhang() { + return overhang; + } + + @Override + public void setMotorOverhang(double overhang) { + if (MathUtil.equals(this.overhang, overhang)) + return; + this.overhang = overhang; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + @Override + public Coordinate getMotorPosition(String id) { + Motor motor = motors.get(id); + if (motor == null) { + throw new IllegalArgumentException("No motor with id " + id + " defined."); + } + + return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang()); + } + + + + + /* + * (non-Javadoc) + * Copy the motor and ejection delay HashMaps. + * + * @see rocketcomponent.RocketComponent#copy() + */ + @SuppressWarnings("unchecked") + @Override + protected RocketComponent copyWithOriginalID() { + RocketComponent c = super.copyWithOriginalID(); + ((BodyTube) c).motors = (HashMap) motors.clone(); + ((BodyTube) c).ejectionDelays = (HashMap) ejectionDelays.clone(); + return c; + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Bulkhead.java b/core/src/net/sf/openrocket/rocketcomponent/Bulkhead.java new file mode 100644 index 00000000..24a1552f --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Bulkhead.java @@ -0,0 +1,46 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + + +public class Bulkhead extends RadiusRingComponent { + private static final Translator trans = Application.getTranslator(); + + public Bulkhead() { + setOuterRadiusAutomatic(true); + setLength(0.002); + } + + @Override + public double getInnerRadius() { + return 0; + } + + @Override + public void setInnerRadius(double r) { + // No-op + } + + @Override + public void setOuterRadiusAutomatic(boolean auto) { + super.setOuterRadiusAutomatic(auto); + } + + @Override + public String getComponentName() { + //// Bulkhead + return trans.get("Bulkhead.Bulkhead"); + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class type) { + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/CenteringRing.java b/core/src/net/sf/openrocket/rocketcomponent/CenteringRing.java new file mode 100644 index 00000000..21b73e2a --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/CenteringRing.java @@ -0,0 +1,70 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.util.Coordinate; + + +public class CenteringRing extends RadiusRingComponent { + + public CenteringRing() { + setOuterRadiusAutomatic(true); + setInnerRadiusAutomatic(true); + setLength(0.002); + } + + + @Override + public double getInnerRadius() { + // Implement sibling inner radius automation + if (isInnerRadiusAutomatic()) { + innerRadius = 0; + // Component can be parentless if disattached from rocket + if (this.getParent() != null) { + for (RocketComponent sibling : this.getParent().getChildren()) { + /* + * Only InnerTubes are considered when determining the automatic + * inner radius (for now). + */ + if (!(sibling instanceof InnerTube)) // Excludes itself + continue; + + double pos1 = this.toRelative(Coordinate.NUL, sibling)[0].x; + double pos2 = this.toRelative(new Coordinate(getLength()), sibling)[0].x; + if (pos2 < 0 || pos1 > sibling.getLength()) + continue; + + innerRadius = Math.max(innerRadius, ((InnerTube) sibling).getOuterRadius()); + } + innerRadius = Math.min(innerRadius, getOuterRadius()); + } + } + + return super.getInnerRadius(); + } + + + @Override + public void setOuterRadiusAutomatic(boolean auto) { + super.setOuterRadiusAutomatic(auto); + } + + @Override + public void setInnerRadiusAutomatic(boolean auto) { + super.setInnerRadiusAutomatic(auto); + } + + @Override + public String getComponentName() { + return "Centering ring"; + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class type) { + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java b/core/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java new file mode 100644 index 00000000..73e8a6de --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java @@ -0,0 +1,118 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + + +/** + * Class that defines different cluster configurations available for the InnerTube. + * The class is immutable, and all the constructors are private. Therefore the only + * available cluster configurations are those available in the static fields. + * + * @author Sampo Niskanen + */ +public class ClusterConfiguration { + // Helper vars + private static final double R5 = 1.0/(2*Math.sin(2*Math.PI/10)); + private static final double SQRT2 = Math.sqrt(2); + private static final double SQRT3 = Math.sqrt(3); + + /** A single motor */ + public static final ClusterConfiguration SINGLE = new ClusterConfiguration("single", 0,0); + + /** Definitions of cluster configurations. Do not modify array. */ + public static final ClusterConfiguration[] CONFIGURATIONS = { + // Single row + SINGLE, + new ClusterConfiguration("double", -0.5,0, 0.5,0), + new ClusterConfiguration("3-row", -1.0,0, 0.0,0, 1.0,0), + new ClusterConfiguration("4-row", -1.5,0, -0.5,0, 0.5,0, 1.5,0), + + // Ring of tubes + new ClusterConfiguration("3-ring", -0.5,-1.0/(2*SQRT3), + 0.5,-1.0/(2*SQRT3), + 0, 1.0/SQRT3), + new ClusterConfiguration("4-ring", -0.5,0.5, 0.5,0.5, 0.5,-0.5, -0.5,-0.5), + new ClusterConfiguration("5-ring", 0,R5, + R5*Math.sin(2*Math.PI/5),R5*Math.cos(2*Math.PI/5), + R5*Math.sin(2*Math.PI*2/5),R5*Math.cos(2*Math.PI*2/5), + R5*Math.sin(2*Math.PI*3/5),R5*Math.cos(2*Math.PI*3/5), + R5*Math.sin(2*Math.PI*4/5),R5*Math.cos(2*Math.PI*4/5)), + new ClusterConfiguration("6-ring", 0,1, SQRT3/2,0.5, SQRT3/2,-0.5, + 0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5), + + // Centered with ring + new ClusterConfiguration("3-star", 0,0, 0,1, SQRT3/2,-0.5, -SQRT3/2,-0.5), + new ClusterConfiguration("4-star", 0,0, -1/SQRT2,1/SQRT2, 1/SQRT2,1/SQRT2, + 1/SQRT2,-1/SQRT2, -1/SQRT2,-1/SQRT2), + new ClusterConfiguration("5-star", 0,0, 0,1, + Math.sin(2*Math.PI/5),Math.cos(2*Math.PI/5), + Math.sin(2*Math.PI*2/5),Math.cos(2*Math.PI*2/5), + Math.sin(2*Math.PI*3/5),Math.cos(2*Math.PI*3/5), + Math.sin(2*Math.PI*4/5),Math.cos(2*Math.PI*4/5)), + new ClusterConfiguration("6-star", 0,0, 0,1, SQRT3/2,0.5, SQRT3/2,-0.5, + 0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5) + }; + + + + private final List points; + private final String xmlName; + + + private ClusterConfiguration(String xmlName, double... points) { + this.xmlName = xmlName; + if (points.length == 0 || points.length%2 == 1) { + throw new IllegalArgumentException("Illegal number of points specified: "+ + points.length); + } + List l = new ArrayList(points.length); + for (double d: points) + l.add(d); + + this.points = Collections.unmodifiableList(l); + } + + public String getXMLName() { + return xmlName; + } + + public int getClusterCount() { + return points.size()/2; + } + + /** + * Returns the relative positions of the cluster components. The list is of length + * 2*getClusterCount() with (x,y) value pairs. The origin is at (0,0) + * and the values are positioned so that the closest clusters have distance of 1. + * + * @return a list of (x,y) coordinate pairs. + */ + public List getPoints() { + return points; // Unmodifiable + } + + /** + * Return the points rotated by rotation radians. + * @param rotation Rotation amount. + */ + public List getPoints(double rotation) { + double cos = Math.cos(rotation); + double sin = Math.sin(rotation); + List ret = new ArrayList(points.size()); + for (int i=0; i + * Note that the mass and CG overrides of the ComponentAssembly class + * overrides all sibling mass/CG as well as its own. + * + * @author Sampo Niskanen + */ +public abstract class ComponentAssembly extends RocketComponent { + + /** + * Sets the position of the components to POSITION_RELATIVE_AFTER. + * (Should have no effect.) + */ + public ComponentAssembly() { + super(RocketComponent.Position.AFTER); + } + + /** + * Null method (ComponentAssembly has no bounds of itself). + */ + @Override + public Collection getComponentBounds() { + return Collections.emptyList(); + } + + /** + * Null method (ComponentAssembly has no mass of itself). + */ + @Override + public Coordinate getComponentCG() { + return Coordinate.NUL; + } + + /** + * Null method (ComponentAssembly has no mass of itself). + */ + @Override + public double getComponentMass() { + return 0; + } + + /** + * Null method (ComponentAssembly has no mass of itself). + */ + @Override + public double getLongitudinalUnitInertia() { + return 0; + } + + /** + * Null method (ComponentAssembly has no mass of itself). + */ + @Override + public double getRotationalUnitInertia() { + return 0; + } + + /** + * Components have no aerodynamic effect, so return false. + */ + @Override + public boolean isAerodynamic() { + return false; + } + + /** + * Component have no effect on mass, so return false (even though the override values + * may have an effect). + */ + @Override + public boolean isMassive() { + return false; + } + + @Override + public boolean getOverrideSubcomponents() { + return true; + } + + @Override + public void setOverrideSubcomponents(boolean override) { + // No-op + } + + @Override + public boolean isOverrideSubcomponentsEnabled() { + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java b/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java new file mode 100644 index 00000000..5a2a2e24 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java @@ -0,0 +1,103 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.EventObject; + +public class ComponentChangeEvent extends EventObject { + private static final long serialVersionUID = 1L; + + + /** A change that does not affect simulation results in any way (name, color, etc.) */ + public static final int NONFUNCTIONAL_CHANGE = 1; + /** A change that affects the mass properties of the rocket */ + public static final int MASS_CHANGE = 2; + /** A change that affects the aerodynamic properties of the rocket */ + public static final int AERODYNAMIC_CHANGE = 4; + /** A change that affects the mass and aerodynamic properties of the rocket */ + public static final int BOTH_CHANGE = MASS_CHANGE | AERODYNAMIC_CHANGE; // Mass & Aerodynamic + + /** A change that affects the rocket tree structure */ + public static final int TREE_CHANGE = 8; + /** A change caused by undo/redo. */ + public static final int UNDO_CHANGE = 16; + /** A change in the motor configurations or names */ + public static final int MOTOR_CHANGE = 32; + /** A change that affects the events occurring during flight. */ + public static final int EVENT_CHANGE = 64; + + /** A bit-field that contains all possible change types. */ + public static final int ALL_CHANGE = 0xFFFFFFFF; + + private final int type; + + + public ComponentChangeEvent(RocketComponent component, int type) { + super(component); + if (type == 0) { + throw new IllegalArgumentException("no event type provided"); + } + this.type = type; + } + + + /** + * Return the source component of this event as specified in the constructor. + */ + @Override + public RocketComponent getSource() { + return (RocketComponent) super.getSource(); + } + + + public boolean isAerodynamicChange() { + return (type & AERODYNAMIC_CHANGE) != 0; + } + + public boolean isMassChange() { + return (type & MASS_CHANGE) != 0; + } + + public boolean isOtherChange() { + return (type & BOTH_CHANGE) == 0; + } + + public boolean isTreeChange() { + return (type & TREE_CHANGE) != 0; + } + + public boolean isUndoChange() { + return (type & UNDO_CHANGE) != 0; + } + + public boolean isMotorChange() { + return (type & MOTOR_CHANGE) != 0; + } + + public int getType() { + return type; + } + + @Override + public String toString() { + String s = ""; + + if ((type & NONFUNCTIONAL_CHANGE) != 0) + s += ",nonfunc"; + if (isMassChange()) + s += ",mass"; + if (isAerodynamicChange()) + s += ",aero"; + if (isTreeChange()) + s += ",tree"; + if (isUndoChange()) + s += ",undo"; + if (isMotorChange()) + s += ",motor"; + if ((type & EVENT_CHANGE) != 0) + s += ",event"; + + if (s.length() > 0) + s = s.substring(1); + + return "ComponentChangeEvent[" + s + "]"; + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java b/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java new file mode 100644 index 00000000..dba150ae --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java @@ -0,0 +1,9 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.EventListener; + +public interface ComponentChangeListener extends EventListener { + + public void componentChanged(ComponentChangeEvent e); + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Configuration.java b/core/src/net/sf/openrocket/rocketcomponent/Configuration.java new file mode 100644 index 00000000..4a76df71 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Configuration.java @@ -0,0 +1,470 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.BitSet; +import java.util.Collection; +import java.util.Collections; +import java.util.EventListener; +import java.util.EventObject; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.StateChangeListener; + + +/** + * A class defining a rocket configuration, including motors and which stages are active. + * + * TODO: HIGH: Remove motor ignition times from this class. + * + * @author Sampo Niskanen + */ +public class Configuration implements Cloneable, ChangeSource, ComponentChangeListener, + Iterable, Monitorable { + + private Rocket rocket; + private BitSet stages = new BitSet(); + + private String motorConfiguration = null; + + private List listenerList = new ArrayList(); + + + /* Cached data */ + private int boundsModID = -1; + private ArrayList cachedBounds = new ArrayList(); + private double cachedLength = -1; + + private int refLengthModID = -1; + private double cachedRefLength = -1; + + + private int modID = 0; + + + /** + * Create a new configuration with the specified Rocket with + * null motor configuration. + * + * @param rocket the rocket + */ + public Configuration(Rocket rocket) { + this.rocket = rocket; + setAllStages(); + rocket.addComponentChangeListener(this); + } + + + + public Rocket getRocket() { + return rocket; + } + + + public void setAllStages() { + stages.clear(); + stages.set(0, rocket.getStageCount()); + fireChangeEvent(); + } + + + /** + * Set all stages up to and including the given stage number. For example, + * setToStage(0) will set only the first stage active. + * + * @param stage the stage number. + */ + public void setToStage(int stage) { + stages.clear(); + stages.set(0, stage + 1, true); + // stages.set(stage+1, rocket.getStageCount(), false); + fireChangeEvent(); + } + + + /** + * Check whether the up-most stage of the rocket is in this configuration. + * + * @return true if the first stage is active in this configuration. + */ + public boolean isHead() { + return isStageActive(0); + } + + + + /** + * Check whether the stage specified by the index is active. + */ + public boolean isStageActive(int stage) { + if (stage >= rocket.getStageCount()) + return false; + return stages.get(stage); + } + + public int getStageCount() { + return rocket.getStageCount(); + } + + public int getActiveStageCount() { + int count = 0; + int s = rocket.getStageCount(); + + for (int i = 0; i < s; i++) { + if (stages.get(i)) + count++; + } + return count; + } + + public int[] getActiveStages() { + int stageCount = rocket.getStageCount(); + List active = new ArrayList(); + int[] ret; + + for (int i = 0; i < stageCount; i++) { + if (stages.get(i)) { + active.add(i); + } + } + + ret = new int[active.size()]; + for (int i = 0; i < ret.length; i++) { + ret[i] = active.get(i); + } + + return ret; + } + + + /** + * Return the reference length associated with the current configuration. The + * reference length type is retrieved from the Rocket. + * + * @return the reference length for this configuration. + */ + public double getReferenceLength() { + if (rocket.getModID() != refLengthModID) { + refLengthModID = rocket.getModID(); + cachedRefLength = rocket.getReferenceType().getReferenceLength(this); + } + return cachedRefLength; + } + + + public double getReferenceArea() { + return Math.PI * MathUtil.pow2(getReferenceLength() / 2); + } + + + public String getMotorConfigurationID() { + return motorConfiguration; + } + + public void setMotorConfigurationID(String id) { + if ((motorConfiguration == null && id == null) || + (id != null && id.equals(motorConfiguration))) + return; + + motorConfiguration = id; + fireChangeEvent(); + } + + public String getMotorConfigurationDescription() { + return rocket.getMotorConfigurationNameOrDescription(motorConfiguration); + } + + + + + + /** + * Removes the listener connection to the rocket and listeners of this object. + * This configuration may not be used after a call to this method! + */ + public void release() { + rocket.removeComponentChangeListener(this); + listenerList = null; + rocket = null; + } + + + //////////////// Listeners //////////////// + + @Override + public void addChangeListener(EventListener listener) { + listenerList.add(listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listenerList.remove(listener); + } + + protected void fireChangeEvent() { + EventObject e = new EventObject(this); + + this.modID++; + boundsModID = -1; + refLengthModID = -1; + + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] listeners = listenerList.toArray(new EventListener[0]); + for (EventListener l : listeners) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(e); + } + } + } + + + @Override + public void componentChanged(ComponentChangeEvent e) { + fireChangeEvent(); + } + + + /////////////// Helper methods /////////////// + + /** + * Return whether this configuration has any motors defined to it. + * + * @return true if this configuration has active motor mounts with motors defined to them. + */ + public boolean hasMotors() { + for (RocketComponent c : this) { + if (c instanceof MotorMount) { + MotorMount mount = (MotorMount) c; + if (!mount.isMotorMount()) + continue; + if (mount.getMotor(this.motorConfiguration) != null) { + return true; + } + } + } + return false; + } + + + /** + * Return whether a component is in the currently active stages. + */ + public boolean isComponentActive(final RocketComponent c) { + int stage = c.getStageNumber(); + return isStageActive(stage); + } + + + /** + * Return the bounds of the current configuration. The bounds are cached. + * + * @return a Collection containing coordinates bouding the rocket. + */ + public Collection getBounds() { + if (rocket.getModID() != boundsModID) { + boundsModID = rocket.getModID(); + cachedBounds.clear(); + + double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; + for (RocketComponent component : this) { + for (Coordinate c : component.getComponentBounds()) { + for (Coordinate coord : component.toAbsolute(c)) { + cachedBounds.add(coord); + if (coord.x < minX) + minX = coord.x; + if (coord.x > maxX) + maxX = coord.x; + } + } + } + + if (Double.isInfinite(minX) || Double.isInfinite(maxX)) { + cachedLength = 0; + } else { + cachedLength = maxX - minX; + } + } + return cachedBounds.clone(); + } + + + /** + * Returns the length of the rocket configuration, from the foremost bound X-coordinate + * to the aft-most X-coordinate. The value is cached. + * + * @return the length of the rocket in the X-direction. + */ + public double getLength() { + if (rocket.getModID() != boundsModID) + getBounds(); // Calculates the length + + return cachedLength; + } + + + + + /** + * Return an iterator that iterates over the currently active components. + * The Rocket and Stage components are not returned, + * but instead all components that are within currently active stages. + */ + @Override + public Iterator iterator() { + return new ConfigurationIterator(); + } + + + /** + * Return an iterator that iterates over all MotorMounts within the + * current configuration that have an active motor. + * + * @return an iterator over active motor mounts. + */ + public Iterator motorIterator() { + return new MotorIterator(); + } + + + /** + * Perform a deep-clone. The object references are also cloned and no + * listeners are listening on the cloned object. The rocket instance remains the same. + */ + @Override + public Configuration clone() { + try { + Configuration config = (Configuration) super.clone(); + config.listenerList = new ArrayList(); + config.stages = (BitSet) this.stages.clone(); + config.cachedBounds = new ArrayList(); + config.boundsModID = -1; + config.refLengthModID = -1; + rocket.addComponentChangeListener(config); + return config; + } catch (CloneNotSupportedException e) { + throw new BugException("clone not supported!", e); + } + } + + + @Override + public int getModID() { + return modID + rocket.getModID(); + } + + + /** + * A class that iterates over all currently active components. + * + * @author Sampo Niskanen + */ + private class ConfigurationIterator implements Iterator { + Iterator> iterators; + Iterator current = null; + + public ConfigurationIterator() { + List> list = new ArrayList>(); + + for (RocketComponent stage : rocket.getChildren()) { + if (isComponentActive(stage)) { + list.add(stage.iterator(false)); + } + } + + // Get iterators and initialize current + iterators = list.iterator(); + if (iterators.hasNext()) { + current = iterators.next(); + } else { + List l = Collections.emptyList(); + current = l.iterator(); + } + } + + + @Override + public boolean hasNext() { + if (!current.hasNext()) + getNextIterator(); + + return current.hasNext(); + } + + @Override + public RocketComponent next() { + if (!current.hasNext()) + getNextIterator(); + + return current.next(); + } + + /** + * Get the next iterator that has items. If such an iterator does + * not exist, current is left to an empty iterator. + */ + private void getNextIterator() { + while ((!current.hasNext()) && iterators.hasNext()) { + current = iterators.next(); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove unsupported"); + } + } + + private class MotorIterator implements Iterator { + private final Iterator iterator; + private MotorMount next = null; + + public MotorIterator() { + this.iterator = iterator(); + } + + @Override + public boolean hasNext() { + getNext(); + return (next != null); + } + + @Override + public MotorMount next() { + getNext(); + if (next == null) { + throw new NoSuchElementException("iterator called for too long"); + } + + MotorMount ret = next; + next = null; + return ret; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove unsupported"); + } + + private void getNext() { + if (next != null) + return; + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + if (c instanceof MotorMount) { + MotorMount mount = (MotorMount) c; + if (mount.isMotorMount() && mount.getMotor(motorConfiguration) != null) { + next = mount; + return; + } + } + } + } + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java new file mode 100644 index 00000000..0fa9497c --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java @@ -0,0 +1,78 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +public class EllipticalFinSet extends FinSet { + private static final Translator trans = Application.getTranslator(); + + private static final int POINTS = 31; + + // Static positioning for the fin points + private static final double[] POINT_X = new double[POINTS]; + private static final double[] POINT_Y = new double[POINTS]; + static { + for (int i = 0; i < POINTS; i++) { + double a = Math.PI * (POINTS - 1 - i) / (POINTS - 1); + POINT_X[i] = (Math.cos(a) + 1) / 2; + POINT_Y[i] = Math.sin(a); + } + POINT_X[0] = 0; + POINT_Y[0] = 0; + POINT_X[POINTS - 1] = 1; + POINT_Y[POINTS - 1] = 0; + } + + + private double height = 0.05; + + public EllipticalFinSet() { + this.length = 0.05; + } + + + @Override + public Coordinate[] getFinPoints() { + double len = MathUtil.max(length, 0.0001); + Coordinate[] points = new Coordinate[POINTS]; + for (int i = 0; i < POINTS; i++) { + points[i] = new Coordinate(POINT_X[i] * len, POINT_Y[i] * height); + } + return points; + } + + @Override + public double getSpan() { + return height; + } + + @Override + public String getComponentName() { + //// Elliptical fin set + return trans.get("EllipticalFinSet.Ellipticalfinset"); + } + + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + if (MathUtil.equals(this.height, height)) + return; + this.height = height; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public void setLength(double length) { + if (MathUtil.equals(this.length, length)) + return; + this.length = length; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/EngineBlock.java b/core/src/net/sf/openrocket/rocketcomponent/EngineBlock.java new file mode 100644 index 00000000..2c2c590b --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/EngineBlock.java @@ -0,0 +1,33 @@ +package net.sf.openrocket.rocketcomponent; + + +public class EngineBlock extends ThicknessRingComponent { + + public EngineBlock() { + super(); + setOuterRadiusAutomatic(true); + setThickness(0.005); + setLength(0.005); + } + + @Override + public void setOuterRadiusAutomatic(boolean auto) { + super.setOuterRadiusAutomatic(auto); + } + + @Override + public String getComponentName() { + return "Engine block"; + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class type) { + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java b/core/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java new file mode 100644 index 00000000..8d958da6 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java @@ -0,0 +1,164 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.List; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.material.Material.Type; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * Class of components with well-defined physical appearance and which have an effect on + * aerodynamic simulation. They have material defined for them, and the mass of the component + * is calculated using the component's volume. + * + * @author Sampo Niskanen + */ + +public abstract class ExternalComponent extends RocketComponent { + + public enum Finish { + //// Rough + ROUGH("ExternalComponent.Rough", 500e-6), + //// Unfinished + UNFINISHED("ExternalComponent.Unfinished", 150e-6), + //// Regular paint + NORMAL("ExternalComponent.Regularpaint", 60e-6), + //// Smooth paint + SMOOTH("ExternalComponent.Smoothpaint", 20e-6), + //// Polished + POLISHED("ExternalComponent.Polished", 2e-6); + + private static final Translator trans = Application.getTranslator(); + private final String name; + private final double roughnessSize; + + Finish(String name, double roughness) { + this.name = name; + this.roughnessSize = roughness; + } + + public double getRoughnessSize() { + return roughnessSize; + } + + @Override + public String toString() { + return trans.get(name) + " (" + UnitGroup.UNITS_ROUGHNESS.toStringUnit(roughnessSize) + ")"; + } + } + + + /** + * The material of the component. + */ + protected Material material = null; + + protected Finish finish = Finish.NORMAL; + + + + /** + * Constructor that sets the relative position of the component. + */ + public ExternalComponent(RocketComponent.Position relativePosition) { + super(relativePosition); + this.material = Application.getPreferences().getDefaultComponentMaterial(this.getClass(), Material.Type.BULK); + } + + /** + * Returns the volume of the component. This value is used in calculating the mass + * of the object. + */ + public abstract double getComponentVolume(); + + /** + * Calculates the mass of the component as the product of the volume and interior density. + */ + @Override + public double getComponentMass() { + return material.getDensity() * getComponentVolume(); + } + + /** + * ExternalComponent has aerodynamic effect, so return true. + */ + @Override + public boolean isAerodynamic() { + return true; + } + + /** + * ExternalComponent has effect on the mass, so return true. + */ + @Override + public boolean isMassive() { + return true; + } + + + public Material getMaterial() { + return material; + } + + public void setMaterial(Material mat) { + if (mat.getType() != Material.Type.BULK) { + throw new IllegalArgumentException("ExternalComponent requires a bulk material" + + " type=" + mat.getType()); + } + + if (material.equals(mat)) + return; + material = mat; + clearPreset(); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + clearPreset(); + } + + public Finish getFinish() { + return finish; + } + + public void setFinish(Finish finish) { + if (this.finish == finish) + return; + this.finish = finish; + fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); + } + + + @Override + protected void loadFromPreset(RocketComponent preset) { + super.loadFromPreset(preset); + + // Surface finish is left unchanged + + ExternalComponent c = (ExternalComponent) preset; + + Material mat = c.getMaterial(); + if (c.isMassOverridden()) { + double mass = c.getOverrideMass(); + double volume = getComponentVolume(); + double density; + if (volume > 0.00001) { + density = mass / volume; + } else { + density = 1000; + } + mat = Material.newMaterial(Type.BULK, mat.getName(), density, true); + } + + setMaterial(mat); + } + + + @Override + protected List copyFrom(RocketComponent c) { + ExternalComponent src = (ExternalComponent) c; + this.finish = src.finish; + this.material = src.material; + return super.copyFrom(c); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/FinSet.java b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java new file mode 100644 index 00000000..5e40810b --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/FinSet.java @@ -0,0 +1,713 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Transformation; + + +public abstract class FinSet extends ExternalComponent { + private static final Translator trans = Application.getTranslator(); + + /** + * Maximum allowed cant of fins. + */ + public static final double MAX_CANT = (15.0 * Math.PI / 180); + + + public enum CrossSection { + //// Square + SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00), + //// Rounded + ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99), + //// Airfoil + AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85); + + private final String name; + private final double volume; + + CrossSection(String name, double volume) { + this.name = name; + this.volume = volume; + } + + public double getRelativeVolume() { + return volume; + } + + @Override + public String toString() { + return name; + } + } + + public enum TabRelativePosition { + //// Root chord leading edge + FRONT(trans.get("FinSet.TabRelativePosition.FRONT")), + //// Root chord midpoint + CENTER(trans.get("FinSet.TabRelativePosition.CENTER")), + //// Root chord trailing edge + END(trans.get("FinSet.TabRelativePosition.END")); + + private final String name; + + TabRelativePosition(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + /** + * Number of fins. + */ + protected int fins = 3; + + /** + * Rotation about the x-axis by 2*PI/fins. + */ + protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins); + + /** + * Rotation angle of the first fin. Zero corresponds to the positive y-axis. + */ + protected double rotation = 0; + + /** + * Rotation about the x-axis by angle this.rotation. + */ + protected Transformation baseRotation = Transformation.rotate_x(rotation); + + + /** + * Cant angle of fins. + */ + protected double cantAngle = 0; + + /* Cached value: */ + private Transformation cantRotation = null; + + + /** + * Thickness of the fins. + */ + protected double thickness = 0.003; + + + /** + * The cross-section shape of the fins. + */ + protected CrossSection crossSection = CrossSection.SQUARE; + + + /* + * Fin tab properties. + */ + private double tabHeight = 0; + private double tabLength = 0.05; + private double tabShift = 0; + private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER; + + + // Cached fin area & CG. Validity of both must be checked using finArea! + // Fin area does not include fin tabs, CG does. + private double finArea = -1; + private double finCGx = -1; + private double finCGy = -1; + + + /** + * New FinSet with given number of fins and given base rotation angle. + * Sets the component relative position to POSITION_RELATIVE_BOTTOM, + * i.e. fins are positioned at the bottom of the parent component. + */ + public FinSet() { + super(RocketComponent.Position.BOTTOM); + } + + + + /** + * Return the number of fins in the set. + * @return The number of fins. + */ + public int getFinCount() { + return fins; + } + + /** + * Sets the number of fins in the set. + * @param n The number of fins, greater of equal to one. + */ + public void setFinCount(int n) { + if (fins == n) + return; + if (n < 1) + n = 1; + if (n > 8) + n = 8; + fins = n; + finRotation = Transformation.rotate_x(2 * Math.PI / fins); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public Transformation getFinRotationTransformation() { + return finRotation; + } + + /** + * Gets the base rotation amount of the first fin. + * @return The base rotation amount. + */ + public double getBaseRotation() { + return rotation; + } + + /** + * Sets the base rotation amount of the first fin. + * @param r The base rotation amount. + */ + public void setBaseRotation(double r) { + r = MathUtil.reduce180(r); + if (MathUtil.equals(r, rotation)) + return; + rotation = r; + baseRotation = Transformation.rotate_x(rotation); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public Transformation getBaseRotationTransformation() { + return baseRotation; + } + + + + public double getCantAngle() { + return cantAngle; + } + + public void setCantAngle(double cant) { + cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT); + if (MathUtil.equals(cant, cantAngle)) + return; + this.cantAngle = cant; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public Transformation getCantRotation() { + if (cantRotation == null) { + if (MathUtil.equals(cantAngle, 0)) { + cantRotation = Transformation.IDENTITY; + } else { + Transformation t = new Transformation(-length / 2, 0, 0); + t = Transformation.rotate_y(cantAngle).applyTransformation(t); + t = new Transformation(length / 2, 0, 0).applyTransformation(t); + cantRotation = t; + } + } + return cantRotation; + } + + + + public double getThickness() { + return thickness; + } + + public void setThickness(double r) { + if (thickness == r) + return; + thickness = Math.max(r, 0); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public CrossSection getCrossSection() { + return crossSection; + } + + public void setCrossSection(CrossSection cs) { + if (crossSection == cs) + return; + crossSection = cs; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + + + @Override + public void setRelativePosition(RocketComponent.Position position) { + super.setRelativePosition(position); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + @Override + public void setPositionValue(double value) { + super.setPositionValue(value); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + + public double getTabHeight() { + return tabHeight; + } + + public void setTabHeight(double height) { + height = MathUtil.max(height, 0); + if (MathUtil.equals(this.tabHeight, height)) + return; + this.tabHeight = height; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public double getTabLength() { + return tabLength; + } + + public void setTabLength(double length) { + length = MathUtil.max(length, 0); + if (MathUtil.equals(this.tabLength, length)) + return; + this.tabLength = length; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public double getTabShift() { + return tabShift; + } + + public void setTabShift(double shift) { + this.tabShift = shift; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public TabRelativePosition getTabRelativePosition() { + return tabRelativePosition; + } + + public void setTabRelativePosition(TabRelativePosition position) { + if (this.tabRelativePosition == position) + return; + + + double front = getTabFrontEdge(); + switch (position) { + case FRONT: + this.tabShift = front; + break; + + case CENTER: + this.tabShift = front + tabLength / 2 - getLength() / 2; + break; + + case END: + this.tabShift = front + tabLength - getLength(); + break; + + default: + throw new IllegalArgumentException("position=" + position); + } + this.tabRelativePosition = position; + + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + /** + * Return the tab front edge position from the front of the fin. + */ + public double getTabFrontEdge() { + switch (this.tabRelativePosition) { + case FRONT: + return tabShift; + + case CENTER: + return getLength() / 2 - tabLength / 2 + tabShift; + + case END: + return getLength() - tabLength + tabShift; + + default: + throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); + } + } + + /** + * Return the tab trailing edge position *from the front of the fin*. + */ + public double getTabTrailingEdge() { + switch (this.tabRelativePosition) { + case FRONT: + return tabLength + tabShift; + case CENTER: + return getLength() / 2 + tabLength / 2 + tabShift; + + case END: + return getLength() + tabShift; + + default: + throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); + } + } + + + + + /////////// Calculation methods /////////// + + /** + * Return the area of one side of one fin. This does NOT include the area of + * the fin tab. + * + * @return the area of one side of one fin. + */ + public double getFinArea() { + if (finArea < 0) + calculateAreaCG(); + + return finArea; + } + + + /** + * Return the unweighted CG of a single fin. The X-coordinate is relative to + * the root chord trailing edge and the Y-coordinate to the fin root chord. + * + * @return the unweighted CG coordinate of a single fin. + */ + public Coordinate getFinCG() { + if (finArea < 0) + calculateAreaCG(); + + return new Coordinate(finCGx, finCGy, 0); + } + + + + @Override + public double getComponentVolume() { + return fins * (getFinArea() + tabHeight * tabLength) * thickness * + crossSection.getRelativeVolume(); + } + + + @Override + public Coordinate getComponentCG() { + if (finArea < 0) + calculateAreaCG(); + + double mass = getComponentMass(); // safe + + if (fins == 1) { + return baseRotation.transform( + new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass)); + } else { + return new Coordinate(finCGx, 0, 0, mass); + } + } + + + private void calculateAreaCG() { + Coordinate[] points = this.getFinPoints(); + finArea = 0; + finCGx = 0; + finCGy = 0; + + for (int i = 0; i < points.length - 1; i++) { + final double x0 = points[i].x; + final double x1 = points[i + 1].x; + final double y0 = points[i].y; + final double y1 = points[i + 1].y; + + double da = (y0 + y1) * (x1 - x0) / 2; + finArea += da; + if (Math.abs(y0 - y1) < 0.00001) { + finCGx += (x0 + x1) / 2 * da; + finCGy += y0 / 2 * da; + } else { + finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da; + finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da; + } + } + + if (finArea < 0) + finArea = 0; + + // Add effect of fin tabs to CG + double tabArea = tabLength * tabHeight; + if (!MathUtil.equals(tabArea, 0)) { + + double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2; + double y = -this.tabHeight / 2; + + finCGx += x * tabArea; + finCGy += y * tabArea; + + } + + if ((finArea + tabArea) > 0) { + finCGx /= (finArea + tabArea); + finCGy /= (finArea + tabArea); + } else { + finCGx = (points[0].x + points[points.length - 1].x) / 2; + finCGy = 0; + } + } + + + /* + * Return an approximation of the longitudinal unitary inertia of the fin set. + * The process is the following: + * + * 1. Approximate the fin with a rectangular fin + * + * 2. The inertia of one fin is taken as the average of the moments of inertia + * through its center perpendicular to the plane, and the inertia through + * its center parallel to the plane + * + * 3. If there are multiple fins, the inertia is shifted to the center of the fin + * set and multiplied by the number of fins. + */ + @Override + public double getLongitudinalUnitInertia() { + double area = getFinArea(); + if (MathUtil.equals(area, 0)) + return 0; + + // Approximate fin with a rectangular fin + // w2 and h2 are squares of the fin width and height + double w = getLength(); + double h = getSpan(); + double w2, h2; + + if (MathUtil.equals(w * h, 0)) { + w2 = area; + h2 = area; + } else { + w2 = w * area / h; + h2 = h * area / w; + } + + double inertia = (h2 + 2 * w2) / 24; + + if (fins == 1) + return inertia; + + double radius = getBodyRadius(); + + return fins * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + radius)); + } + + + /* + * Return an approximation of the rotational unitary inertia of the fin set. + * The process is the following: + * + * 1. Approximate the fin with a rectangular fin and calculate the inertia of the + * rectangular approximate + * + * 2. If there are multiple fins, shift the inertia center to the fin set center + * and multiply with the number of fins. + */ + @Override + public double getRotationalUnitInertia() { + double area = getFinArea(); + if (MathUtil.equals(area, 0)) + return 0; + + // Approximate fin with a rectangular fin + double w = getLength(); + double h = getSpan(); + + if (MathUtil.equals(w * h, 0)) { + h = MathUtil.safeSqrt(area); + } else { + h = MathUtil.safeSqrt(h * area / w); + } + + if (fins == 1) + return h * h / 12; + + double radius = getBodyRadius(); + + return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius)); + } + + + /** + * Adds the fin set's bounds to the collection. + */ + @Override + public Collection getComponentBounds() { + List bounds = new ArrayList(); + double r = getBodyRadius(); + + for (Coordinate point : getFinPoints()) { + addFinBound(bounds, point.x, point.y + r); + } + + return bounds; + } + + + /** + * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for + * all fin rotations. + */ + private void addFinBound(Collection set, double x, double y) { + Coordinate c; + int i; + + c = new Coordinate(x, y, thickness / 2); + c = baseRotation.transform(c); + set.add(c); + for (i = 1; i < fins; i++) { + c = finRotation.transform(c); + set.add(c); + } + + c = new Coordinate(x, y, -thickness / 2); + c = baseRotation.transform(c); + set.add(c); + for (i = 1; i < fins; i++) { + c = finRotation.transform(c); + set.add(c); + } + } + + + + @Override + public void componentChanged(ComponentChangeEvent e) { + if (e.isAerodynamicChange()) { + finArea = -1; + cantRotation = null; + } + } + + + /** + * Return the radius of the BodyComponent the fin set is situated on. Currently + * only supports SymmetricComponents and returns the radius at the starting point of the + * root chord. + * + * @return radius of the underlying BodyComponent or 0 if none exists. + */ + public double getBodyRadius() { + RocketComponent s; + + s = this.getParent(); + while (s != null) { + if (s instanceof SymmetricComponent) { + double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x; + return ((SymmetricComponent) s).getRadius(x); + } + s = s.getParent(); + } + return 0; + } + + @Override + public boolean allowsChildren() { + return false; + } + + /** + * Allows nothing to be attached to a FinSet. + * + * @return false + */ + @Override + public boolean isCompatible(Class type) { + return false; + } + + + + + /** + * Return a list of coordinates defining the geometry of a single fin. + * The coordinates are the XY-coordinates of points defining the shape of a single fin, + * where the origin is the leading root edge. Therefore, the first point must be (0,0,0). + * All Z-coordinates must be zero, and the last coordinate must have Y=0. + * + * @return List of XY-coordinates. + */ + public abstract Coordinate[] getFinPoints(); + + + /** + * Return a list of coordinates defining the geometry of a single fin, including a + * possible fin tab. The coordinates are the XY-coordinates of points defining the + * shape of a single fin, where the origin is the leading root edge. This implementation + * calls {@link #getFinPoints()} and adds the necessary points for the fin tab. + * The tab coordinates will have a negative y value. + * + * @return List of XY-coordinates. + */ + public Coordinate[] getFinPointsWithTab() { + Coordinate[] points = getFinPoints(); + + if (MathUtil.equals(getTabHeight(), 0) || + MathUtil.equals(getTabLength(), 0)) + return points; + + double x1 = getTabFrontEdge(); + double x2 = getTabTrailingEdge(); + double y = -getTabHeight(); + + int n = points.length; + points = Arrays.copyOf(points, points.length + 4); + points[n] = new Coordinate(x2, 0); + points[n + 1] = new Coordinate(x2, y); + points[n + 2] = new Coordinate(x1, y); + points[n + 3] = new Coordinate(x1, 0); + return points; + } + + + + /** + * Get the span of a single fin. That is, the length from the root to the tip of the fin. + * @return Span of a single fin. + */ + public abstract double getSpan(); + + + @Override + protected List copyFrom(RocketComponent c) { + FinSet src = (FinSet) c; + this.fins = src.fins; + this.finRotation = src.finRotation; + this.rotation = src.rotation; + this.baseRotation = src.baseRotation; + this.cantAngle = src.cantAngle; + this.cantRotation = src.cantRotation; + this.thickness = src.thickness; + this.crossSection = src.crossSection; + this.tabHeight = src.tabHeight; + this.tabLength = src.tabLength; + this.tabRelativePosition = src.tabRelativePosition; + this.tabShift = src.tabShift; + + return super.copyFrom(c); + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java new file mode 100644 index 00000000..ffa44be3 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java @@ -0,0 +1,339 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; + + +public class FreeformFinSet extends FinSet { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + private ArrayList points = new ArrayList(); + + public FreeformFinSet() { + points.add(Coordinate.NUL); + points.add(new Coordinate(0.025, 0.05)); + points.add(new Coordinate(0.075, 0.05)); + points.add(new Coordinate(0.05, 0)); + + this.length = 0.05; + } + + + public FreeformFinSet(Coordinate[] finpoints) throws IllegalFinPointException { + setPoints(finpoints); + } + + /* + public FreeformFinSet(FinSet finset) { + Coordinate[] finpoints = finset.getFinPoints(); + this.copyFrom(finset); + + points.clear(); + for (Coordinate c: finpoints) { + points.add(c); + } + this.length = points.get(points.size()-1).x - points.get(0).x; + } + */ + + + /** + * Convert an existing fin set into a freeform fin set. The specified + * fin set is taken out of the rocket tree (if any) and the new component + * inserted in its stead. + *

+ * The specified fin set should not be used after the call! + * + * @param finset the fin set to convert. + * @return the new freeform fin set. + */ + public static FreeformFinSet convertFinSet(FinSet finset) { + log.info("Converting " + finset.getComponentName() + " into freeform fin set"); + final RocketComponent root = finset.getRoot(); + FreeformFinSet freeform; + List toInvalidate = Collections.emptyList(); + + try { + if (root instanceof Rocket) { + ((Rocket) root).freeze(); + } + + // Get fin set position and remove fin set + final RocketComponent parent = finset.getParent(); + final int position; + if (parent != null) { + position = parent.getChildPosition(finset); + parent.removeChild(position); + } else { + position = -1; + } + + + // Create the freeform fin set + Coordinate[] finpoints = finset.getFinPoints(); + try { + freeform = new FreeformFinSet(finpoints); + } catch (IllegalFinPointException e) { + throw new BugException("Illegal fin points when converting existing fin to " + + "freeform fin, fin=" + finset + " points=" + Arrays.toString(finpoints), + e); + } + + // Copy component attributes + toInvalidate = freeform.copyFrom(finset); + + // Set name + final String componentTypeName = finset.getComponentName(); + final String name = freeform.getName(); + + if (name.startsWith(componentTypeName)) { + freeform.setName(freeform.getComponentName() + + name.substring(componentTypeName.length())); + } + + // Add freeform fin set to parent + if (parent != null) { + parent.addChild(freeform, position); + } + + } finally { + if (root instanceof Rocket) { + ((Rocket) root).thaw(); + } + // Invalidate components after events have been fired + for (RocketComponent c : toInvalidate) { + c.invalidate(); + } + } + return freeform; + } + + + + /** + * Add a fin point between indices index-1 and index. + * The point is placed at the midpoint of the current segment. + * + * @param index the fin point before which to add the new point. + */ + public void addPoint(int index) { + double x0, y0, x1, y1; + + x0 = points.get(index - 1).x; + y0 = points.get(index - 1).y; + x1 = points.get(index).x; + y1 = points.get(index).y; + + points.add(index, new Coordinate((x0 + x1) / 2, (y0 + y1) / 2)); + // adding a point within the segment affects neither mass nor aerodynamics + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + /** + * Remove the fin point with the given index. The first and last fin points + * cannot be removed, and will cause an IllegalFinPointException + * if attempted. + * + * @param index the fin point index to remove + * @throws IllegalFinPointException if removing would result in invalid fin planform + */ + public void removePoint(int index) throws IllegalFinPointException { + if (index == 0 || index == points.size() - 1) { + throw new IllegalFinPointException("cannot remove first or last point"); + } + + ArrayList copy = this.points.clone(); + copy.remove(index); + validate(copy); + this.points = copy; + + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public int getPointCount() { + return points.size(); + } + + public void setPoints(Coordinate[] points) throws IllegalFinPointException { + ArrayList list = new ArrayList(points.length); + for (Coordinate p : points) { + list.add(p); + } + validate(list); + this.points = list; + + this.length = points[points.length - 1].x; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + /** + * Set the point at position i to coordinates (x,y). + *

+ * Note that this method enforces basic fin shape restrictions (non-negative y, + * first and last point locations) silently, but throws an + * IllegalFinPointException if the point causes fin segments to + * intersect. + *

+ * Moving of the first point in the X-axis is allowed, but this actually moves + * all of the other points the corresponding distance back. + * + * @param index the point index to modify. + * @param x the x-coordinate. + * @param y the y-coordinate. + * @throws IllegalFinPointException if the specified fin point would cause intersecting + * segments + */ + public void setPoint(int index, double x, double y) throws IllegalFinPointException { + if (y < 0) + y = 0; + + double x0, y0, x1, y1; + + if (index == 0) { + + // Restrict point + x = Math.min(x, points.get(points.size() - 1).x); + y = 0; + x0 = Double.NaN; + y0 = Double.NaN; + x1 = points.get(1).x; + y1 = points.get(1).y; + + } else if (index == points.size() - 1) { + + // Restrict point + x = Math.max(x, 0); + y = 0; + x0 = points.get(index - 1).x; + y0 = points.get(index - 1).y; + x1 = Double.NaN; + y1 = Double.NaN; + + } else { + + x0 = points.get(index - 1).x; + y0 = points.get(index - 1).y; + x1 = points.get(index + 1).x; + y1 = points.get(index + 1).y; + + } + + + + // Check for intersecting + double px0, py0, px1, py1; + px0 = 0; + py0 = 0; + for (int i = 1; i < points.size(); i++) { + px1 = points.get(i).x; + py1 = points.get(i).y; + + if (i != index - 1 && i != index && i != index + 1) { + if (intersects(x0, y0, x, y, px0, py0, px1, py1)) { + throw new IllegalFinPointException("segments intersect"); + } + } + if (i != index && i != index + 1 && i != index + 2) { + if (intersects(x, y, x1, y1, px0, py0, px1, py1)) { + throw new IllegalFinPointException("segments intersect"); + } + } + + px0 = px1; + py0 = py1; + } + + if (index == 0) { + + System.out.println("Set point zero to x:" + x); + for (int i = 1; i < points.size(); i++) { + Coordinate c = points.get(i); + points.set(i, c.setX(c.x - x)); + } + + } else { + + points.set(index, new Coordinate(x, y)); + + } + if (index == 0 || index == points.size() - 1) { + this.length = points.get(points.size() - 1).x; + } + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + private boolean intersects(double ax0, double ay0, double ax1, double ay1, + double bx0, double by0, double bx1, double by1) { + + double d = ((by1 - by0) * (ax1 - ax0) - (bx1 - bx0) * (ay1 - ay0)); + + double ua = ((bx1 - bx0) * (ay0 - by0) - (by1 - by0) * (ax0 - bx0)) / d; + double ub = ((ax1 - ax0) * (ay0 - by0) - (ay1 - ay0) * (ax0 - bx0)) / d; + + return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1); + } + + + @Override + public Coordinate[] getFinPoints() { + return points.toArray(new Coordinate[0]); + } + + @Override + public double getSpan() { + double max = 0; + for (Coordinate c : points) { + if (c.y > max) + max = c.y; + } + return max; + } + + @Override + public String getComponentName() { + //// Freeform fin set + return trans.get("FreeformFinSet.FreeformFinSet"); + } + + + @Override + protected RocketComponent copyWithOriginalID() { + RocketComponent c = super.copyWithOriginalID(); + ((FreeformFinSet) c).points = this.points.clone(); + return c; + } + + private void validate(ArrayList pts) throws IllegalFinPointException { + final int n = pts.size(); + if (pts.get(0).x != 0 || pts.get(0).y != 0 || + pts.get(n - 1).x < 0 || pts.get(n - 1).y != 0) { + throw new IllegalFinPointException("Start or end point illegal."); + } + for (int i = 0; i < n - 1; i++) { + for (int j = i + 2; j < n - 1; j++) { + if (intersects(pts.get(i).x, pts.get(i).y, pts.get(i + 1).x, pts.get(i + 1).y, + pts.get(j).x, pts.get(j).y, pts.get(j + 1).x, pts.get(j + 1).y)) { + throw new IllegalFinPointException("segments intersect"); + } + } + if (pts.get(i).z != 0) { + throw new IllegalFinPointException("z-coordinate not zero"); + } + } + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/IllegalFinPointException.java b/core/src/net/sf/openrocket/rocketcomponent/IllegalFinPointException.java new file mode 100644 index 00000000..8f9a88ac --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/IllegalFinPointException.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.rocketcomponent; + +/** + * An exception signifying that an operation on the freeform fin set points was + * illegal (segments intersect, removing first or last point, etc). + * + * @author Sampo Niskanen + */ +public class IllegalFinPointException extends Exception { + + public IllegalFinPointException() { + + } + + public IllegalFinPointException(String message) { + super(message); + } + + public IllegalFinPointException(Throwable cause) { + super(cause); + } + + public IllegalFinPointException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java new file mode 100644 index 00000000..0b8c92d2 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/InnerTube.java @@ -0,0 +1,339 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +/** + * This class defines an inner tube that can be used as a motor mount. The component + * may also be clustered. + * + * @author Sampo Niskanen + */ +public class InnerTube extends ThicknessRingComponent + implements Clusterable, RadialParent, MotorMount { + private static final Translator trans = Application.getTranslator(); + + private ClusterConfiguration cluster = ClusterConfiguration.SINGLE; + private double clusterScale = 1.0; + private double clusterRotation = 0.0; + + + private boolean motorMount = false; + private HashMap ejectionDelays = new HashMap(); + private HashMap motors = new HashMap(); + private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC; + private double ignitionDelay = 0; + private double overhang = 0; + + + /** + * Main constructor. + */ + public InnerTube() { + // A-C motor size: + this.setOuterRadius(0.019 / 2); + this.setInnerRadius(0.018 / 2); + this.setLength(0.070); + } + + + @Override + public double getInnerRadius(double x) { + return getInnerRadius(); + } + + + @Override + public double getOuterRadius(double x) { + return getOuterRadius(); + } + + + @Override + public String getComponentName() { + //// Inner Tube + return trans.get("InnerTube.InnerTube"); + } + + @Override + public boolean allowsChildren() { + return true; + } + + /** + * Allow all InternalComponents to be added to this component. + */ + @Override + public boolean isCompatible(Class type) { + return InternalComponent.class.isAssignableFrom(type); + } + + + + ///////////// Cluster methods ////////////// + + /** + * Get the current cluster configuration. + * @return The current cluster configuration. + */ + @Override + public ClusterConfiguration getClusterConfiguration() { + return cluster; + } + + /** + * Set the current cluster configuration. + * @param cluster The cluster configuration. + */ + @Override + public void setClusterConfiguration(ClusterConfiguration cluster) { + this.cluster = cluster; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + /** + * Return the number of tubes in the cluster. + * @return Number of tubes in the current cluster. + */ + @Override + public int getClusterCount() { + return cluster.getClusterCount(); + } + + /** + * Get the cluster scaling. A value of 1.0 indicates that the tubes are packed + * touching each other, larger values separate the tubes and smaller values + * pack inside each other. + */ + public double getClusterScale() { + return clusterScale; + } + + /** + * Set the cluster scaling. + * @see #getClusterScale() + */ + public void setClusterScale(double scale) { + scale = Math.max(scale, 0); + if (MathUtil.equals(clusterScale, scale)) + return; + clusterScale = scale; + fireComponentChangeEvent(new ComponentChangeEvent(this, ComponentChangeEvent.MASS_CHANGE)); + } + + + + /** + * @return the clusterRotation + */ + public double getClusterRotation() { + return clusterRotation; + } + + + /** + * @param rotation the clusterRotation to set + */ + public void setClusterRotation(double rotation) { + rotation = MathUtil.reduce180(rotation); + if (clusterRotation == rotation) + return; + this.clusterRotation = rotation; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + /** + * Return the distance between the closest two cluster inner tube center points. + * This is equivalent to the cluster scale multiplied by the tube diameter. + */ + @Override + public double getClusterSeparation() { + return 2 * getOuterRadius() * clusterScale; + } + + + public List getClusterPoints() { + List list = new ArrayList(getClusterCount()); + List points = cluster.getPoints(clusterRotation - getRadialDirection()); + double separation = getClusterSeparation(); + for (int i = 0; i < points.size() / 2; i++) { + list.add(new Coordinate(0, points.get(2 * i) * separation, points.get(2 * i + 1) * separation)); + } + return list; + } + + + @Override + public Coordinate[] shiftCoordinates(Coordinate[] array) { + array = super.shiftCoordinates(array); + + int count = getClusterCount(); + if (count == 1) + return array; + + List points = getClusterPoints(); + if (points.size() != count) { + throw new BugException("Inconsistent cluster configuration, cluster count=" + count + + " point count=" + points.size()); + } + Coordinate[] newArray = new Coordinate[array.length * count]; + for (int i = 0; i < array.length; i++) { + for (int j = 0; j < count; j++) { + newArray[i * count + j] = array[i].add(points.get(j)); + } + } + + return newArray; + } + + + + + //////////////// Motor mount ///////////////// + + @Override + public boolean isMotorMount() { + return motorMount; + } + + @Override + public void setMotorMount(boolean mount) { + if (motorMount == mount) + return; + motorMount = mount; + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + @Override + public Motor getMotor(String id) { + if (id == null) + return null; + + // Check whether the id is valid for the current rocket + RocketComponent root = this.getRoot(); + if (!(root instanceof Rocket)) + return null; + if (!((Rocket) root).isMotorConfigurationID(id)) + return null; + + return motors.get(id); + } + + @Override + public void setMotor(String id, Motor motor) { + if (id == null) { + if (motor != null) { + throw new IllegalArgumentException("Cannot set non-null motor for id null"); + } + } + Motor current = motors.get(id); + if ((motor == null && current == null) || + (motor != null && motor.equals(current))) + return; + motors.put(id, motor); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + @Override + public double getMotorDelay(String id) { + Double delay = ejectionDelays.get(id); + if (delay == null) + return Motor.PLUGGED; + return delay; + } + + @Override + public void setMotorDelay(String id, double delay) { + ejectionDelays.put(id, delay); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + @Deprecated + @Override + public int getMotorCount() { + return getClusterCount(); + } + + @Override + public double getMotorMountDiameter() { + return getInnerRadius() * 2; + } + + @Override + public IgnitionEvent getIgnitionEvent() { + return ignitionEvent; + } + + @Override + public void setIgnitionEvent(IgnitionEvent event) { + if (ignitionEvent == event) + return; + ignitionEvent = event; + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + } + + + @Override + public double getIgnitionDelay() { + return ignitionDelay; + } + + @Override + public void setIgnitionDelay(double delay) { + if (MathUtil.equals(delay, ignitionDelay)) + return; + ignitionDelay = delay; + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + } + + + @Override + public double getMotorOverhang() { + return overhang; + } + + @Override + public void setMotorOverhang(double overhang) { + if (MathUtil.equals(this.overhang, overhang)) + return; + this.overhang = overhang; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + @Override + public Coordinate getMotorPosition(String id) { + Motor motor = motors.get(id); + if (motor == null) { + throw new IllegalArgumentException("No motor with id " + id + " defined."); + } + + return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang()); + } + + /* + * (non-Javadoc) + * Copy the motor and ejection delay HashMaps. + * + * @see rocketcomponent.RocketComponent#copy() + */ + @SuppressWarnings("unchecked") + @Override + protected RocketComponent copyWithOriginalID() { + RocketComponent c = super.copyWithOriginalID(); + ((InnerTube) c).motors = (HashMap) motors.clone(); + ((InnerTube) c).ejectionDelays = (HashMap) ejectionDelays.clone(); + return c; + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/rocketcomponent/InternalComponent.java b/core/src/net/sf/openrocket/rocketcomponent/InternalComponent.java new file mode 100644 index 00000000..1f2dba40 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/InternalComponent.java @@ -0,0 +1,51 @@ +package net.sf.openrocket.rocketcomponent; + + +/** + * A component internal to the rocket. Internal components have no effect on the + * the aerodynamics of a rocket, only its mass properties (though the location of the + * components is not enforced to be within external components). Internal components + * are always attached relative to the parent component, which can be internal or + * external, or absolutely. + * + * @author Sampo Niskanen + */ +public abstract class InternalComponent extends RocketComponent { + + public InternalComponent() { + super(RocketComponent.Position.BOTTOM); + } + + + @Override + public final void setRelativePosition(RocketComponent.Position position) { + super.setRelativePosition(position); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + @Override + public final void setPositionValue(double value) { + super.setPositionValue(value); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + /** + * Non-aerodynamic components. + * @return false + */ + @Override + public final boolean isAerodynamic() { + return false; + } + + /** + * Is massive. + * @return true + */ + @Override + public final boolean isMassive() { + return true; + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java b/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java new file mode 100644 index 00000000..5614e077 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/LaunchLug.java @@ -0,0 +1,208 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayList; +import java.util.Collection; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + + +public class LaunchLug extends ExternalComponent implements Coaxial { + + private static final Translator trans = Application.getTranslator(); + + private double radius; + private double thickness; + + private double radialDirection = 0; + + /* These are calculated when the component is first attached to any Rocket */ + private double shiftY, shiftZ; + + + + public LaunchLug() { + super(Position.MIDDLE); + radius = 0.01 / 2; + thickness = 0.001; + length = 0.03; + } + + + @Override + public double getOuterRadius() { + return radius; + } + + @Override + public void setOuterRadius(double radius) { + if (MathUtil.equals(this.radius, radius)) + return; + this.radius = radius; + this.thickness = Math.min(this.thickness, this.radius); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + @Override + public double getInnerRadius() { + return radius - thickness; + } + + @Override + public void setInnerRadius(double innerRadius) { + setOuterRadius(innerRadius + thickness); + } + + @Override + public double getThickness() { + return thickness; + } + + public void setThickness(double thickness) { + if (MathUtil.equals(this.thickness, thickness)) + return; + this.thickness = MathUtil.clamp(thickness, 0, radius); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public double getRadialDirection() { + return radialDirection; + } + + public void setRadialDirection(double direction) { + direction = MathUtil.reduce180(direction); + if (MathUtil.equals(this.radialDirection, direction)) + return; + this.radialDirection = direction; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + public void setLength(double length) { + if (MathUtil.equals(this.length, length)) + return; + this.length = length; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + + + @Override + public void setRelativePosition(RocketComponent.Position position) { + super.setRelativePosition(position); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + @Override + public void setPositionValue(double value) { + super.setPositionValue(value); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + @Override + public Coordinate[] shiftCoordinates(Coordinate[] array) { + array = super.shiftCoordinates(array); + + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(0, shiftY, shiftZ); + } + + return array; + } + + + @Override + public void componentChanged(ComponentChangeEvent e) { + super.componentChanged(e); + + /* + * shiftY and shiftZ must be computed here since calculating them + * in shiftCoordinates() would cause an infinite loop due to .toRelative + */ + RocketComponent body; + double parentRadius; + + for (body = this.getParent(); body != null; body = body.getParent()) { + if (body instanceof SymmetricComponent) + break; + } + + if (body == null) { + parentRadius = 0; + } else { + SymmetricComponent s = (SymmetricComponent) body; + double x1, x2; + x1 = this.toRelative(Coordinate.NUL, body)[0].x; + x2 = this.toRelative(new Coordinate(length, 0, 0), body)[0].x; + x1 = MathUtil.clamp(x1, 0, body.getLength()); + x2 = MathUtil.clamp(x2, 0, body.getLength()); + parentRadius = Math.max(s.getRadius(x1), s.getRadius(x2)); + } + + shiftY = Math.cos(radialDirection) * (parentRadius + radius); + shiftZ = Math.sin(radialDirection) * (parentRadius + radius); + + // System.out.println("Computed shift: y="+shiftY+" z="+shiftZ); + } + + + + + @Override + public double getComponentVolume() { + return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius - thickness)); + } + + @Override + public Collection getComponentBounds() { + ArrayList set = new ArrayList(); + addBound(set, 0, radius); + addBound(set, length, radius); + return set; + } + + @Override + public Coordinate getComponentCG() { + return new Coordinate(length / 2, 0, 0, getComponentMass()); + } + + @Override + public String getComponentName() { + //// Launch lug + return trans.get("LaunchLug.Launchlug"); + } + + @Override + public double getLongitudinalUnitInertia() { + // 1/12 * (3 * (r1^2 + r2^2) + h^2) + return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getLength())) / 12; + } + + @Override + public double getRotationalUnitInertia() { + // 1/2 * (r1^2 + r2^2) + return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius())) / 2; + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class type) { + // Allow nothing to be attached to a LaunchLug + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/MassComponent.java b/core/src/net/sf/openrocket/rocketcomponent/MassComponent.java new file mode 100644 index 00000000..49c139b0 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/MassComponent.java @@ -0,0 +1,59 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; + +/** + * This class represents a generic component that has a specific mass and an approximate shape. + * The mass is accessed via get/setComponentMass. + * + * @author Sampo Niskanen + */ +public class MassComponent extends MassObject { + private static final Translator trans = Application.getTranslator(); + + private double mass = 0; + + + public MassComponent() { + super(); + } + + public MassComponent(double length, double radius, double mass) { + super(length, radius); + this.mass = mass; + } + + + @Override + public double getComponentMass() { + return mass; + } + + public void setComponentMass(double mass) { + mass = Math.max(mass, 0); + if (MathUtil.equals(this.mass, mass)) + return; + this.mass = mass; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + @Override + public String getComponentName() { + //// Mass component + return trans.get("MassComponent.MassComponent"); + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class type) { + // Allow no components to be attached to a MassComponent + return false; + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/MassObject.java b/core/src/net/sf/openrocket/rocketcomponent/MassObject.java new file mode 100644 index 00000000..fb4aa744 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/MassObject.java @@ -0,0 +1,138 @@ +package net.sf.openrocket.rocketcomponent; + +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.ArrayList; +import java.util.Collection; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +/** + * A MassObject is an internal component that can a specific weight, but not necessarily a strictly bound shape. It is + * represented as a homogeneous cylinder and drawn in the rocket figure with rounded corners. + *

+ * Subclasses of this class need only implement the {@link #getComponentMass()}, {@link #getComponentName()} and {@link + * #isCompatible(RocketComponent)} methods. + * + * @author Sampo Niskanen + */ +public abstract class MassObject extends InternalComponent { + + private double radius; + + private double radialPosition; + private double radialDirection; + + private double shiftY = 0; + private double shiftZ = 0; + + + public MassObject() { + this(0.025, 0.0125); + } + + public MassObject(double length, double radius) { + super(); + + this.length = length; + this.radius = radius; + + this.setRelativePosition(Position.TOP); + this.setPositionValue(0.0); + } + + + public void setLength(double length) { + length = Math.max(length, 0); + if (MathUtil.equals(this.length, length)) { + return; + } + this.length = length; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public final double getRadius() { + return radius; + } + + + public final void setRadius(double radius) { + radius = Math.max(radius, 0); + if (MathUtil.equals(this.radius, radius)) { + return; + } + this.radius = radius; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + public final double getRadialPosition() { + return radialPosition; + } + + public final void setRadialPosition(double radialPosition) { + radialPosition = Math.max(radialPosition, 0); + if (MathUtil.equals(this.radialPosition, radialPosition)) { + return; + } + this.radialPosition = radialPosition; + shiftY = radialPosition * Math.cos(radialDirection); + shiftZ = radialPosition * Math.sin(radialDirection); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public final double getRadialDirection() { + return radialDirection; + } + + public final void setRadialDirection(double radialDirection) { + radialDirection = MathUtil.reduce180(radialDirection); + if (MathUtil.equals(this.radialDirection, radialDirection)) { + return; + } + this.radialDirection = radialDirection; + shiftY = radialPosition * Math.cos(radialDirection); + shiftZ = radialPosition * Math.sin(radialDirection); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + /** + * Shift the coordinates according to the radial position and direction. + */ + @Override + public final Coordinate[] shiftCoordinates(Coordinate[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(0, shiftY, shiftZ); + } + return array; + } + + @Override + public final Coordinate getComponentCG() { + return new Coordinate(length / 2, shiftY, shiftZ, getComponentMass()); + } + + @Override + public final double getLongitudinalUnitInertia() { + return (3 * pow2(radius) + pow2(length)) / 12; + } + + @Override + public final double getRotationalUnitInertia() { + return pow2(radius) / 2; + } + + @Override + public final Collection getComponentBounds() { + Collection c = new ArrayList(); + addBound(c, 0, radius); + addBound(c, length, radius); + return c; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/MotorMount.java b/core/src/net/sf/openrocket/rocketcomponent/MotorMount.java new file mode 100644 index 00000000..1c849d4d --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/MotorMount.java @@ -0,0 +1,213 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Coordinate; + +public interface MotorMount extends ChangeSource { + + public static enum IgnitionEvent { + //// Automatic (launch or ejection charge) + AUTOMATIC("MotorMount.IgnitionEvent.AUTOMATIC") { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + int count = source.getRocket().getStageCount(); + int stage = source.getStageNumber(); + + if (stage == count - 1) { + return LAUNCH.isActivationEvent(e, source); + } else { + return EJECTION_CHARGE.isActivationEvent(e, source); + } + } + }, + //// Launch + LAUNCH("MotorMount.IgnitionEvent.LAUNCH") { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + return (e.getType() == FlightEvent.Type.LAUNCH); + } + }, + //// First ejection charge of previous stage + EJECTION_CHARGE("MotorMount.IgnitionEvent.EJECTION_CHARGE") { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + if (e.getType() != FlightEvent.Type.EJECTION_CHARGE) + return false; + + int charge = e.getSource().getStageNumber(); + int mount = source.getStageNumber(); + return (mount + 1 == charge); + } + }, + //// First burnout of previous stage + BURNOUT("MotorMount.IgnitionEvent.BURNOUT") { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + if (e.getType() != FlightEvent.Type.BURNOUT) + return false; + + int charge = e.getSource().getStageNumber(); + int mount = source.getStageNumber(); + return (mount + 1 == charge); + } + }, + //// Never + NEVER("MotorMount.IgnitionEvent.NEVER") { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + return false; + } + }, + ; + + + private static final Translator trans = Application.getTranslator(); + private final String description; + + IgnitionEvent(String description) { + this.description = description; + } + + public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source); + + @Override + public String toString() { + return trans.get(description); + } + }; + + + /** + * Is the component currently a motor mount. + * + * @return whether the component holds a motor. + */ + public boolean isMotorMount(); + + /** + * Set whether the component is currently a motor mount. + */ + public void setMotorMount(boolean mount); + + + /** + * Return the motor for the motor configuration. May return null + * if no motor has been set. This method must return null if ID + * is null or if the ID is not valid for the current rocket + * (or if the component is not part of any rocket). + * + * @param id the motor configuration ID + * @return the motor, or null if not set. + */ + public Motor getMotor(String id); + + /** + * Set the motor for the motor configuration. May be set to null + * to remove the motor. + * + * @param id the motor configuration ID + * @param motor the motor, or null. + */ + public void setMotor(String id, Motor motor); + + /** + * Get the number of similar motors clustered. + * + * TODO: HIGH: This should not be used, since the components themselves can be clustered + * + * @return the number of motors. + */ + @Deprecated + public int getMotorCount(); + + + + /** + * Return the ejection charge delay of given motor configuration. + * A "plugged" motor without an ejection charge is given by + * {@link Motor#PLUGGED} (Double.POSITIVE_INFINITY). + * + * @param id the motor configuration ID + * @return the ejection charge delay. + */ + public double getMotorDelay(String id); + + /** + * Set the ejection change delay of the given motor configuration. + * The ejection charge is disable (a "plugged" motor) is set by + * {@link Motor#PLUGGED} (Double.POSITIVE_INFINITY). + * + * @param id the motor configuration ID + * @param delay the ejection charge delay. + */ + public void setMotorDelay(String id, double delay); + + + /** + * Return the event that ignites this motor. + * + * @return the {@link IgnitionEvent} that ignites this motor. + */ + public IgnitionEvent getIgnitionEvent(); + + /** + * Sets the event that ignites this motor. + * + * @param event the {@link IgnitionEvent} that ignites this motor. + */ + public void setIgnitionEvent(IgnitionEvent event); + + + /** + * Returns the ignition delay of this motor. + * + * @return the ignition delay + */ + public double getIgnitionDelay(); + + /** + * Sets the ignition delay of this motor. + * + * @param delay the ignition delay. + */ + public void setIgnitionDelay(double delay); + + + /** + * Return the distance that the motors hang outside this motor mount. + * + * @return the overhang length. + */ + public double getMotorOverhang(); + + /** + * Sets the distance that the motors hang outside this motor mount. + * + * @param overhang the overhang length. + */ + public void setMotorOverhang(double overhang); + + + + /** + * Return the inner diameter of the motor mount. + * + * @return the inner diameter of the motor mount. + */ + public double getMotorMountDiameter(); + + + /** + * Return the position of the motor relative to this component. The coordinate + * is that of the front cap of the motor. + * + * @return the position of the motor relative to this component. + * @throws IllegalArgumentException if a motor with the specified ID does not exist. + */ + public Coordinate getMotorPosition(String id); + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java b/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java new file mode 100644 index 00000000..a4de3a9c --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/NoseCone.java @@ -0,0 +1,123 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +/** + * Rocket nose cones of various types. Implemented as a transition with the + * fore radius == 0. + * + * @author Sampo Niskanen + */ + +public class NoseCone extends Transition { + private static final Translator trans = Application.getTranslator(); + + + /********* Constructors **********/ + public NoseCone() { + this(Transition.Shape.OGIVE, 6 * DEFAULT_RADIUS, DEFAULT_RADIUS); + } + + public NoseCone(Transition.Shape type, double length, double radius) { + super(); + super.setType(type); + super.setForeRadiusAutomatic(false); + super.setForeRadius(0); + super.setForeShoulderLength(0); + super.setForeShoulderRadius(0.9 * radius); + super.setForeShoulderThickness(0); + super.setForeShoulderCapped(filled); + super.setThickness(0.002); + super.setLength(length); + super.setClipped(false); + + } + + + /********** Get/set methods for component parameters **********/ + + @Override + public double getForeRadius() { + return 0; + } + + @Override + public void setForeRadius(double r) { + // No-op + } + + @Override + public boolean isForeRadiusAutomatic() { + return false; + } + + @Override + public void setForeRadiusAutomatic(boolean b) { + // No-op + } + + @Override + public double getForeShoulderLength() { + return 0; + } + + @Override + public double getForeShoulderRadius() { + return 0; + } + + @Override + public double getForeShoulderThickness() { + return 0; + } + + @Override + public boolean isForeShoulderCapped() { + return false; + } + + @Override + public void setForeShoulderCapped(boolean capped) { + // No-op + } + + @Override + public void setForeShoulderLength(double foreShoulderLength) { + // No-op + } + + @Override + public void setForeShoulderRadius(double foreShoulderRadius) { + // No-op + } + + @Override + public void setForeShoulderThickness(double foreShoulderThickness) { + // No-op + } + + @Override + public boolean isClipped() { + return false; + } + + @Override + public void setClipped(boolean b) { + // No-op + } + + + + /********** RocketComponent methods **********/ + + /** + * Return component name. + */ + @Override + public String getComponentName() { + //// Nose cone + return trans.get("NoseCone.NoseCone"); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Parachute.java b/core/src/net/sf/openrocket/rocketcomponent/Parachute.java new file mode 100644 index 00000000..640810bb --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Parachute.java @@ -0,0 +1,122 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; + +public class Parachute extends RecoveryDevice { + private static final Translator trans = Application.getTranslator(); + + public static final double DEFAULT_CD = 0.8; + + private double diameter; + + private Material lineMaterial; + private int lineCount = 6; + private double lineLength = 0.3; + + + public Parachute() { + this.diameter = 0.3; + this.lineMaterial = Application.getPreferences().getDefaultComponentMaterial(Parachute.class, Material.Type.LINE); + this.lineLength = 0.3; + } + + + public double getDiameter() { + return diameter; + } + + public void setDiameter(double d) { + if (MathUtil.equals(this.diameter, d)) + return; + this.diameter = d; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + public final Material getLineMaterial() { + return lineMaterial; + } + + public final void setLineMaterial(Material mat) { + if (mat.getType() != Material.Type.LINE) { + throw new IllegalArgumentException("Attempted to set non-line material " + mat); + } + if (mat.equals(lineMaterial)) + return; + this.lineMaterial = mat; + if (getLineCount() != 0) + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + else + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + public final int getLineCount() { + return lineCount; + } + + public final void setLineCount(int n) { + if (this.lineCount == n) + return; + this.lineCount = n; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public final double getLineLength() { + return lineLength; + } + + public final void setLineLength(double length) { + if (MathUtil.equals(this.lineLength, length)) + return; + this.lineLength = length; + if (getLineCount() != 0) + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + else + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + @Override + public double getComponentCD(double mach) { + return DEFAULT_CD; // TODO: HIGH: Better parachute CD estimate? + } + + @Override + public double getArea() { + return Math.PI * MathUtil.pow2(diameter / 2); + } + + public void setArea(double area) { + if (MathUtil.equals(getArea(), area)) + return; + diameter = MathUtil.safeSqrt(area / Math.PI) * 2; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + @Override + public double getComponentMass() { + return super.getComponentMass() + + getLineCount() * getLineLength() * getLineMaterial().getDensity(); + } + + @Override + public String getComponentName() { + //// Parachute + return trans.get("Parachute.Parachute"); + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class type) { + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/RadialParent.java b/core/src/net/sf/openrocket/rocketcomponent/RadialParent.java new file mode 100644 index 00000000..41c731af --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/RadialParent.java @@ -0,0 +1,31 @@ +package net.sf.openrocket.rocketcomponent; + +public interface RadialParent { + + /** + * Return the outer radius of the component at local coordinate x. + * Values for x < 0 and x > getLength() are undefined. + * + * @param x the lengthwise position in the coordinates of this component. + * @return the outer radius of the component at that position. + */ + public double getOuterRadius(double x); + + /** + * Return the inner radius of the component at local coordinate x. + * Values for x < 0 and x > getLength() are undefined. + * + * @param x the lengthwise position in the coordinates of this component. + * @return the inner radius of the component at that position. + */ + public double getInnerRadius(double x); + + + /** + * Return the length of this component. + * + * @return the length of this component. + */ + public double getLength(); + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java new file mode 100644 index 00000000..46a35143 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java @@ -0,0 +1,84 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +/** + * An inner component that consists of a hollow cylindrical component. This can be + * an inner tube, tube coupler, centering ring, bulkhead etc. + * + * The properties include the inner and outer radii, length and radial position. + * + * @author Sampo Niskanen + */ +public abstract class RadiusRingComponent extends RingComponent implements Coaxial { + + protected double outerRadius = 0; + protected double innerRadius = 0; + + @Override + public double getOuterRadius() { + if (outerRadiusAutomatic && getParent() instanceof RadialParent) { + RocketComponent parent = getParent(); + double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x; + double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x; + pos1 = MathUtil.clamp(pos1, 0, parent.getLength()); + pos2 = MathUtil.clamp(pos2, 0, parent.getLength()); + outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1), + ((RadialParent)parent).getInnerRadius(pos2)); + } + + return outerRadius; + } + + @Override + public void setOuterRadius(double r) { + r = Math.max(r,0); + if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic()) + return; + + outerRadius = r; + outerRadiusAutomatic = false; + if (getInnerRadius() > r) { + innerRadius = r; + innerRadiusAutomatic = false; + } + + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + @Override + public double getInnerRadius() { + return innerRadius; + } + @Override + public void setInnerRadius(double r) { + r = Math.max(r,0); + if (MathUtil.equals(innerRadius, r)) + return; + + innerRadius = r; + innerRadiusAutomatic = false; + if (getOuterRadius() < r) { + outerRadius = r; + outerRadiusAutomatic = false; + } + + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + @Override + public double getThickness() { + return Math.max(getOuterRadius() - getInnerRadius(), 0); + } + @Override + public void setThickness(double thickness) { + double outer = getOuterRadius(); + + thickness = MathUtil.clamp(thickness, 0, outer); + setInnerRadius(outer - thickness); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java b/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java new file mode 100644 index 00000000..4156bfbe --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java @@ -0,0 +1,219 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Pair; + + +/** + * RecoveryDevice is a class representing devices that slow down descent. + * Recovery devices report that they have no aerodynamic effect, since they + * are within the rocket during ascent. + *

+ * A recovery device includes a surface material of which it is made of. + * The mass of the component is calculated based on the material and the + * area of the device from {@link #getArea()}. {@link #getComponentMass()} + * may be overridden if additional mass needs to be included. + * + * @author Sampo Niskanen + */ +public abstract class RecoveryDevice extends MassObject { + private static final Translator trans = Application.getTranslator(); + + public static enum DeployEvent { + //// Launch (plus NN seconds) + LAUNCH(trans.get("RecoveryDevice.DeployEvent.LAUNCH")) { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + return e.getType() == FlightEvent.Type.LAUNCH; + } + }, + //// First ejection charge of this stage + EJECTION(trans.get("RecoveryDevice.DeployEvent.EJECTION")) { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + if (e.getType() != FlightEvent.Type.EJECTION_CHARGE) + return false; + RocketComponent charge = e.getSource(); + return charge.getStageNumber() == source.getStageNumber(); + } + }, + //// Apogee + APOGEE(trans.get("RecoveryDevice.DeployEvent.APOGEE")) { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + return e.getType() == FlightEvent.Type.APOGEE; + } + }, + //// Specific altitude during descent + ALTITUDE(trans.get("RecoveryDevice.DeployEvent.ALTITUDE")) { + @SuppressWarnings("unchecked") + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + if (e.getType() != FlightEvent.Type.ALTITUDE) + return false; + + double alt = ((RecoveryDevice)source).getDeployAltitude(); + Pair altitude = (Pair)e.getData(); + + return (altitude.getU() >= alt) && (altitude.getV() <= alt); + } + }, + //// Never + NEVER(trans.get("RecoveryDevice.DeployEvent.NEVER")) { + @Override + public boolean isActivationEvent(FlightEvent e, RocketComponent source) { + return false; + } + } + ; + + private final String description; + + DeployEvent(String description) { + this.description = description; + } + + public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source); + + @Override + public String toString() { + return description; + } + + } + + + private DeployEvent deployEvent = DeployEvent.EJECTION; + private double deployAltitude = 200; + private double deployDelay = 0; + + private double cd = Parachute.DEFAULT_CD; + private boolean cdAutomatic = true; + + + private Material.Surface material; + + + public RecoveryDevice() { + this(Application.getPreferences().getDefaultComponentMaterial(RecoveryDevice.class, Material.Type.SURFACE)); + } + + public RecoveryDevice(Material material) { + super(); + setMaterial(material); + } + + public RecoveryDevice(double length, double radius, Material material) { + super(length, radius); + setMaterial(material); + } + + + + + public abstract double getArea(); + + public abstract double getComponentCD(double mach); + + + + public double getCD() { + return getCD(0); + } + + public double getCD(double mach) { + if (cdAutomatic) + cd = getComponentCD(mach); + return cd; + } + + public void setCD(double cd) { + if (MathUtil.equals(this.cd, cd) && !isCDAutomatic()) + return; + this.cd = cd; + this.cdAutomatic = false; + fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); + } + + + public boolean isCDAutomatic() { + return cdAutomatic; + } + + public void setCDAutomatic(boolean auto) { + if (cdAutomatic == auto) + return; + this.cdAutomatic = auto; + fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); + } + + + + public final Material getMaterial() { + return material; + } + + public final void setMaterial(Material mat) { + if (!(mat instanceof Material.Surface)) { + throw new IllegalArgumentException("Attempted to set non-surface material "+mat); + } + if (mat.equals(material)) + return; + this.material = (Material.Surface)mat; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + public DeployEvent getDeployEvent() { + return deployEvent; + } + + public void setDeployEvent(DeployEvent deployEvent) { + if (this.deployEvent == deployEvent) + return; + this.deployEvent = deployEvent; + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + } + + + public double getDeployAltitude() { + return deployAltitude; + } + + public void setDeployAltitude(double deployAltitude) { + if (MathUtil.equals(this.deployAltitude, deployAltitude)) + return; + this.deployAltitude = deployAltitude; + if (getDeployEvent() == DeployEvent.ALTITUDE) + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + else + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + public double getDeployDelay() { + return deployDelay; + } + + public void setDeployDelay(double delay) { + delay = MathUtil.max(delay, 0); + if (MathUtil.equals(this.deployDelay, delay)) + return; + this.deployDelay = delay; + fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); + } + + + + @Override + public double getComponentMass() { + return getArea() * getMaterial().getDensity(); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ReferenceType.java b/core/src/net/sf/openrocket/rocketcomponent/ReferenceType.java new file mode 100644 index 00000000..02264ebe --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ReferenceType.java @@ -0,0 +1,50 @@ +/** + * + */ +package net.sf.openrocket.rocketcomponent; + +public enum ReferenceType { + + NOSECONE { + @Override + public double getReferenceLength(Configuration config) { + for (RocketComponent c: config) { + if (c instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent)c; + if (s.getForeRadius() >= 0.0005) + return s.getForeRadius() * 2; + if (s.getAftRadius() >= 0.0005) + return s.getAftRadius() * 2; + } + } + return Rocket.DEFAULT_REFERENCE_LENGTH; + } + }, + + MAXIMUM { + @Override + public double getReferenceLength(Configuration config) { + double r = 0; + for (RocketComponent c: config) { + if (c instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent)c; + r = Math.max(r, s.getForeRadius()); + r = Math.max(r, s.getAftRadius()); + } + } + r *= 2; + if (r < 0.001) + r = Rocket.DEFAULT_REFERENCE_LENGTH; + return r; + } + }, + + CUSTOM { + @Override + public double getReferenceLength(Configuration config) { + return config.getRocket().getCustomReferenceLength(); + } + }; + + public abstract double getReferenceLength(Configuration rocket); +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java new file mode 100644 index 00000000..9e2c16bc --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/RingComponent.java @@ -0,0 +1,221 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +/** + * An inner component that consists of a hollow cylindrical component. This can be + * an inner tube, tube coupler, centering ring, bulkhead etc. + * + * The properties include the inner and outer radii, length and radial position. + * + * @author Sampo Niskanen + */ +public abstract class RingComponent extends StructuralComponent implements Coaxial { + + protected boolean outerRadiusAutomatic = false; + protected boolean innerRadiusAutomatic = false; + + + private double radialDirection = 0; + private double radialPosition = 0; + + private double shiftY = 0; + private double shiftZ = 0; + + + + @Override + public abstract double getOuterRadius(); + + @Override + public abstract void setOuterRadius(double r); + + @Override + public abstract double getInnerRadius(); + + @Override + public abstract void setInnerRadius(double r); + + @Override + public abstract double getThickness(); + + public abstract void setThickness(double thickness); + + + public final boolean isOuterRadiusAutomatic() { + return outerRadiusAutomatic; + } + + // Setter is protected, subclasses may make it public + protected void setOuterRadiusAutomatic(boolean auto) { + if (auto == outerRadiusAutomatic) + return; + outerRadiusAutomatic = auto; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public final boolean isInnerRadiusAutomatic() { + return innerRadiusAutomatic; + } + + // Setter is protected, subclasses may make it public + protected void setInnerRadiusAutomatic(boolean auto) { + if (auto == innerRadiusAutomatic) + return; + innerRadiusAutomatic = auto; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + public final void setLength(double length) { + double l = Math.max(length, 0); + if (this.length == l) + return; + + this.length = l; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + /** + * Return the radial direction of displacement of the component. Direction 0 + * is equivalent to the Y-direction. + * + * @return the radial direction. + */ + public double getRadialDirection() { + return radialDirection; + } + + /** + * Set the radial direction of displacement of the component. Direction 0 + * is equivalent to the Y-direction. + * + * @param dir the radial direction. + */ + public void setRadialDirection(double dir) { + dir = MathUtil.reduce180(dir); + if (radialDirection == dir) + return; + radialDirection = dir; + shiftY = radialPosition * Math.cos(radialDirection); + shiftZ = radialPosition * Math.sin(radialDirection); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + /** + * Return the radial position of the component. The position is the distance + * of the center of the component from the center of the parent component. + * + * @return the radial position. + */ + public double getRadialPosition() { + return radialPosition; + } + + /** + * Set the radial position of the component. The position is the distance + * of the center of the component from the center of the parent component. + * + * @param pos the radial position. + */ + public void setRadialPosition(double pos) { + pos = Math.max(pos, 0); + if (radialPosition == pos) + return; + radialPosition = pos; + shiftY = radialPosition * Math.cos(radialDirection); + shiftZ = radialPosition * Math.sin(radialDirection); + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public double getRadialShiftY() { + return shiftY; + } + + public double getRadialShiftZ() { + return shiftZ; + } + + public void setRadialShift(double y, double z) { + radialPosition = Math.hypot(y, z); + radialDirection = Math.atan2(z, y); + + // Re-calculate to ensure consistency + shiftY = radialPosition * Math.cos(radialDirection); + shiftZ = radialPosition * Math.sin(radialDirection); + assert (MathUtil.equals(y, shiftY)); + assert (MathUtil.equals(z, shiftZ)); + + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + /** + * Return the number of times the component is multiplied. + */ + public int getClusterCount() { + if (this instanceof Clusterable) + return ((Clusterable) this).getClusterConfiguration().getClusterCount(); + return 1; + } + + + /** + * Shift the coordinates according to the radial position and direction. + */ + @Override + public Coordinate[] shiftCoordinates(Coordinate[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(0, shiftY, shiftZ); + } + return array; + } + + + @Override + public Collection getComponentBounds() { + List bounds = new ArrayList(); + addBound(bounds, 0, getOuterRadius()); + addBound(bounds, length, getOuterRadius()); + return bounds; + } + + + + @Override + public Coordinate getComponentCG() { + return new Coordinate(length / 2, 0, 0, getComponentMass()); + } + + @Override + public double getComponentMass() { + return ringMass(getOuterRadius(), getInnerRadius(), getLength(), + getMaterial().getDensity()) * getClusterCount(); + } + + + @Override + public double getLongitudinalUnitInertia() { + return ringLongitudinalUnitInertia(getOuterRadius(), getInnerRadius(), getLength()); + } + + @Override + public double getRotationalUnitInertia() { + return ringRotationalUnitInertia(getOuterRadius(), getInnerRadius()); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Rocket.java b/core/src/net/sf/openrocket/rocketcomponent/Rocket.java new file mode 100644 index 00000000..226f9527 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Rocket.java @@ -0,0 +1,837 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.Collection; +import java.util.Collections; +import java.util.EventListener; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.Chars; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.StateChangeListener; +import net.sf.openrocket.util.UniqueID; + + +/** + * Base for all rocket components. This is the "starting point" for all rocket trees. + * It provides the actual implementations of several methods defined in RocketComponent + * (eg. the rocket listener lists) and the methods defined in RocketComponent call these. + * It also defines some other methods that concern the whole rocket, and helper methods + * that keep information about the program state. + * + * @author Sampo Niskanen + */ + +public class Rocket extends RocketComponent { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + public static final double DEFAULT_REFERENCE_LENGTH = 0.01; + + + /** + * List of component change listeners. + */ + private List listenerList = new ArrayList(); + + /** + * When freezeList != null, events are not dispatched but stored in the list. + * When the structure is thawed, a single combined event will be fired. + */ + private List freezeList = null; + + + private int modID; + private int massModID; + private int aeroModID; + private int treeModID; + private int functionalModID; + + + private ReferenceType refType = ReferenceType.MAXIMUM; // Set in constructor + private double customReferenceLength = DEFAULT_REFERENCE_LENGTH; + + + // The default configuration used in dialogs + private final Configuration defaultConfiguration; + + + private String designer = ""; + private String revision = ""; + + + // Motor configuration list + private ArrayList motorConfigurationIDs = new ArrayList(); + private HashMap motorConfigurationNames = new HashMap(); + { + motorConfigurationIDs.add(null); + } + + + // Does the rocket have a perfect finish (a notable amount of laminar flow) + private boolean perfectFinish = false; + + + + ///////////// Constructor ///////////// + + public Rocket() { + super(RocketComponent.Position.AFTER); + modID = UniqueID.next(); + massModID = modID; + aeroModID = modID; + treeModID = modID; + functionalModID = modID; + defaultConfiguration = new Configuration(this); + } + + + + public String getDesigner() { + checkState(); + return designer; + } + + public void setDesigner(String s) { + if (s == null) + s = ""; + designer = s; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + public String getRevision() { + checkState(); + return revision; + } + + public void setRevision(String s) { + if (s == null) + s = ""; + revision = s; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + + + /** + * Return the number of stages in this rocket. + * + * @return the number of stages in this rocket. + */ + public int getStageCount() { + checkState(); + return this.getChildCount(); + } + + + /** + * Return the non-negative modification ID of this rocket. The ID is changed + * every time any change occurs in the rocket. This can be used to check + * whether it is necessary to void cached data in cases where listeners can not + * or should not be used. + *

+ * Three other modification IDs are also available, {@link #getMassModID()}, + * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time + * a mass change, aerodynamic change, or tree change occur. Even though the values + * of the different modification ID's may be equal, they should be treated totally + * separate. + *

+ * Note that undo events restore the modification IDs that were in use at the + * corresponding undo level. Subsequent modifications, however, produce modIDs + * distinct from those already used. + * + * @return a unique ID number for this modification state. + */ + public int getModID() { + return modID; + } + + /** + * Return the non-negative mass modification ID of this rocket. See + * {@link #getModID()} for details. + * + * @return a unique ID number for this mass-modification state. + */ + public int getMassModID() { + return massModID; + } + + /** + * Return the non-negative aerodynamic modification ID of this rocket. See + * {@link #getModID()} for details. + * + * @return a unique ID number for this aerodynamic-modification state. + */ + public int getAerodynamicModID() { + return aeroModID; + } + + /** + * Return the non-negative tree modification ID of this rocket. See + * {@link #getModID()} for details. + * + * @return a unique ID number for this tree-modification state. + */ + public int getTreeModID() { + return treeModID; + } + + /** + * Return the non-negative functional modificationID of this rocket. + * This changes every time a functional change occurs. + * + * @return a unique ID number for this functional modification state. + */ + public int getFunctionalModID() { + return functionalModID; + } + + + + + public ReferenceType getReferenceType() { + checkState(); + return refType; + } + + public void setReferenceType(ReferenceType type) { + if (refType == type) + return; + refType = type; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + public double getCustomReferenceLength() { + checkState(); + return customReferenceLength; + } + + public void setCustomReferenceLength(double length) { + if (MathUtil.equals(customReferenceLength, length)) + return; + + this.customReferenceLength = Math.max(length, 0.001); + + if (refType == ReferenceType.CUSTOM) { + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + } + + + + + + /** + * Set whether the rocket has a perfect finish. This will affect whether the + * boundary layer is assumed to be fully turbulent or not. + * + * @param perfectFinish whether the finish is perfect. + */ + public void setPerfectFinish(boolean perfectFinish) { + if (this.perfectFinish == perfectFinish) + return; + this.perfectFinish = perfectFinish; + fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); + } + + + + /** + * Get whether the rocket has a perfect finish. + * + * @return the perfectFinish + */ + public boolean isPerfectFinish() { + return perfectFinish; + } + + + + + + /** + * Make a deep copy of the Rocket structure. This method is exposed as public to allow + * for undo/redo system functionality. + */ + @SuppressWarnings("unchecked") + @Override + public Rocket copyWithOriginalID() { + Rocket copy = (Rocket) super.copyWithOriginalID(); + copy.motorConfigurationIDs = this.motorConfigurationIDs.clone(); + copy.motorConfigurationNames = + (HashMap) this.motorConfigurationNames.clone(); + copy.resetListeners(); + + return copy; + } + + /** + * Load the rocket structure from the source. The method loads the fields of this + * Rocket object and copies the references to siblings from the source. + * The object source should not be used after this call, as it is in + * an illegal state! + *

+ * This method is meant to be used in conjunction with undo/redo functionality, + * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree + * changes. + */ + @SuppressWarnings("unchecked") + public void loadFrom(Rocket r) { + + // Store list of components to invalidate after event has been fired + List toInvalidate = this.copyFrom(r); + + int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE; + if (this.massModID != r.massModID) + type |= ComponentChangeEvent.MASS_CHANGE; + if (this.aeroModID != r.aeroModID) + type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; + // Loading a rocket is always a tree change since the component objects change + type |= ComponentChangeEvent.TREE_CHANGE; + + this.modID = r.modID; + this.massModID = r.massModID; + this.aeroModID = r.aeroModID; + this.treeModID = r.treeModID; + this.functionalModID = r.functionalModID; + this.refType = r.refType; + this.customReferenceLength = r.customReferenceLength; + + this.motorConfigurationIDs = r.motorConfigurationIDs.clone(); + this.motorConfigurationNames = + (HashMap) r.motorConfigurationNames.clone(); + this.perfectFinish = r.perfectFinish; + + String id = defaultConfiguration.getMotorConfigurationID(); + if (!this.motorConfigurationIDs.contains(id)) + defaultConfiguration.setMotorConfigurationID(null); + + this.checkComponentStructure(); + + fireComponentChangeEvent(type); + + // Invalidate obsolete components after event + for (RocketComponent c : toInvalidate) { + c.invalidate(); + } + } + + + + + /////// Implement the ComponentChangeListener lists + + /** + * Creates a new EventListenerList for this component. This is necessary when cloning + * the structure. + */ + public void resetListeners() { + // System.out.println("RESETTING LISTENER LIST of Rocket "+this); + listenerList = new ArrayList(); + } + + + public void printListeners() { + System.out.println("" + this + " has " + listenerList.size() + " listeners:"); + int i =0; + for( EventListener l : listenerList ) { + System.out.println(" " + (i) + ": " + l); + i++; + } + } + + @Override + public void addComponentChangeListener(ComponentChangeListener l) { + checkState(); + listenerList.add(l); + log.verbose("Added ComponentChangeListener " + l + ", current number of listeners is " + + listenerList.size()); + } + + @Override + public void removeComponentChangeListener(ComponentChangeListener l) { + listenerList.remove(l); + log.verbose("Removed ComponentChangeListener " + l + ", current number of listeners is " + + listenerList.size()); + } + + + @Override + public void addChangeListener(EventListener l) { + checkState(); + listenerList.add(l); + log.verbose("Added ChangeListener " + l + ", current number of listeners is " + + listenerList.size()); + } + + @Override + public void removeChangeListener(EventListener l) { + listenerList.remove(l); + log.verbose("Removed ChangeListener " + l + ", current number of listeners is " + + listenerList.size()); + } + + + @Override + protected void fireComponentChangeEvent(ComponentChangeEvent e) { + mutex.lock("fireComponentChangeEvent"); + try { + checkState(); + + // Update modification ID's only for normal (not undo/redo) events + if (!e.isUndoChange()) { + modID = UniqueID.next(); + if (e.isMassChange()) + massModID = modID; + if (e.isAerodynamicChange()) + aeroModID = modID; + if (e.isTreeChange()) + treeModID = modID; + if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE) + functionalModID = modID; + } + + // Check whether frozen + if (freezeList != null) { + log.debug("Rocket is in frozen state, adding event " + e + " info freeze list"); + freezeList.add(e); + return; + } + + log.debug("Firing rocket change event " + e); + + // Notify all components first + Iterator iterator = this.iterator(true); + while (iterator.hasNext()) { + iterator.next().componentChanged(e); + } + + // Notify all listeners + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] list = listenerList.toArray( new EventListener[0] ); + for ( EventListener l : list ) { + if ( l instanceof ComponentChangeListener ) { + ((ComponentChangeListener) l ).componentChanged(e); + } else if ( l instanceof StateChangeListener ) { + ((StateChangeListener) l ).stateChanged(e); + } + } + } finally { + mutex.unlock("fireComponentChangeEvent"); + } + } + + + /** + * Freezes the rocket structure from firing any events. This may be performed to + * combine several actions on the structure into a single large action. + * thaw() must always be called afterwards. + * + * NOTE: Always use a try/finally to ensure thaw() is called: + *

+	 *     Rocket r = c.getRocket();
+	 *     try {
+	 *         r.freeze();
+	 *         // do stuff
+	 *     } finally {
+	 *         r.thaw();
+	 *     }
+	 * 
+ * + * @see #thaw() + */ + public void freeze() { + checkState(); + if (freezeList == null) { + freezeList = new LinkedList(); + log.debug("Freezing Rocket"); + } else { + Application.getExceptionHandler().handleErrorCondition("Attempting to freeze Rocket when it is already frozen, " + + "freezeList=" + freezeList); + } + } + + /** + * Thaws a frozen rocket structure and fires a combination of the events fired during + * the freeze. The event type is a combination of those fired and the source is the + * last component to have been an event source. + * + * @see #freeze() + */ + public void thaw() { + checkState(); + if (freezeList == null) { + Application.getExceptionHandler().handleErrorCondition("Attempting to thaw Rocket when it is not frozen"); + return; + } + if (freezeList.size() == 0) { + log.warn("Thawing rocket with no changes made"); + freezeList = null; + return; + } + + log.debug("Thawing rocket, freezeList=" + freezeList); + + int type = 0; + Object c = null; + for (ComponentChangeEvent e : freezeList) { + type = type | e.getType(); + c = e.getSource(); + } + freezeList = null; + + fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent) c, type)); + } + + + + + //////// Motor configurations //////// + + + /** + * Return the default configuration. This should be used in the user interface + * to ensure a consistent rocket configuration between dialogs. It should NOT + * be used in simulations not relating to the UI. + * + * @return the default {@link Configuration}. + */ + public Configuration getDefaultConfiguration() { + checkState(); + return defaultConfiguration; + } + + + /** + * Return an array of the motor configuration IDs. This array is guaranteed + * to contain the null ID as the first element. + * + * @return an array of the motor configuration IDs. + */ + public String[] getMotorConfigurationIDs() { + checkState(); + return motorConfigurationIDs.toArray(new String[0]); + } + + /** + * Add a new motor configuration ID to the motor configurations. The new ID + * is returned. + * + * @return the new motor configuration ID. + */ + public String newMotorConfigurationID() { + checkState(); + String id = UUID.randomUUID().toString(); + motorConfigurationIDs.add(id); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + return id; + } + + /** + * Add a specified motor configuration ID to the motor configurations. + * + * @param id the motor configuration ID. + * @return true if successful, false if the ID was already used. + */ + public boolean addMotorConfigurationID(String id) { + checkState(); + if (id == null || motorConfigurationIDs.contains(id)) + return false; + motorConfigurationIDs.add(id); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + return true; + } + + /** + * Remove a motor configuration ID from the configuration IDs. The null + * ID cannot be removed, and an attempt to remove it will be silently ignored. + * + * @param id the motor configuration ID to remove + */ + public void removeMotorConfigurationID(String id) { + checkState(); + if (id == null) + return; + motorConfigurationIDs.remove(id); + fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); + } + + + /** + * Check whether id is a valid motor configuration ID. + * + * @param id the configuration ID. + * @return whether a motor configuration with that ID exists. + */ + public boolean isMotorConfigurationID(String id) { + checkState(); + return motorConfigurationIDs.contains(id); + } + + + + /** + * Check whether the given motor configuration ID has motors defined for it. + * + * @param id the motor configuration ID (may be invalid). + * @return whether any motors are defined for it. + */ + public boolean hasMotors(String id) { + checkState(); + if (id == null) + return false; + + Iterator iterator = this.iterator(); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + + if (c instanceof MotorMount) { + MotorMount mount = (MotorMount) c; + if (!mount.isMotorMount()) + continue; + if (mount.getMotor(id) != null) { + return true; + } + } + } + return false; + } + + + /** + * Return the user-set name of the motor configuration. If no name has been set, + * returns an empty string (not null). + * + * @param id the motor configuration id + * @return the configuration name + */ + public String getMotorConfigurationName(String id) { + checkState(); + if (!isMotorConfigurationID(id)) + return ""; + String s = motorConfigurationNames.get(id); + if (s == null) + return ""; + return s; + } + + + /** + * Set the name of the motor configuration. A name can be unset by passing + * null or an empty string. + * + * @param id the motor configuration id + * @param name the name for the motor configuration + */ + public void setMotorConfigurationName(String id, String name) { + checkState(); + motorConfigurationNames.put(id, name); + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + /** + * Return either the motor configuration name (if set) or its description. + * + * @param id the motor configuration ID. + * @return a textual representation of the configuration + */ + public String getMotorConfigurationNameOrDescription(String id) { + checkState(); + String name; + + name = getMotorConfigurationName(id); + if (name != null && !name.equals("")) + return name; + + return getMotorConfigurationDescription(id); + } + + /** + * Return a description for the motor configuration, generated from the motor + * designations of the components. + * + * @param id the motor configuration ID. + * @return a textual representation of the configuration + */ + @SuppressWarnings("null") + public String getMotorConfigurationDescription(String id) { + checkState(); + String name; + int motorCount = 0; + + // Generate the description + + // First iterate over each stage and store the designations of each motor + List> list = new ArrayList>(); + List currentList = null; + + Iterator iterator = this.iterator(); + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + + if (c instanceof Stage) { + + currentList = new ArrayList(); + list.add(currentList); + + } else if (c instanceof MotorMount) { + + MotorMount mount = (MotorMount) c; + Motor motor = mount.getMotor(id); + + if (mount.isMotorMount() && motor != null) { + String designation = motor.getDesignation(mount.getMotorDelay(id)); + + for (int i = 0; i < mount.getMotorCount(); i++) { + currentList.add(designation); + motorCount++; + } + } + + } + } + + if (motorCount == 0) { + //// [No motors] + return trans.get("Rocket.motorCount.Nomotor"); + } + + // Change multiple occurrences of a motor to n x motor + List stages = new ArrayList(); + + for (List stage : list) { + String stageName = ""; + String previous = null; + int count = 0; + + Collections.sort(stage); + for (String current : stage) { + if (current.equals(previous)) { + + count++; + + } else { + + if (previous != null) { + String s = ""; + if (count > 1) { + s = "" + count + Chars.TIMES + previous; + } else { + s = previous; + } + + if (stageName.equals("")) + stageName = s; + else + stageName = stageName + "," + s; + } + + previous = current; + count = 1; + + } + } + if (previous != null) { + String s = ""; + if (count > 1) { + s = "" + count + Chars.TIMES + previous; + } else { + s = previous; + } + + if (stageName.equals("")) + stageName = s; + else + stageName = stageName + "," + s; + } + + stages.add(stageName); + } + + name = "["; + for (int i = 0; i < stages.size(); i++) { + String s = stages.get(i); + if (s.equals("")) + s = "None"; + if (i == 0) + name = name + s; + else + name = name + "; " + s; + } + name += "]"; + return name; + } + + + + //////// Obligatory component information + + + @Override + public String getComponentName() { + //// Rocket + return trans.get("Rocket.compname.Rocket"); + } + + @Override + public Coordinate getComponentCG() { + return new Coordinate(0, 0, 0, 0); + } + + @Override + public double getComponentMass() { + return 0; + } + + @Override + public double getLongitudinalUnitInertia() { + return 0; + } + + @Override + public double getRotationalUnitInertia() { + return 0; + } + + @Override + public Collection getComponentBounds() { + return Collections.emptyList(); + } + + @Override + public boolean isAerodynamic() { + return false; + } + + @Override + public boolean isMassive() { + return false; + } + + @Override + public boolean allowsChildren() { + return true; + } + + /** + * Allows only Stage components to be added to the type Rocket. + */ + @Override + public boolean isCompatible(Class type) { + return (Stage.class.isAssignableFrom(type)); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java new file mode 100644 index 00000000..a5c8c66a --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketComponent.java @@ -0,0 +1,1893 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Deque; +import java.util.EventListener; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.preset.ComponentPreset; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.Color; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Invalidator; +import net.sf.openrocket.util.LineStyle; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.SafetyMutex; +import net.sf.openrocket.util.UniqueID; + + +public abstract class RocketComponent implements ChangeSource, Cloneable, Iterable { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + /* + * Text is suitable to the form + * Position relative to: + */ + public enum Position { + /** Position relative to the top of the parent component. */ + //// Top of the parent component + TOP(trans.get("RocketComponent.Position.TOP")), + /** Position relative to the middle of the parent component. */ + //// Middle of the parent component + MIDDLE(trans.get("RocketComponent.Position.MIDDLE")), + /** Position relative to the bottom of the parent component. */ + //// Bottom of the parent component + BOTTOM(trans.get("RocketComponent.Position.BOTTOM")), + /** Position after the parent component (for body components). */ + //// After the parent component + AFTER(trans.get("RocketComponent.Position.AFTER")), + /** Specify an absolute X-coordinate position. */ + //// Tip of the nose cone + ABSOLUTE(trans.get("RocketComponent.Position.ABSOLUTE")); + + private String title; + + Position(String title) { + this.title = title; + } + + @Override + public String toString() { + return title; + } + } + + /** + * A safety mutex that can be used to prevent concurrent access to this component. + */ + protected SafetyMutex mutex = SafetyMutex.newInstance(); + + //////// Parent/child trees + /** + * Parent component of the current component, or null if none exists. + */ + private RocketComponent parent = null; + + /** + * List of child components of this component. + */ + private ArrayList<RocketComponent> children = new ArrayList<RocketComponent>(); + + + //////// Parameters common to all components: + + /** + * Characteristic length of the component. This is used in calculating the coordinate + * transformations and positions of other components in reference to this component. + * This may and should be used as the "true" length of the component, where applicable. + * By default it is zero, i.e. no translation. + */ + protected double length = 0; + + /** + * Positioning of this component relative to the parent component. + */ + protected Position relativePosition; + + /** + * Offset of the position of this component relative to the normal position given by + * relativePosition. By default zero, i.e. no position change. + */ + protected double position = 0; + + + // Color of the component, null means to use the default color + private Color color = null; + private LineStyle lineStyle = null; + + + // Override mass/CG + private double overrideMass = 0; + private boolean massOverriden = false; + private double overrideCGX = 0; + private boolean cgOverriden = false; + + private boolean overrideSubcomponents = false; + + + // User-given name of the component + private String name = null; + + // User-specified comment + private String comment = ""; + + // Unique ID of the component + private String id = null; + + // Preset component this component is based upon + private ComponentPreset presetComponent = null; + + + /** + * Used to invalidate the component after calling {@link #copyFrom(RocketComponent)}. + */ + private Invalidator invalidator = new Invalidator(this); + + + //// NOTE !!! All fields must be copied in the method copyFrom()! //// + + + + /** + * Default constructor. Sets the name of the component to the component's static name + * and the relative position of the component. + */ + public RocketComponent(Position relativePosition) { + // These must not fire any events, due to Rocket undo system initialization + this.name = getComponentName(); + this.relativePosition = relativePosition; + newID(); + } + + //////////// Methods that must be implemented //////////// + + + /** + * Static component name. The name may not vary of the parameters, it must be static. + */ + public abstract String getComponentName(); // Static component type name + + /** + * Return the component mass (regardless of mass overriding). + */ + public abstract double getComponentMass(); // Mass of non-overridden component + + /** + * Return the component CG and mass (regardless of CG or mass overriding). + */ + public abstract Coordinate getComponentCG(); // CG of non-overridden component + + + /** + * Return the longitudinal (around the y- or z-axis) unitary moment of inertia. + * The unitary moment of inertia is the moment of inertia with the assumption that + * the mass of the component is one kilogram. The inertia is measured in + * respect to the non-overridden CG. + * + * @return the longitudinal unitary moment of inertia of this component. + */ + public abstract double getLongitudinalUnitInertia(); + + + /** + * Return the rotational (around the x-axis) unitary moment of inertia. + * The unitary moment of inertia is the moment of inertia with the assumption that + * the mass of the component is one kilogram. The inertia is measured in + * respect to the non-overridden CG. + * + * @return the rotational unitary moment of inertia of this component. + */ + public abstract double getRotationalUnitInertia(); + + + /** + * Test whether this component allows any children components. This method must + * return true if and only if {@link #isCompatible(Class)} returns true for any + * rocket component class. + * + * @return <code>true</code> if children can be attached to this component, <code>false</code> otherwise. + */ + public abstract boolean allowsChildren(); + + /** + * Test whether the given component type can be added to this component. This type safety + * is enforced by the <code>addChild()</code> methods. The return value of this method + * may change to reflect the current state of this component (e.g. two components of some + * type cannot be placed as children). + * + * @param type The RocketComponent class type to add. + * @return Whether such a component can be added. + */ + public abstract boolean isCompatible(Class<? extends RocketComponent> type); + + + /* Non-abstract helper method */ + /** + * Test whether the given component can be added to this component. This is equivalent + * to calling <code>isCompatible(c.getClass())</code>. + * + * @param c Component to test. + * @return Whether the component can be added. + * @see #isCompatible(Class) + */ + public final boolean isCompatible(RocketComponent c) { + mutex.verify(); + return isCompatible(c.getClass()); + } + + + + /** + * Return a collection of bounding coordinates. The coordinates must be such that + * the component is fully enclosed in their convex hull. + * + * @return a collection of coordinates that bound the component. + */ + public abstract Collection<Coordinate> getComponentBounds(); + + /** + * Return true if the component may have an aerodynamic effect on the rocket. + */ + public abstract boolean isAerodynamic(); + + /** + * Return true if the component may have an effect on the rocket's mass. + */ + public abstract boolean isMassive(); + + + + + + //////////// Methods that may be overridden //////////// + + + /** + * Shift the coordinates in the array corresponding to radial movement. A component + * that has a radial position must shift the coordinates in this array suitably. + * If the component is clustered, then a new array must be returned with a + * coordinate for each cluster. + * <p> + * The default implementation simply returns the array, and thus produces no shift. + * + * @param c an array of coordinates to shift. + * @return an array of shifted coordinates. The method may modify the contents + * of the passed array and return the array itself. + */ + public Coordinate[] shiftCoordinates(Coordinate[] c) { + checkState(); + return c; + } + + + /** + * Called when any component in the tree fires a ComponentChangeEvent. This is by + * default a no-op, but subclasses may override this method to e.g. invalidate + * cached data. The overriding method *must* call + * <code>super.componentChanged(e)</code> at some point. + * + * @param e The event fired + */ + protected void componentChanged(ComponentChangeEvent e) { + // No-op + checkState(); + } + + + + + /** + * Return the user-provided name of the component, or the component base + * name if the user-provided name is empty. This can be used in the UI. + * + * @return A string describing the component. + */ + @Override + public final String toString() { + mutex.verify(); + if (name.length() == 0) + return getComponentName(); + else + return name; + } + + + /** + * Create a string describing the basic component structure from this component downwards. + * @return a string containing the rocket structure + */ + public final String toDebugString() { + mutex.lock("toDebugString"); + try { + StringBuilder sb = new StringBuilder(); + toDebugString(sb); + return sb.toString(); + } finally { + mutex.unlock("toDebugString"); + } + } + + private void toDebugString(StringBuilder sb) { + sb.append(this.getClass().getSimpleName()).append('@').append(System.identityHashCode(this)); + sb.append("[\"").append(this.getName()).append('"'); + for (RocketComponent c : this.children) { + sb.append("; "); + c.toDebugString(sb); + } + sb.append(']'); + } + + + /** + * Make a deep copy of the rocket component tree structure from this component + * downwards for copying purposes. Each component in the copy will be assigned + * a new component ID, making it a safe copy. This method does not fire any events. + * + * @return A deep copy of the structure. + */ + public final RocketComponent copy() { + RocketComponent clone = copyWithOriginalID(); + + Iterator<RocketComponent> iterator = clone.iterator(true); + while (iterator.hasNext()) { + iterator.next().newID(); + } + return clone; + } + + + + /** + * Make a deep copy of the rocket component tree structure from this component + * downwards while maintaining the component ID's. The purpose of this method is + * to allow copies to be created with the original ID's for the purpose of the + * undo/redo mechanism. This method should not be used for other purposes, + * such as copy/paste. This method does not fire any events. + * <p> + * This method must be overridden by any component that refers to mutable objects, + * or if some fields should not be copied. This should be performed by + * <code>RocketComponent c = super.copyWithOriginalID();</code> and then cloning/modifying + * the appropriate fields. + * <p> + * This is not performed as serializing/deserializing for performance reasons. + * + * @return A deep copy of the structure. + */ + protected RocketComponent copyWithOriginalID() { + mutex.lock("copyWithOriginalID"); + try { + checkState(); + RocketComponent clone; + try { + clone = (RocketComponent) this.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException encountered, report a bug!", e); + } + + // Reset the mutex + clone.mutex = SafetyMutex.newInstance(); + + // Reset all parent/child information + clone.parent = null; + clone.children = new ArrayList<RocketComponent>(); + + // Add copied children to the structure without firing events. + for (RocketComponent child : this.children) { + RocketComponent childCopy = child.copyWithOriginalID(); + // Don't use add method since it fires events + clone.children.add(childCopy); + childCopy.parent = clone; + } + + this.checkComponentStructure(); + clone.checkComponentStructure(); + + return clone; + } finally { + mutex.unlock("copyWithOriginalID"); + } + } + + + ////////////// Methods that may not be overridden //////////// + + + + ////////// Common parameter setting/getting ////////// + + /** + * Return the color of the object to use in 2D figures, or <code>null</code> + * to use the default color. + */ + public final Color getColor() { + mutex.verify(); + return color; + } + + /** + * Set the color of the object to use in 2D figures. + */ + public final void setColor(Color c) { + if ((color == null && c == null) || + (color != null && color.equals(c))) + return; + + checkState(); + this.color = c; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + public final LineStyle getLineStyle() { + mutex.verify(); + return lineStyle; + } + + public final void setLineStyle(LineStyle style) { + if (this.lineStyle == style) + return; + checkState(); + this.lineStyle = style; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + + + /** + * Get the current override mass. The mass is not necessarily in use + * at the moment. + * + * @return the override mass + */ + public final double getOverrideMass() { + mutex.verify(); + return overrideMass; + } + + /** + * Set the current override mass. The mass is not set to use by this + * method. + * + * @param m the override mass + */ + public final void setOverrideMass(double m) { + if (MathUtil.equals(m, overrideMass)) + return; + checkState(); + overrideMass = Math.max(m, 0); + if (massOverriden) + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + /** + * Return whether mass override is active for this component. This does NOT + * take into account whether a parent component is overriding the mass. + * + * @return whether the mass is overridden + */ + public final boolean isMassOverridden() { + mutex.verify(); + return massOverriden; + } + + /** + * Set whether the mass is currently overridden. + * + * @param o whether the mass is overridden + */ + public final void setMassOverridden(boolean o) { + if (massOverriden == o) { + return; + } + checkState(); + massOverriden = o; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + + /** + * Return the current override CG. The CG is not necessarily overridden. + * + * @return the override CG + */ + public final Coordinate getOverrideCG() { + mutex.verify(); + return getComponentCG().setX(overrideCGX); + } + + /** + * Return the x-coordinate of the current override CG. + * + * @return the x-coordinate of the override CG. + */ + public final double getOverrideCGX() { + mutex.verify(); + return overrideCGX; + } + + /** + * Set the current override CG to (x,0,0). + * + * @param x the x-coordinate of the override CG to set. + */ + public final void setOverrideCGX(double x) { + if (MathUtil.equals(overrideCGX, x)) + return; + checkState(); + this.overrideCGX = x; + if (isCGOverridden()) + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + else + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + /** + * Return whether the CG is currently overridden. + * + * @return whether the CG is overridden + */ + public final boolean isCGOverridden() { + mutex.verify(); + return cgOverriden; + } + + /** + * Set whether the CG is currently overridden. + * + * @param o whether the CG is overridden + */ + public final void setCGOverridden(boolean o) { + if (cgOverriden == o) { + return; + } + checkState(); + cgOverriden = o; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + /** + * Return whether the mass and/or CG override overrides all subcomponent values + * as well. The default implementation is a normal getter/setter implementation, + * however, subclasses are allowed to override this behavior if some subclass + * always or never overrides subcomponents. In this case the subclass should + * also override {@link #isOverrideSubcomponentsEnabled()} to return + * <code>false</code>. + * + * @return whether the current mass and/or CG override overrides subcomponents as well. + */ + public boolean getOverrideSubcomponents() { + mutex.verify(); + return overrideSubcomponents; + } + + + /** + * Set whether the mass and/or CG override overrides all subcomponent values + * as well. See {@link #getOverrideSubcomponents()} for details. + * + * @param override whether the mass and/or CG override overrides all subcomponent. + */ + public void setOverrideSubcomponents(boolean override) { + if (overrideSubcomponents == override) { + return; + } + checkState(); + overrideSubcomponents = override; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + /** + * Return whether the option to override all subcomponents is enabled or not. + * The default implementation returns <code>false</code> if neither mass nor + * CG is overridden, <code>true</code> otherwise. + * <p> + * This method may be overridden if the setting of overriding subcomponents + * cannot be set. + * + * @return whether the option to override subcomponents is currently enabled. + */ + public boolean isOverrideSubcomponentsEnabled() { + mutex.verify(); + return isCGOverridden() || isMassOverridden(); + } + + + + + /** + * Get the user-defined name of the component. + */ + public final String getName() { + mutex.verify(); + return name; + } + + /** + * Set the user-defined name of the component. If name==null, sets the name to + * the default name, currently the component name. + */ + public final void setName(String name) { + if (this.name.equals(name)) { + return; + } + checkState(); + if (name == null || name.matches("^\\s*$")) + this.name = getComponentName(); + else + this.name = name; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + /** + * Return the comment of the component. The component may contain multiple lines + * using \n as a newline separator. + * + * @return the comment of the component. + */ + public final String getComment() { + mutex.verify(); + return comment; + } + + /** + * Set the comment of the component. + * + * @param comment the comment of the component. + */ + public final void setComment(String comment) { + if (this.comment.equals(comment)) + return; + checkState(); + if (comment == null) + this.comment = ""; + else + this.comment = comment; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + + /** + * Return the preset component that this component is based upon. + * + * @return the preset component, or <code>null</code> if this is not based on a preset. + */ + public final ComponentPreset getPresetComponent() { + return presetComponent; + } + + /** + * Set the preset component this component is based upon and load all of the + * preset values. + * + * @param preset the preset component to load, or <code>null</code> to clear the preset. + */ + public final void loadPreset(ComponentPreset preset) { + if (presetComponent == preset) { + return; + } + + if (preset == null) { + clearPreset(); + return; + } + + if (preset.getComponentClass() != this.getClass()) { + throw new IllegalArgumentException("Attempting to load preset of type " + preset.getComponentClass() + + " into component of type " + this.getClass()); + } + + RocketComponent root = getRoot(); + final Rocket rocket; + if (root instanceof Rocket) { + rocket = (Rocket) root; + } else { + rocket = null; + } + + try { + if (rocket != null) { + rocket.freeze(); + } + + loadFromPreset(preset.getPrototype()); + + this.presetComponent = preset; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + + } finally { + if (rocket != null) { + rocket.thaw(); + } + } + } + + + /** + * Load component properties from the specified preset. The preset is guaranteed + * to be of the correct type. + * <p> + * This method should fire the appropriate events related to the changes. The rocket + * is frozen by the caller, so the events will be automatically combined. + * <p> + * This method must FIRST perform the preset loading and THEN call super.loadFromPreset(). + * This is because mass setting requires the dimensions to be set beforehand. + * + * @param preset the preset to load from + */ + protected void loadFromPreset(RocketComponent preset) { + // No-op + } + + + /** + * Clear the current component preset. This does not affect the component properties + * otherwise. + */ + public final void clearPreset() { + if (presetComponent == null) + return; + presetComponent = null; + fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); + } + + + + /** + * Returns the unique ID of the component. + * + * @return the ID of the component. + */ + public final String getID() { + return id; + } + + /** + * Generate a new ID for this component. + */ + private final void newID() { + mutex.verify(); + this.id = UniqueID.uuid(); + } + + + + + /** + * Get the characteristic length of the component, for example the length of a body tube + * of the length of the root chord of a fin. This is used in positioning the component + * relative to its parent. + * + * If the length of a component is settable, the class must define the setter method + * itself. + */ + public final double getLength() { + mutex.verify(); + return length; + } + + /** + * Get the positioning of the component relative to its parent component. + * This is one of the enums of {@link Position}. A setter method is not provided, + * but can be provided by a subclass. + */ + public final Position getRelativePosition() { + mutex.verify(); + return relativePosition; + } + + + /** + * Set the positioning of the component relative to its parent component. + * The actual position of the component is maintained to the best ability. + * <p> + * The default implementation is of protected visibility, since many components + * do not support setting the relative position. A component that does support + * it should override this with a public method that simply calls this + * supermethod AND fire a suitable ComponentChangeEvent. + * + * @param position the relative positioning. + */ + protected void setRelativePosition(RocketComponent.Position position) { + if (this.relativePosition == position) + return; + checkState(); + + // Update position so as not to move the component + if (this.parent != null) { + double thisPos = this.toRelative(Coordinate.NUL, this.parent)[0].x; + + switch (position) { + case ABSOLUTE: + this.position = this.toAbsolute(Coordinate.NUL)[0].x; + break; + + case TOP: + this.position = thisPos; + break; + + case MIDDLE: + this.position = thisPos - (this.parent.length - this.length) / 2; + break; + + case BOTTOM: + this.position = thisPos - (this.parent.length - this.length); + break; + + default: + throw new BugException("Unknown position type: " + position); + } + } + + this.relativePosition = position; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + /** + * Determine position relative to given position argument. Note: This is a side-effect free method. No state + * is modified. + * + * @param thePosition the relative position to be used as the basis for the computation + * @param relativeTo the position is computed relative the the given component + * + * @return double position of the component relative to the parent, with respect to <code>position</code> + */ + public double asPositionValue (Position thePosition, RocketComponent relativeTo) { + double result = this.position; + if (relativeTo != null) { + double thisPos = this.toRelative(Coordinate.NUL, relativeTo)[0].x; + + switch (thePosition) { + case ABSOLUTE: + result = this.toAbsolute(Coordinate.NUL)[0].x; + break; + case TOP: + result = thisPos; + break; + case MIDDLE: + result = thisPos - (relativeTo.length - this.length) / 2; + break; + case BOTTOM: + result = thisPos - (relativeTo.length - this.length); + break; + default: + throw new BugException("Unknown position type: " + thePosition); + } + } + return result; + } + + /** + * Get the position value of the component. The exact meaning of the value is + * dependent on the current relative positioning. + * + * @return the positional value. + */ + public final double getPositionValue() { + mutex.verify(); + return position; + } + + + /** + * Set the position value of the component. The exact meaning of the value + * depends on the current relative positioning. + * <p> + * The default implementation is of protected visibility, since many components + * do not support setting the relative position. A component that does support + * it should override this with a public method that simply calls this + * supermethod AND fire a suitable ComponentChangeEvent. + * + * @param value the position value of the component. + */ + public void setPositionValue(double value) { + if (MathUtil.equals(this.position, value)) + return; + checkState(); + this.position = value; + } + + + + /////////// Coordinate changes /////////// + + /** + * Returns coordinate c in absolute coordinates. Equivalent to toComponent(c,null). + */ + public Coordinate[] toAbsolute(Coordinate c) { + checkState(); + return toRelative(c, null); + } + + + /** + * Return coordinate <code>c</code> described in the coordinate system of + * <code>dest</code>. If <code>dest</code> is <code>null</code> returns + * absolute coordinates. + * <p> + * This method returns an array of coordinates, each of which represents a + * position of the coordinate in clustered cases. The array is guaranteed + * to contain at least one element. + * <p> + * The current implementation does not support rotating components. + * + * @param c Coordinate in the component's coordinate system. + * @param dest Destination component coordinate system. + * @return an array of coordinates describing <code>c</code> in coordinates + * relative to <code>dest</code>. + */ + public final Coordinate[] toRelative(Coordinate c, RocketComponent dest) { + checkState(); + mutex.lock("toRelative"); + try { + double absoluteX = Double.NaN; + RocketComponent search = dest; + Coordinate[] array = new Coordinate[1]; + array[0] = c; + + RocketComponent component = this; + while ((component != search) && (component.parent != null)) { + + array = component.shiftCoordinates(array); + + switch (component.relativePosition) { + case TOP: + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(component.position, 0, 0); + } + break; + + case MIDDLE: + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(component.position + + (component.parent.length - component.length) / 2, 0, 0); + } + break; + + case BOTTOM: + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(component.position + + (component.parent.length - component.length), 0, 0); + } + break; + + case AFTER: + // Add length of all previous brother-components with POSITION_RELATIVE_AFTER + int index = component.parent.children.indexOf(component); + assert (index >= 0); + for (index--; index >= 0; index--) { + RocketComponent comp = component.parent.children.get(index); + double componentLength = comp.getTotalLength(); + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(componentLength, 0, 0); + } + } + for (int i = 0; i < array.length; i++) { + array[i] = array[i].add(component.position + component.parent.length, 0, 0); + } + break; + + case ABSOLUTE: + search = null; // Requires back-search if dest!=null + if (Double.isNaN(absoluteX)) { + absoluteX = component.position; + } + break; + + default: + throw new BugException("Unknown relative positioning type of component" + + component + ": " + component.relativePosition); + } + + component = component.parent; // parent != null + } + + if (!Double.isNaN(absoluteX)) { + for (int i = 0; i < array.length; i++) { + array[i] = array[i].setX(absoluteX + c.x); + } + } + + // Check whether destination has been found or whether to backtrack + // TODO: LOW: Backtracking into clustered components uses only one component + if ((dest != null) && (component != dest)) { + Coordinate[] origin = dest.toAbsolute(Coordinate.NUL); + for (int i = 0; i < array.length; i++) { + array[i] = array[i].sub(origin[0]); + } + } + + return array; + } finally { + mutex.unlock("toRelative"); + } + } + + + /** + * Recursively sum the lengths of all subcomponents that have position + * Position.AFTER. + * + * @return Sum of the lengths. + */ + private final double getTotalLength() { + checkState(); + this.checkComponentStructure(); + mutex.lock("getTotalLength"); + try { + double l = 0; + if (relativePosition == Position.AFTER) + l = length; + for (int i = 0; i < children.size(); i++) + l += children.get(i).getTotalLength(); + return l; + } finally { + mutex.unlock("getTotalLength"); + } + } + + + + /////////// Total mass and CG calculation //////////// + + /** + * Return the (possibly overridden) mass of component. + * + * @return The mass of the component or the given override mass. + */ + public final double getMass() { + mutex.verify(); + if (massOverriden) + return overrideMass; + return getComponentMass(); + } + + /** + * Return the (possibly overridden) center of gravity and mass. + * + * Returns the CG with the weight of the coordinate set to the weight of the component. + * Both CG and mass may be separately overridden. + * + * @return The CG of the component or the given override CG. + */ + public final Coordinate getCG() { + checkState(); + if (cgOverriden) + return getOverrideCG().setWeight(getMass()); + + if (massOverriden) + return getComponentCG().setWeight(getMass()); + + return getComponentCG(); + } + + + /** + * Return the longitudinal (around the y- or z-axis) moment of inertia of this component. + * The moment of inertia is scaled in reference to the (possibly overridden) mass + * and is relative to the non-overridden CG. + * + * @return the longitudinal moment of inertia of this component. + */ + public final double getLongitudinalInertia() { + checkState(); + return getLongitudinalUnitInertia() * getMass(); + } + + /** + * Return the rotational (around the y- or z-axis) moment of inertia of this component. + * The moment of inertia is scaled in reference to the (possibly overridden) mass + * and is relative to the non-overridden CG. + * + * @return the rotational moment of inertia of this component. + */ + public final double getRotationalInertia() { + checkState(); + return getRotationalUnitInertia() * getMass(); + } + + + + /////////// Children handling /////////// + + + /** + * Adds a child to the rocket component tree. The component is added to the end + * of the component's child list. This is a helper method that calls + * {@link #addChild(RocketComponent,int)}. + * + * @param component The component to add. + * @throws IllegalArgumentException if the component is already part of some + * component tree. + * @see #addChild(RocketComponent,int) + */ + public final void addChild(RocketComponent component) { + checkState(); + addChild(component, children.size()); + } + + + /** + * Adds a child to the rocket component tree. The component is added to + * the given position of the component's child list. + * <p> + * This method may be overridden to enforce more strict component addition rules. + * The tests should be performed first and then this method called. + * + * @param component The component to add. + * @param index Position to add component to. + * @throws IllegalArgumentException If the component is already part of + * some component tree. + */ + public void addChild(RocketComponent component, int index) { + checkState(); + if (component.parent != null) { + throw new IllegalArgumentException("component " + component.getComponentName() + + " is already in a tree"); + } + if (!isCompatible(component)) { + throw new IllegalStateException("Component " + component.getComponentName() + + " not currently compatible with component " + getComponentName()); + } + + children.add(index, component); + component.parent = this; + + this.checkComponentStructure(); + component.checkComponentStructure(); + + fireAddRemoveEvent(component); + } + + + /** + * Removes a child from the rocket component tree. + * + * @param n remove the n'th child. + * @throws IndexOutOfBoundsException if n is out of bounds + */ + public final void removeChild(int n) { + checkState(); + RocketComponent component = children.remove(n); + component.parent = null; + + this.checkComponentStructure(); + component.checkComponentStructure(); + + fireAddRemoveEvent(component); + } + + /** + * Removes a child from the rocket component tree. Does nothing if the component + * is not present as a child. + * + * @param component the component to remove + * @return whether the component was a child + */ + public final boolean removeChild(RocketComponent component) { + checkState(); + + component.checkComponentStructure(); + + if (children.remove(component)) { + component.parent = null; + + this.checkComponentStructure(); + component.checkComponentStructure(); + + fireAddRemoveEvent(component); + return true; + } + return false; + } + + + + + /** + * Move a child to another position. + * + * @param component the component to move + * @param index the component's new position + * @throws IllegalArgumentException If an illegal placement was attempted. + */ + public final void moveChild(RocketComponent component, int index) { + checkState(); + if (children.remove(component)) { + children.add(index, component); + + this.checkComponentStructure(); + component.checkComponentStructure(); + + fireAddRemoveEvent(component); + } + } + + + /** + * Fires an AERODYNAMIC_CHANGE, MASS_CHANGE or OTHER_CHANGE event depending on the + * type of component removed. + */ + private void fireAddRemoveEvent(RocketComponent component) { + Iterator<RocketComponent> iter = component.iterator(true); + int type = ComponentChangeEvent.TREE_CHANGE; + while (iter.hasNext()) { + RocketComponent c = iter.next(); + if (c.isAerodynamic()) + type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; + if (c.isMassive()) + type |= ComponentChangeEvent.MASS_CHANGE; + } + + fireComponentChangeEvent(type); + } + + + public final int getChildCount() { + checkState(); + this.checkComponentStructure(); + return children.size(); + } + + public final RocketComponent getChild(int n) { + checkState(); + this.checkComponentStructure(); + return children.get(n); + } + + public final List<RocketComponent> getChildren() { + checkState(); + this.checkComponentStructure(); + return children.clone(); + } + + + /** + * Returns the position of the child in this components child list, or -1 if the + * component is not a child of this component. + * + * @param child The child to search for. + * @return Position in the list or -1 if not found. + */ + public final int getChildPosition(RocketComponent child) { + checkState(); + this.checkComponentStructure(); + return children.indexOf(child); + } + + /** + * Get the parent component of this component. Returns <code>null</code> if the component + * has no parent. + * + * @return The parent of this component or <code>null</code>. + */ + public final RocketComponent getParent() { + checkState(); + return parent; + } + + /** + * Get the root component of the component tree. + * + * @return The root component of the component tree. + */ + public final RocketComponent getRoot() { + checkState(); + RocketComponent gp = this; + while (gp.parent != null) + gp = gp.parent; + return gp; + } + + /** + * Returns the root Rocket component of this component tree. Throws an + * IllegalStateException if the root component is not a Rocket. + * + * @return The root Rocket component of the component tree. + * @throws IllegalStateException If the root component is not a Rocket. + */ + public final Rocket getRocket() { + checkState(); + RocketComponent r = getRoot(); + if (r instanceof Rocket) + return (Rocket) r; + throw new IllegalStateException("getRocket() called with root component " + + r.getComponentName()); + } + + + /** + * Return the Stage component that this component belongs to. Throws an + * IllegalStateException if a Stage is not in the parentage of this component. + * + * @return The Stage component this component belongs to. + * @throws IllegalStateException if a Stage component is not in the parentage. + */ + public final Stage getStage() { + checkState(); + RocketComponent c = this; + while (c != null) { + if (c instanceof Stage) + return (Stage) c; + c = c.getParent(); + } + throw new IllegalStateException("getStage() called without Stage as a parent."); + } + + /** + * Return the stage number of the stage this component belongs to. The stages + * are numbered from zero upwards. + * + * @return the stage number this component belongs to. + */ + public final int getStageNumber() { + checkState(); + if (parent == null) { + throw new IllegalArgumentException("getStageNumber() called for root component"); + } + + RocketComponent stage = this; + while (!(stage instanceof Stage)) { + stage = stage.parent; + if (stage == null || stage.parent == null) { + throw new IllegalStateException("getStageNumber() could not find parent " + + "stage."); + } + } + return stage.parent.getChildPosition(stage); + } + + + /** + * Find a component with the given ID. The component tree is searched from this component + * down (including this component) for the ID and the corresponding component is returned, + * or null if not found. + * + * @param idToFind ID to search for. + * @return The component with the ID, or null if not found. + */ + public final RocketComponent findComponent(String idToFind) { + checkState(); + Iterator<RocketComponent> iter = this.iterator(true); + while (iter.hasNext()) { + RocketComponent c = iter.next(); + if (c.getID().equals(idToFind)) + return c; + } + return null; + } + + + // TODO: Move these methods elsewhere (used only in SymmetricComponent) + public final RocketComponent getPreviousComponent() { + checkState(); + this.checkComponentStructure(); + if (parent == null) + return null; + int pos = parent.getChildPosition(this); + if (pos < 0) { + StringBuffer sb = new StringBuffer(); + sb.append("Inconsistent internal state: "); + sb.append("this=").append(this).append('[') + .append(System.identityHashCode(this)).append(']'); + sb.append(" parent.children=["); + for (int i = 0; i < parent.children.size(); i++) { + RocketComponent c = parent.children.get(i); + sb.append(c).append('[').append(System.identityHashCode(c)).append(']'); + if (i < parent.children.size() - 1) + sb.append(", "); + } + sb.append(']'); + throw new IllegalStateException(sb.toString()); + } + assert (pos >= 0); + if (pos == 0) + return parent; + RocketComponent c = parent.getChild(pos - 1); + while (c.getChildCount() > 0) + c = c.getChild(c.getChildCount() - 1); + return c; + } + + // TODO: Move these methods elsewhere (used only in SymmetricComponent) + public final RocketComponent getNextComponent() { + checkState(); + if (getChildCount() > 0) + return getChild(0); + + RocketComponent current = this; + RocketComponent nextParent = this.parent; + + while (nextParent != null) { + int pos = nextParent.getChildPosition(current); + if (pos < nextParent.getChildCount() - 1) + return nextParent.getChild(pos + 1); + + current = nextParent; + nextParent = current.parent; + } + return null; + } + + + /////////// Event handling ////////// + // + // Listener lists are provided by the root Rocket component, + // a single listener list for the whole rocket. + // + + /** + * Adds a ComponentChangeListener to the rocket tree. The listener is added to the root + * component, which must be of type Rocket (which overrides this method). Events of all + * subcomponents are sent to all listeners. + * + * @throws IllegalStateException - if the root component is not a Rocket + */ + public void addComponentChangeListener(ComponentChangeListener l) { + checkState(); + getRocket().addComponentChangeListener(l); + } + + /** + * Removes a ComponentChangeListener from the rocket tree. The listener is removed from + * the root component, which must be of type Rocket (which overrides this method). + * Does nothing if the root component is not a Rocket. (The asymmetry is so + * that listeners can always be removed just in case.) + * + * @param l Listener to remove + */ + public void removeComponentChangeListener(ComponentChangeListener l) { + if (parent != null) { + getRoot().removeComponentChangeListener(l); + } + } + + + /** + * Adds a <code>ChangeListener</code> to the rocket tree. This is identical to + * <code>addComponentChangeListener()</code> except that it uses a + * <code>ChangeListener</code>. The same events are dispatched to the + * <code>ChangeListener</code>, as <code>ComponentChangeEvent</code> is a subclass + * of <code>ChangeEvent</code>. + * + * @throws IllegalStateException - if the root component is not a <code>Rocket</code> + */ + @Override + public void addChangeListener(EventListener l) { + checkState(); + getRocket().addChangeListener(l); + } + + /** + * Removes a ChangeListener from the rocket tree. This is identical to + * removeComponentChangeListener() except it uses a ChangeListener. + * Does nothing if the root component is not a Rocket. (The asymmetry is so + * that listeners can always be removed just in case.) + * + * @param l Listener to remove + */ + @Override + public void removeChangeListener(EventListener l) { + if (this.parent != null) { + getRoot().removeChangeListener(l); + } + } + + + /** + * Fires a ComponentChangeEvent on the rocket structure. The call is passed to the + * root component, which must be of type Rocket (which overrides this method). + * Events of all subcomponents are sent to all listeners. + * + * If the component tree root is not a Rocket, the event is ignored. This is the + * case when constructing components not in any Rocket tree. In this case it + * would be impossible for the component to have listeners in any case. + * + * @param e Event to send + */ + protected void fireComponentChangeEvent(ComponentChangeEvent e) { + checkState(); + if (parent == null) { + /* Ignore if root invalid. */ + log.debug("Attempted firing event " + e + " with root " + this.getComponentName() + ", ignoring event"); + return; + } + getRoot().fireComponentChangeEvent(e); + } + + + /** + * Fires a ComponentChangeEvent of the given type. The source of the event is set to + * this component. + * + * @param type Type of event + * @see #fireComponentChangeEvent(ComponentChangeEvent) + */ + protected void fireComponentChangeEvent(int type) { + fireComponentChangeEvent(new ComponentChangeEvent(this, type)); + } + + + /** + * Checks whether this component has been invalidated and should no longer be used. + * This is a safety check that in-place replaced components are no longer used. + * All non-trivial methods (with the exception of methods simply getting a property) + * should call this method before changing or computing anything. + * + * @throws BugException if this component has been invalidated by {@link #copyFrom(RocketComponent)}. + */ + protected void checkState() { + invalidator.check(true); + mutex.verify(); + } + + + /** + * Check that the local component structure is correct. This can be called after changing + * the component structure in order to verify the integrity. + * <p> + * TODO: Remove this after the "inconsistent internal state" bug has been corrected + */ + public void checkComponentStructure() { + if (this.parent != null) { + // Test that this component is found in parent's children with == operator + if (!containsExact(this.parent.children, this)) { + throw new BugException("Inconsistent component structure detected, parent does not contain this " + + "component as a child, parent=" + parent.toDebugString() + " this=" + this.toDebugString()); + } + } + for (RocketComponent child : this.children) { + if (child.parent != this) { + throw new BugException("Inconsistent component structure detected, child does not have this component " + + "as the parent, this=" + this.toDebugString() + " child=" + child.toDebugString() + + " child.parent=" + (child.parent == null ? "null" : child.parent.toDebugString())); + } + } + } + + // Check whether the list contains exactly the searched-for component (with == operator) + private boolean containsExact(List<RocketComponent> haystack, RocketComponent needle) { + for (RocketComponent c : haystack) { + if (needle == c) { + return true; + } + } + return false; + } + + + /////////// Iterators ////////// + + /** + * Returns an iterator that iterates over all children and sub-children. + * <p> + * The iterator iterates through all children below this object, including itself if + * <code>returnSelf</code> is true. The order of the iteration is not specified + * (it may be specified in the future). + * <p> + * If an iterator iterating over only the direct children of the component is required, + * use <code>component.getChildren().iterator()</code>. + * + * TODO: HIGH: Remove this after merges have been done + * + * @param returnSelf boolean value specifying whether the component itself should be + * returned + * @return An iterator for the children and sub-children. + * @deprecated Use {@link #iterator(boolean)} instead + */ + @Deprecated + public final Iterator<RocketComponent> deepIterator(boolean returnSelf) { + return iterator(returnSelf); + } + + + /** + * Returns an iterator that iterates over all children and sub-children, including itself. + * <p> + * This method is equivalent to <code>deepIterator(true)</code>. + * + * TODO: HIGH: Remove this after merges have been done + * + * @return An iterator for this component, its children and sub-children. + * @deprecated Use {@link #iterator()} instead + */ + @Deprecated + public final Iterator<RocketComponent> deepIterator() { + return iterator(); + } + + + + /** + * Returns an iterator that iterates over all children and sub-children. + * <p> + * The iterator iterates through all children below this object, including itself if + * <code>returnSelf</code> is true. The order of the iteration is not specified + * (it may be specified in the future). + * <p> + * If an iterator iterating over only the direct children of the component is required, + * use <code>component.getChildren().iterator()</code>. + * + * @param returnSelf boolean value specifying whether the component itself should be + * returned + * @return An iterator for the children and sub-children. + */ + public final Iterator<RocketComponent> iterator(boolean returnSelf) { + checkState(); + return new RocketComponentIterator(this, returnSelf); + } + + + /** + * Returns an iterator that iterates over this component, its children and sub-children. + * <p> + * This method is equivalent to <code>iterator(true)</code>. + * + * @return An iterator for this component, its children and sub-children. + */ + @Override + public final Iterator<RocketComponent> iterator() { + return iterator(true); + } + + + + + + /** + * Compare component equality based on the ID of this component. Only the + * ID and class type is used for a basis of comparison. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (this.getClass() != obj.getClass()) + return false; + RocketComponent other = (RocketComponent) obj; + return this.id.equals(other.id); + } + + + + @Override + public int hashCode() { + return id.hashCode(); + } + + + + //////////// Helper methods for subclasses + + + + + /** + * Helper method to add rotationally symmetric bounds at the specified coordinates. + * The X-axis value is <code>x</code> and the radius at the specified position is + * <code>r</code>. + */ + protected static final void addBound(Collection<Coordinate> bounds, double x, double r) { + bounds.add(new Coordinate(x, -r, -r)); + bounds.add(new Coordinate(x, r, -r)); + bounds.add(new Coordinate(x, r, r)); + bounds.add(new Coordinate(x, -r, r)); + } + + + protected static final Coordinate ringCG(double outerRadius, double innerRadius, + double x1, double x2, double density) { + return new Coordinate((x1 + x2) / 2, 0, 0, + ringMass(outerRadius, innerRadius, x2 - x1, density)); + } + + protected static final double ringMass(double outerRadius, double innerRadius, + double length, double density) { + return Math.PI * (MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) * + length * density; + } + + protected static final double ringLongitudinalUnitInertia(double outerRadius, + double innerRadius, double length) { + // 1/12 * (3 * (r1^2 + r2^2) + h^2) + return (3 * (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) + MathUtil.pow2(length)) / 12; + } + + protected static final double ringRotationalUnitInertia(double outerRadius, + double innerRadius) { + // 1/2 * (r1^2 + r2^2) + return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) / 2; + } + + + + //////////// OTHER + + + /** + * Loads the RocketComponent fields from the given component. This method is meant + * for in-place replacement of a component. It is used with the undo/redo + * mechanism and when converting a finset into a freeform fin set. + * This component must not have a parent, otherwise this method will fail. + * <p> + * The child components in the source tree are copied into the current tree, however, + * the original components should not be used since they represent old copies of the + * components. It is recommended to invalidate them by calling {@link #invalidate()}. + * <p> + * This method returns a list of components that should be invalidated after references + * to them have been removed (for example by firing appropriate events). The list contains + * all children and sub-children of the current component and the entire component + * tree of <code>src</code>. + * + * @return a list of components that should not be used after this call. + */ + protected List<RocketComponent> copyFrom(RocketComponent src) { + checkState(); + List<RocketComponent> toInvalidate = new ArrayList<RocketComponent>(); + + if (this.parent != null) { + throw new UnsupportedOperationException("copyFrom called for non-root component, parent=" + + this.parent.toDebugString() + ", this=" + this.toDebugString()); + } + + // Add current structure to be invalidated + Iterator<RocketComponent> iterator = this.iterator(false); + while (iterator.hasNext()) { + toInvalidate.add(iterator.next()); + } + + // Remove previous components + for (RocketComponent child : this.children) { + child.parent = null; + } + this.children.clear(); + + // Copy new children to this component + for (RocketComponent c : src.children) { + RocketComponent copy = c.copyWithOriginalID(); + this.children.add(copy); + copy.parent = this; + } + + this.checkComponentStructure(); + src.checkComponentStructure(); + + // Set all parameters + this.length = src.length; + this.relativePosition = src.relativePosition; + this.position = src.position; + this.color = src.color; + this.lineStyle = src.lineStyle; + this.overrideMass = src.overrideMass; + this.massOverriden = src.massOverriden; + this.overrideCGX = src.overrideCGX; + this.cgOverriden = src.cgOverriden; + this.overrideSubcomponents = src.overrideSubcomponents; + this.name = src.name; + this.comment = src.comment; + this.id = src.id; + + // Add source components to invalidation tree + for (RocketComponent c : src) { + toInvalidate.add(c); + } + + return toInvalidate; + } + + protected void invalidate() { + invalidator.invalidate(); + } + + + ////////// Iterator implementation /////////// + + /** + * Private inner class to implement the Iterator. + * + * This iterator is fail-fast if the root of the structure is a Rocket. + */ + private static class RocketComponentIterator implements Iterator<RocketComponent> { + // Stack holds iterators which still have some components left. + private final Deque<Iterator<RocketComponent>> iteratorStack = new ArrayDeque<Iterator<RocketComponent>>(); + + private final Rocket root; + private final int treeModID; + + private final RocketComponent original; + private boolean returnSelf = false; + + // Construct iterator with component's child's iterator, if it has elements + public RocketComponentIterator(RocketComponent c, boolean returnSelf) { + + RocketComponent gp = c.getRoot(); + if (gp instanceof Rocket) { + root = (Rocket) gp; + treeModID = root.getTreeModID(); + } else { + root = null; + treeModID = -1; + } + + Iterator<RocketComponent> i = c.children.iterator(); + if (i.hasNext()) + iteratorStack.push(i); + + this.original = c; + this.returnSelf = returnSelf; + } + + @Override + public boolean hasNext() { + checkID(); + if (returnSelf) + return true; + return !iteratorStack.isEmpty(); // Elements remain if stack is not empty + } + + @Override + public RocketComponent next() { + Iterator<RocketComponent> i; + + checkID(); + + // Return original component first + if (returnSelf) { + returnSelf = false; + return original; + } + + // Peek first iterator from stack, throw exception if empty + i = iteratorStack.peek(); + if (i == null) { + throw new NoSuchElementException("No further elements in RocketComponent iterator"); + } + + // Retrieve next component of the iterator, remove iterator from stack if empty + RocketComponent c = i.next(); + if (!i.hasNext()) + iteratorStack.pop(); + + // Add iterator of component children to stack if it has children + i = c.children.iterator(); + if (i.hasNext()) + iteratorStack.push(i); + + return c; + } + + private void checkID() { + if (root != null) { + if (root.getTreeModID() != treeModID) { + throw new IllegalStateException("Rocket modified while being iterated"); + } + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove() not supported by " + + "RocketComponent iterator"); + } + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/RocketUtils.java b/core/src/net/sf/openrocket/rocketcomponent/RocketUtils.java new file mode 100644 index 00000000..2e4b63b8 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/RocketUtils.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.rocketcomponent; + +import java.util.Collection; + +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; +import net.sf.openrocket.util.Coordinate; + +public abstract class RocketUtils { + + public static double getLength(Rocket rocket) { + double length = 0; + Collection<Coordinate> bounds = rocket.getDefaultConfiguration().getBounds(); + if (!bounds.isEmpty()) { + double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; + for (Coordinate c : bounds) { + if (c.x < minX) + minX = c.x; + if (c.x > maxX) + maxX = c.x; + } + length = maxX - minX; + } + return length; + } + + public static Coordinate getCG(Rocket rocket) { + MassCalculator massCalculator = new BasicMassCalculator(); + Coordinate cg = massCalculator.getCG(rocket.getDefaultConfiguration(), MassCalcType.LAUNCH_MASS); + return cg; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ShockCord.java b/core/src/net/sf/openrocket/rocketcomponent/ShockCord.java new file mode 100644 index 00000000..8012a87e --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ShockCord.java @@ -0,0 +1,71 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; + +public class ShockCord extends MassObject { + private static final Translator trans = Application.getTranslator(); + + private Material material; + private double cordLength; + + public ShockCord() { + material = Application.getPreferences().getDefaultComponentMaterial(ShockCord.class, Material.Type.LINE); + cordLength = 0.4; + } + + + + public Material getMaterial() { + return material; + } + + public void setMaterial(Material m) { + if (m.getType() != Material.Type.LINE) + throw new BugException("Attempting to set non-linear material."); + if (material.equals(m)) + return; + this.material = m; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + public double getCordLength() { + return cordLength; + } + + public void setCordLength(double length) { + length = MathUtil.max(length, 0); + if (MathUtil.equals(length, this.length)) + return; + this.cordLength = length; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + @Override + public double getComponentMass() { + return material.getDensity() * cordLength; + } + + @Override + public String getComponentName() { + //// Shock cord + return trans.get("ShockCord.ShockCord"); + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class<? extends RocketComponent> type) { + return false; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Sleeve.java b/core/src/net/sf/openrocket/rocketcomponent/Sleeve.java new file mode 100644 index 00000000..ec244ee0 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Sleeve.java @@ -0,0 +1,106 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +/** + * A RingComponent that comes on top of another tube. It's defined by the inner + * radius and thickness. The inner radius can be automatic, in which case it + * takes the radius of the parent component. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Sleeve extends RingComponent { + + protected double innerRadius = 0; + protected double thickness = 0; + + + public Sleeve() { + super(); + setInnerRadiusAutomatic(true); + setThickness(0.001); + setLength(0.05); + } + + + @Override + public double getOuterRadius() { + return getInnerRadius() + thickness; + } + + @Override + public void setOuterRadius(double r) { + if (MathUtil.equals(getOuterRadius(), r)) + return; + + innerRadius = Math.max(r - thickness, 0); + if (thickness > r) + thickness = r; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + @Override + public double getInnerRadius() { + // Implement parent inner radius automation + if (isInnerRadiusAutomatic() && getParent() instanceof RadialParent) { + RocketComponent parent = getParent(); + double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x; + double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x; + pos1 = MathUtil.clamp(pos1, 0, parent.getLength()); + pos2 = MathUtil.clamp(pos2, 0, parent.getLength()); + innerRadius = Math.max(((RadialParent) parent).getOuterRadius(pos1), + ((RadialParent) parent).getOuterRadius(pos2)); + } + + return innerRadius; + } + + @Override + public void setInnerRadius(double r) { + r = Math.max(r, 0); + if (MathUtil.equals(innerRadius, r)) + return; + innerRadius = r; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + @Override + public double getThickness() { + return thickness; + } + + @Override + public void setThickness(double t) { + t = Math.max(t, 0); + if (MathUtil.equals(thickness, t)) + return; + thickness = t; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + @Override + public void setInnerRadiusAutomatic(boolean auto) { + super.setOuterRadiusAutomatic(auto); + } + + @Override + public String getComponentName() { + return "Sleeve"; + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class<? extends RocketComponent> type) { + return false; + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Stage.java b/core/src/net/sf/openrocket/rocketcomponent/Stage.java new file mode 100644 index 00000000..f5079bfe --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Stage.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +public class Stage extends ComponentAssembly { + + private static final Translator trans = Application.getTranslator(); + + @Override + public String getComponentName () { + //// Stage + return trans.get("Stage.Stage"); + } + + + @Override + public boolean allowsChildren() { + return true; + } + + /** + * Check whether the given type can be added to this component. A Stage allows + * only BodyComponents to be added. + * + * @param type The RocketComponent class type to add. + * + * @return Whether such a component can be added. + */ + @Override + public boolean isCompatible (Class<? extends RocketComponent> type) { + return BodyComponent.class.isAssignableFrom(type); + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Streamer.java b/core/src/net/sf/openrocket/rocketcomponent/Streamer.java new file mode 100644 index 00000000..8d73c457 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Streamer.java @@ -0,0 +1,110 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.util.MathUtil; + +public class Streamer extends RecoveryDevice { + + public static final double DEFAULT_CD = 0.6; + + public static final double MAX_COMPUTED_CD = 0.4; + + + private double stripLength; + private double stripWidth; + + + public Streamer() { + this.stripLength = 0.5; + this.stripWidth = 0.05; + } + + + public double getStripLength() { + return stripLength; + } + + public void setStripLength(double stripLength) { + if (MathUtil.equals(this.stripLength, stripLength)) + return; + this.stripLength = stripLength; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public double getStripWidth() { + return stripWidth; + } + + public void setStripWidth(double stripWidth) { + if (MathUtil.equals(this.stripWidth, stripWidth)) + return; + this.stripWidth = stripWidth; + this.length = stripWidth; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + @Override + public void setLength(double length) { + setStripWidth(length); + } + + + public double getAspectRatio() { + if (stripWidth > 0.0001) + return stripLength / stripWidth; + return 1000; + } + + public void setAspectRatio(double ratio) { + if (MathUtil.equals(getAspectRatio(), ratio)) + return; + + ratio = Math.max(ratio, 0.01); + double area = getArea(); + stripWidth = MathUtil.safeSqrt(area / ratio); + stripLength = ratio * stripWidth; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + @Override + public double getArea() { + return stripWidth * stripLength; + } + + public void setArea(double area) { + if (MathUtil.equals(getArea(), area)) + return; + + double ratio = Math.max(getAspectRatio(), 0.01); + stripWidth = MathUtil.safeSqrt(area / ratio); + stripLength = ratio * stripWidth; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + @Override + public double getComponentCD(double mach) { + double density = this.getMaterial().getDensity(); + double cd; + + cd = 0.034 * ((density + 0.025) / 0.105) * (stripLength + 1) / stripLength; + cd = MathUtil.min(cd, MAX_COMPUTED_CD); + return cd; + } + + @Override + public String getComponentName() { + return "Streamer"; + } + + @Override + public boolean allowsChildren() { + return false; + } + + @Override + public boolean isCompatible(Class<? extends RocketComponent> type) { + return false; + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java b/core/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java new file mode 100644 index 00000000..d37af27e --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.material.Material; +import net.sf.openrocket.startup.Application; + +public abstract class StructuralComponent extends InternalComponent { + + private Material material; + + public StructuralComponent() { + super(); + material = Application.getPreferences().getDefaultComponentMaterial(this.getClass(), Material.Type.BULK); + } + + + public final Material getMaterial() { + return material; + } + + public final void setMaterial(Material mat) { + if (mat.getType() != Material.Type.BULK) { + throw new IllegalArgumentException("Attempted to set non-bulk material "+mat); + } + if (mat.equals(material)) + return; + this.material = mat; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java b/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java new file mode 100644 index 00000000..5dc18354 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java @@ -0,0 +1,577 @@ +package net.sf.openrocket.rocketcomponent; + +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +/** + * Class for an axially symmetric rocket component generated by rotating + * a function y=f(x) >= 0 around the x-axis (eg. tube, cone, etc.) + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + +public abstract class SymmetricComponent extends BodyComponent implements RadialParent { + public static final double DEFAULT_RADIUS = 0.025; + public static final double DEFAULT_THICKNESS = 0.002; + + private static final int DIVISIONS = 100; // No. of divisions when integrating + + protected boolean filled = false; + protected double thickness = DEFAULT_THICKNESS; + + + // Cached data, default values signify not calculated + private double wetArea = -1; + private double planArea = -1; + private double planCenter = -1; + private double volume = -1; + private double fullVolume = -1; + private double longitudinalInertia = -1; + private double rotationalInertia = -1; + private Coordinate cg = null; + + + + public SymmetricComponent() { + super(); + } + + + /** + * Return the component radius at position x. + * @param x Position on x-axis. + * @return Radius of the component at the given position, or 0 if outside + * the component. + */ + public abstract double getRadius(double x); + + @Override + public abstract double getInnerRadius(double x); + + public abstract double getForeRadius(); + + public abstract boolean isForeRadiusAutomatic(); + + public abstract double getAftRadius(); + + public abstract boolean isAftRadiusAutomatic(); + + + // Implement the Radial interface: + @Override + public final double getOuterRadius(double x) { + return getRadius(x); + } + + + @Override + public final double getRadius(double x, double theta) { + return getRadius(x); + } + + @Override + public final double getInnerRadius(double x, double theta) { + return getInnerRadius(x); + } + + + + /** + * Return the component wall thickness. + */ + public double getThickness() { + if (filled) + return Math.max(getForeRadius(), getAftRadius()); + return Math.min(thickness, Math.max(getForeRadius(), getAftRadius())); + } + + + /** + * Set the component wall thickness. Values greater than the maximum radius are not + * allowed, and will result in setting the thickness to the maximum radius. + */ + public void setThickness(double thickness) { + if ((this.thickness == thickness) && !filled) + return; + this.thickness = MathUtil.clamp(thickness, 0, Math.max(getForeRadius(), getAftRadius())); + filled = false; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + clearPreset(); + } + + + /** + * Returns whether the component is set as filled. If it is set filled, then the + * wall thickness will have no effect. + */ + public boolean isFilled() { + return filled; + } + + + /** + * Sets whether the component is set as filled. If the component is filled, then + * the wall thickness will have no effect. + */ + public void setFilled(boolean filled) { + if (this.filled == filled) + return; + this.filled = filled; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + clearPreset(); + } + + + /** + * Adds component bounds at a number of points between 0...length. + */ + @Override + public Collection<Coordinate> getComponentBounds() { + List<Coordinate> list = new ArrayList<Coordinate>(20); + for (int n = 0; n <= 5; n++) { + double x = n * length / 5; + double r = getRadius(x); + addBound(list, x, r); + } + return list; + } + + + + @Override + protected void loadFromPreset(RocketComponent preset) { + SymmetricComponent c = (SymmetricComponent) preset; + this.setThickness(c.getThickness()); + this.setFilled(c.isFilled()); + + super.loadFromPreset(preset); + } + + + + + /** + * Calculate volume of the component by integrating over the length of the component. + * The method caches the result, so subsequent calls are instant. Subclasses may + * override this method for simple shapes and use this method as necessary. + * + * @return The volume of the component. + */ + @Override + public double getComponentVolume() { + if (volume < 0) + integrate(); + return volume; + } + + + /** + * Calculate full (filled) volume of the component by integrating over the length + * of the component. The method caches the result, so subsequent calls are instant. + * Subclasses may override this method for simple shapes and use this method as + * necessary. + * + * @return The filled volume of the component. + */ + public double getFullVolume() { + if (fullVolume < 0) + integrate(); + return fullVolume; + } + + + /** + * Calculate the wetted area of the component by integrating over the length + * of the component. The method caches the result, so subsequent calls are instant. + * Subclasses may override this method for simple shapes and use this method as + * necessary. + * + * @return The wetted area of the component. + */ + public double getComponentWetArea() { + if (wetArea < 0) + integrate(); + return wetArea; + } + + + /** + * Calculate the planform area of the component by integrating over the length of + * the component. The method caches the result, so subsequent calls are instant. + * Subclasses may override this method for simple shapes and use this method as + * necessary. + * + * @return The planform area of the component. + */ + public double getComponentPlanformArea() { + if (planArea < 0) + integrate(); + return planArea; + } + + + /** + * Calculate the planform center X-coordinate of the component by integrating over + * the length of the component. The planform center is defined as + * <pre> integrate(x*2*r(x)) / planform area </pre> + * The method caches the result, so subsequent calls are instant. Subclasses may + * override this method for simple shapes and use this method as necessary. + * + * @return The planform center of the component. + */ + public double getComponentPlanformCenter() { + if (planCenter < 0) + integrate(); + return planCenter; + } + + + /** + * Calculate CG of the component by integrating over the length of the component. + * The method caches the result, so subsequent calls are instant. Subclasses may + * override this method for simple shapes and use this method as necessary. + * + * @return The CG+mass of the component. + */ + @Override + public Coordinate getComponentCG() { + if (cg == null) + integrate(); + return cg; + } + + + @Override + public double getLongitudinalUnitInertia() { + if (longitudinalInertia < 0) { + if (getComponentVolume() > 0.0000001) // == 0.1cm^3 + integrateInertiaVolume(); + else + integrateInertiaSurface(); + } + return longitudinalInertia; + } + + + @Override + public double getRotationalUnitInertia() { + if (rotationalInertia < 0) { + if (getComponentVolume() > 0.0000001) + integrateInertiaVolume(); + else + integrateInertiaSurface(); + } + return rotationalInertia; + } + + + + /** + * Performs integration over the length of the component and updates the cached variables. + */ + private void integrate() { + double x, r1, r2; + double cgx; + + // Check length > 0 + if (length <= 0) { + wetArea = 0; + planArea = 0; + planCenter = 0; + volume = 0; + cg = Coordinate.NUL; + return; + } + + + // Integrate for volume, CG, wetted area and planform area + + final double l = length / DIVISIONS; + final double pil = Math.PI * l; // PI * l + final double pil3 = Math.PI * l / 3; // PI * l/3 + r1 = getRadius(0); + x = 0; + wetArea = 0; + planArea = 0; + planCenter = 0; + fullVolume = 0; + volume = 0; + cgx = 0; + + for (int n = 1; n <= DIVISIONS; n++) { + /* + * r1 and r2 are the two radii + * x is the position of r1 + * hyp is the length of the hypotenuse from r1 to r2 + * height if the y-axis height of the component if not filled + */ + + r2 = getRadius(x + l); + final double hyp = MathUtil.hypot(r2 - r1, l); + + + // Volume differential elements + final double dV; + final double dFullV; + + dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2); + if (filled || r1 < thickness || r2 < thickness) { + // Filled piece + dV = dFullV; + } else { + // Hollow piece + final double height = thickness * hyp / l; + dV = MathUtil.max(pil * height * (r1 + r2 - height), 0); + } + + // Add to the volume-related components + volume += dV; + fullVolume += dFullV; + cgx += (x + l / 2) * dV; + + // Wetted area ( * PI at the end) + wetArea += hyp * (r1 + r2); + + // Planform area & center + final double p = l * (r1 + r2); + planArea += p; + planCenter += (x + l / 2) * p; + + // Update for next iteration + r1 = r2; + x += l; + } + + wetArea *= Math.PI; + + if (planArea > 0) + planCenter /= planArea; + + if (volume < 0.0000000001) { // 0.1 mm^3 + volume = 0; + cg = new Coordinate(length / 2, 0, 0, 0); + } else { + // getComponentMass is safe now + // Use super.getComponentMass() to ensure only the transition shape mass + // is used, not the shoulders + cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass()); + } + } + + + /** + * Integrate the longitudinal and rotational inertia based on component volume. + * This method may be used only if the total volume is zero. + */ + private void integrateInertiaVolume() { + double x, r1, r2; + + final double l = length / DIVISIONS; + final double pil = Math.PI * l; // PI * l + final double pil3 = Math.PI * l / 3; // PI * l/3 + + r1 = getRadius(0); + x = 0; + longitudinalInertia = 0; + rotationalInertia = 0; + + double vol = 0; + + for (int n = 1; n <= DIVISIONS; n++) { + /* + * r1 and r2 are the two radii, outer is their average + * x is the position of r1 + * hyp is the length of the hypotenuse from r1 to r2 + * height if the y-axis height of the component if not filled + */ + r2 = getRadius(x + l); + final double outer = (r1 + r2) / 2; + + + // Volume differential elements + final double inner; + final double dV; + + if (filled || r1 < thickness || r2 < thickness) { + inner = 0; + dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2); + } else { + final double hyp = MathUtil.hypot(r2 - r1, l); + final double height = thickness * hyp / l; + dV = pil * height * (r1 + r2 - height); + inner = Math.max(outer - height, 0); + } + + rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2; + longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12 + + pow2(x + l / 2)); + + vol += dV; + + // Update for next iteration + r1 = r2; + x += l; + } + + if (MathUtil.equals(vol, 0)) { + integrateInertiaSurface(); + return; + } + + rotationalInertia /= vol; + longitudinalInertia /= vol; + + // Shift longitudinal inertia to CG + longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0); + } + + + + /** + * Integrate the longitudinal and rotational inertia based on component surface area. + * This method may be used only if the total volume is zero. + */ + private void integrateInertiaSurface() { + double x, r1, r2; + + final double l = length / DIVISIONS; + + r1 = getRadius(0); + System.out.println(r1); + x = 0; + + longitudinalInertia = 0; + rotationalInertia = 0; + + double surface = 0; + + for (int n = 1; n <= DIVISIONS; n++) { + /* + * r1 and r2 are the two radii, outer is their average + * x is the position of r1 + * hyp is the length of the hypotenuse from r1 to r2 + * height if the y-axis height of the component if not filled + */ + r2 = getRadius(x + l); + final double hyp = MathUtil.hypot(r2 - r1, l); + final double outer = (r1 + r2) / 2; + + final double dS = hyp * (r1 + r2) * Math.PI; + + rotationalInertia += dS * pow2(outer); + longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2)); + + surface += dS; + + // Update for next iteration + r1 = r2; + x += l; + } + + if (MathUtil.equals(surface, 0)) { + longitudinalInertia = 0; + rotationalInertia = 0; + return; + } + + longitudinalInertia /= surface; + rotationalInertia /= surface; + + // Shift longitudinal inertia to CG + longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0); + } + + + + + /** + * Invalidates the cached volume and CG information. + */ + @Override + protected void componentChanged(ComponentChangeEvent e) { + super.componentChanged(e); + if (!e.isOtherChange()) { + wetArea = -1; + planArea = -1; + planCenter = -1; + volume = -1; + fullVolume = -1; + longitudinalInertia = -1; + rotationalInertia = -1; + cg = null; + } + } + + + + /////////// Auto radius helper methods + + + /** + * Returns the automatic radius for this component towards the + * front of the rocket. The automatics will not search towards the + * rear of the rocket for a suitable radius. A positive return value + * indicates a preferred radius, a negative value indicates that a + * match was not found. + */ + protected abstract double getFrontAutoRadius(); + + /** + * Returns the automatic radius for this component towards the + * end of the rocket. The automatics will not search towards the + * front of the rocket for a suitable radius. A positive return value + * indicates a preferred radius, a negative value indicates that a + * match was not found. + */ + protected abstract double getRearAutoRadius(); + + + + /** + * Return the previous symmetric component, or null if none exists. + * NOTE: This method currently assumes that there are no external + * "pods". + * + * @return the previous SymmetricComponent, or null. + */ + protected final SymmetricComponent getPreviousSymmetricComponent() { + RocketComponent c; + for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) { + if (c instanceof SymmetricComponent) { + return (SymmetricComponent) c; + } + if (!(c instanceof Stage) && + (c.relativePosition == RocketComponent.Position.AFTER)) + return null; // Bad component type as "parent" + } + return null; + } + + /** + * Return the next symmetric component, or null if none exists. + * NOTE: This method currently assumes that there are no external + * "pods". + * + * @return the next SymmetricComponent, or null. + */ + protected final SymmetricComponent getNextSymmetricComponent() { + RocketComponent c; + for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) { + if (c instanceof SymmetricComponent) { + return (SymmetricComponent) c; + } + if (!(c instanceof Stage) && + (c.relativePosition == RocketComponent.Position.AFTER)) + return null; // Bad component type as "parent" + } + return null; + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java b/core/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java new file mode 100644 index 00000000..4bded707 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java @@ -0,0 +1,82 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +/** + * An inner component that consists of a hollow cylindrical component. This can be + * an inner tube, tube coupler, centering ring, bulkhead etc. + * + * The properties include the inner and outer radii, length and radial position. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public abstract class ThicknessRingComponent extends RingComponent { + + protected double outerRadius = 0; + protected double thickness = 0; + + + @Override + public double getOuterRadius() { + if (isOuterRadiusAutomatic() && getParent() instanceof RadialParent) { + RocketComponent parent = getParent(); + double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x; + double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x; + pos1 = MathUtil.clamp(pos1, 0, parent.getLength()); + pos2 = MathUtil.clamp(pos2, 0, parent.getLength()); + outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1), + ((RadialParent)parent).getInnerRadius(pos2)); + } + + return outerRadius; + } + + + @Override + public void setOuterRadius(double r) { + r = Math.max(r,0); + if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic()) + return; + + outerRadius = r; + outerRadiusAutomatic = false; + + if (thickness > outerRadius) + thickness = outerRadius; + + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + @Override + public double getThickness() { + return Math.min(thickness, getOuterRadius()); + } + @Override + public void setThickness(double thickness) { + double outer = getOuterRadius(); + + thickness = MathUtil.clamp(thickness, 0, outer); + if (MathUtil.equals(getThickness(), thickness)) + return; + + this.thickness = thickness; + + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + @Override + public double getInnerRadius() { + return Math.max(getOuterRadius()-thickness, 0); + } + @Override + public void setInnerRadius(double r) { + r = Math.max(r,0); + setThickness(getOuterRadius() - r); + } + + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Transition.java b/core/src/net/sf/openrocket/rocketcomponent/Transition.java new file mode 100644 index 00000000..f3a6f9b6 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Transition.java @@ -0,0 +1,841 @@ +package net.sf.openrocket.rocketcomponent; + +import static java.lang.Math.sin; +import static net.sf.openrocket.util.MathUtil.*; + +import java.util.Collection; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + + +public class Transition extends SymmetricComponent { + private static final Translator trans = Application.getTranslator(); + private static final double CLIP_PRECISION = 0.0001; + + + private Shape type; + private double shapeParameter; + private boolean clipped; // Not to be read - use isClipped(), which may be overriden + + private double radius1, radius2; + private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic + + + private double foreShoulderRadius; + private double foreShoulderThickness; + private double foreShoulderLength; + private boolean foreShoulderCapped; + private double aftShoulderRadius; + private double aftShoulderThickness; + private double aftShoulderLength; + private boolean aftShoulderCapped; + + + // Used to cache the clip length + private double clipLength = -1; + + public Transition() { + super(); + + this.radius1 = DEFAULT_RADIUS; + this.radius2 = DEFAULT_RADIUS; + this.length = DEFAULT_RADIUS * 3; + this.autoRadius1 = true; + this.autoRadius2 = true; + + this.type = Shape.CONICAL; + this.shapeParameter = 0; + this.clipped = true; + } + + + + + //////// Fore radius //////// + + + @Override + public double getForeRadius() { + if (isForeRadiusAutomatic()) { + // Get the automatic radius from the front + double r = -1; + SymmetricComponent c = this.getPreviousSymmetricComponent(); + if (c != null) { + r = c.getFrontAutoRadius(); + } + if (r < 0) + r = DEFAULT_RADIUS; + return r; + } + return radius1; + } + + public void setForeRadius(double radius) { + if ((this.radius1 == radius) && (autoRadius1 == false)) + return; + + this.autoRadius1 = false; + this.radius1 = Math.max(radius, 0); + + if (this.thickness > this.radius1 && this.thickness > this.radius2) + this.thickness = Math.max(this.radius1, this.radius2); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + @Override + public boolean isForeRadiusAutomatic() { + return autoRadius1; + } + + public void setForeRadiusAutomatic(boolean auto) { + if (autoRadius1 == auto) + return; + + autoRadius1 = auto; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + //////// Aft radius ///////// + + @Override + public double getAftRadius() { + if (isAftRadiusAutomatic()) { + // Return the auto radius from the rear + double r = -1; + SymmetricComponent c = this.getNextSymmetricComponent(); + if (c != null) { + r = c.getRearAutoRadius(); + } + if (r < 0) + r = DEFAULT_RADIUS; + return r; + } + return radius2; + } + + + + public void setAftRadius(double radius) { + if ((this.radius2 == radius) && (autoRadius2 == false)) + return; + + this.autoRadius2 = false; + this.radius2 = Math.max(radius, 0); + + if (this.thickness > this.radius1 && this.thickness > this.radius2) + this.thickness = Math.max(this.radius1, this.radius2); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + @Override + public boolean isAftRadiusAutomatic() { + return autoRadius2; + } + + public void setAftRadiusAutomatic(boolean auto) { + if (autoRadius2 == auto) + return; + + autoRadius2 = auto; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + //// Radius automatics + + @Override + protected double getFrontAutoRadius() { + if (isAftRadiusAutomatic()) + return -1; + return getAftRadius(); + } + + + @Override + protected double getRearAutoRadius() { + if (isForeRadiusAutomatic()) + return -1; + return getForeRadius(); + } + + + + + //////// Type & shape ///////// + + public Shape getType() { + return type; + } + + public void setType(Shape type) { + if (type == null) { + throw new IllegalArgumentException("setType called with null argument"); + } + if (this.type == type) + return; + this.type = type; + this.clipped = type.isClippable(); + this.shapeParameter = type.defaultParameter(); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public double getShapeParameter() { + return shapeParameter; + } + + public void setShapeParameter(double n) { + if (shapeParameter == n) + return; + this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter()); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public boolean isClipped() { + if (!type.isClippable()) + return false; + return clipped; + } + + public void setClipped(boolean c) { + if (clipped == c) + return; + clipped = c; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public boolean isClippedEnabled() { + return type.isClippable(); + } + + public double getShapeParameterMin() { + return type.minParameter(); + } + + public double getShapeParameterMax() { + return type.maxParameter(); + } + + + //////// Shoulders //////// + + public double getForeShoulderRadius() { + return foreShoulderRadius; + } + + public void setForeShoulderRadius(double foreShoulderRadius) { + if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius)) + return; + this.foreShoulderRadius = foreShoulderRadius; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public double getForeShoulderThickness() { + return foreShoulderThickness; + } + + public void setForeShoulderThickness(double foreShoulderThickness) { + if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness)) + return; + this.foreShoulderThickness = foreShoulderThickness; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public double getForeShoulderLength() { + return foreShoulderLength; + } + + public void setForeShoulderLength(double foreShoulderLength) { + if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength)) + return; + this.foreShoulderLength = foreShoulderLength; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public boolean isForeShoulderCapped() { + return foreShoulderCapped; + } + + public void setForeShoulderCapped(boolean capped) { + if (this.foreShoulderCapped == capped) + return; + this.foreShoulderCapped = capped; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + public double getAftShoulderRadius() { + return aftShoulderRadius; + } + + public void setAftShoulderRadius(double aftShoulderRadius) { + if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius)) + return; + this.aftShoulderRadius = aftShoulderRadius; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public double getAftShoulderThickness() { + return aftShoulderThickness; + } + + public void setAftShoulderThickness(double aftShoulderThickness) { + if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness)) + return; + this.aftShoulderThickness = aftShoulderThickness; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public double getAftShoulderLength() { + return aftShoulderLength; + } + + public void setAftShoulderLength(double aftShoulderLength) { + if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength)) + return; + this.aftShoulderLength = aftShoulderLength; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + public boolean isAftShoulderCapped() { + return aftShoulderCapped; + } + + public void setAftShoulderCapped(boolean capped) { + if (this.aftShoulderCapped == capped) + return; + this.aftShoulderCapped = capped; + fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); + } + + + + + /////////// Shape implementations //////////// + + + + /** + * Return the radius at point x of the transition. + */ + @Override + public double getRadius(double x) { + if (x < 0 || x > length) + return 0; + + double r1 = getForeRadius(); + double r2 = getAftRadius(); + + if (r1 == r2) + return r1; + + if (r1 > r2) { + x = length - x; + double tmp = r1; + r1 = r2; + r2 = tmp; + } + + if (isClipped()) { + // Check clip calculation + if (clipLength < 0) + calculateClip(r1, r2); + return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter); + } else { + // Not clipped + return r1 + type.getRadius(x, r2 - r1, length, shapeParameter); + } + } + + /** + * Numerically solve clipLength from the equation + * r1 == type.getRadius(clipLength,r2,clipLength+length) + * using a binary search. It assumes getOuterRadius() to be monotonically increasing. + */ + private void calculateClip(double r1, double r2) { + double min = 0, max = length; + + if (r1 >= r2) { + double tmp = r1; + r1 = r2; + r2 = tmp; + } + + if (r1 == 0) { + clipLength = 0; + return; + } + + if (length <= 0) { + clipLength = 0; + return; + } + + // Required: + // getR(min,min+length,r2) - r1 < 0 + // getR(max,max+length,r2) - r1 > 0 + + int n = 0; + while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) { + min = max; + max *= 2; + n++; + if (n > 10) + break; + } + + while (true) { + clipLength = (min + max) / 2; + if ((max - min) < CLIP_PRECISION) + return; + double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter); + if (val - r1 > 0) { + max = clipLength; + } else { + min = clipLength; + } + } + } + + + @Override + public double getInnerRadius(double x) { + return Math.max(getRadius(x) - thickness, 0); + } + + + + @Override + public Collection<Coordinate> getComponentBounds() { + Collection<Coordinate> bounds = super.getComponentBounds(); + if (foreShoulderLength > 0.001) + addBound(bounds, -foreShoulderLength, foreShoulderRadius); + if (aftShoulderLength > 0.001) + addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius); + return bounds; + } + + @Override + public double getComponentMass() { + double mass = super.getComponentMass(); + if (getForeShoulderLength() > 0.001) { + final double or = getForeShoulderRadius(); + final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); + mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity()); + } + if (isForeShoulderCapped()) { + final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); + mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity()); + } + + if (getAftShoulderLength() > 0.001) { + final double or = getAftShoulderRadius(); + final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); + mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity()); + } + if (isAftShoulderCapped()) { + final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); + mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity()); + } + + return mass; + } + + @Override + public Coordinate getComponentCG() { + Coordinate cg = super.getComponentCG(); + if (getForeShoulderLength() > 0.001) { + final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); + cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0, + getMaterial().getDensity())); + } + if (isForeShoulderCapped()) { + final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); + cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(), + getForeShoulderThickness() - getForeShoulderLength(), + getMaterial().getDensity())); + } + + if (getAftShoulderLength() > 0.001) { + final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); + cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(), + getLength() + getAftShoulderLength(), getMaterial().getDensity())); + } + if (isAftShoulderCapped()) { + final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); + cg = cg.average(ringCG(ir, 0, + getLength() + getAftShoulderLength() - getAftShoulderThickness(), + getLength() + getAftShoulderLength(), getMaterial().getDensity())); + } + return cg; + } + + + /* + * The moments of inertia are not explicitly corrected for the shoulders. + * However, since the mass is corrected, the inertia is automatically corrected + * to very nearly the correct value. + */ + + + + /** + * Returns the name of the component ("Transition"). + */ + @Override + public String getComponentName() { + //// Transition + return trans.get("Transition.Transition"); + } + + @Override + protected void componentChanged(ComponentChangeEvent e) { + super.componentChanged(e); + clipLength = -1; + } + + /** + * Check whether the given type can be added to this component. Transitions allow any + * InternalComponents to be added. + * + * @param ctype The RocketComponent class type to add. + * @return Whether such a component can be added. + */ + @Override + public boolean isCompatible(Class<? extends RocketComponent> ctype) { + if (InternalComponent.class.isAssignableFrom(ctype)) + return true; + return false; + } + + + + /** + * An enumeration listing the possible shapes of transitions. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + public static enum Shape { + + /** + * Conical shape. + */ + //// Conical + CONICAL(trans.get("Shape.Conical"), + //// A conical nose cone has a profile of a triangle. + trans.get("Shape.Conical.desc1"), + //// A conical transition has straight sides. + trans.get("Shape.Conical.desc2")) { + @Override + public double getRadius(double x, double radius, double length, double param) { + assert x >= 0; + assert x <= length; + assert radius >= 0; + return radius * x / length; + } + }, + + /** + * Ogive shape. The shape parameter is the portion of an extended tangent ogive + * that will be used. That is, for param==1 a tangent ogive will be produced, and + * for smaller values the shape straightens out into a cone at param==0. + */ + //// Ogive + OGIVE(trans.get("Shape.Ogive"), + //// An ogive nose cone has a profile that is a segment of a circle. The shape parameter value 1 produces a <b>tangent ogive</b>, which has a smooth transition to the body tube, values less than 1 produce <b>secant ogives</b>. + trans.get("Shape.Ogive.desc1"), + //// An ogive transition has a profile that is a segment of a circle. The shape parameter value 1 produces a <b>tangent ogive</b>, which has a smooth transition to the body tube at the aft end, values less than 1 produce <b>secant ogives</b>. + trans.get("Shape.Ogive.desc2")) { + @Override + public boolean usesParameter() { + return true; // Range 0...1 is default + } + + @Override + public double defaultParameter() { + return 1.0; // Tangent ogive by default + } + + @Override + public double getRadius(double x, double radius, double length, double param) { + assert x >= 0; + assert x <= length; + assert radius >= 0; + assert param >= 0; + assert param <= 1; + + // Impossible to calculate ogive for length < radius, scale instead + // TODO: LOW: secant ogive could be calculated lower + if (length < radius) { + x = x * radius / length; + length = radius; + } + + if (param < 0.001) + return CONICAL.getRadius(x, radius, length, param); + + // Radius of circle is: + double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) * + (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius))); + double L = length / param; + // double R = (radius + length*length/(radius*param*param))/2; + double y0 = MathUtil.safeSqrt(R * R - L * L); + return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0; + } + }, + + /** + * Ellipsoidal shape. + */ + //// Ellipsoid + ELLIPSOID(trans.get("Shape.Ellipsoid"), + //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. + trans.get("Shape.Ellipsoid.desc1"), + //// An ellipsoidal transition has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the transition is not clipped, then the profile is extended at the center by the corresponding radius. + trans.get("Shape.Ellipsoid.desc2"), true) { + @Override + public double getRadius(double x, double radius, double length, double param) { + assert x >= 0; + assert x <= length; + assert radius >= 0; + x = x * radius / length; + return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere + } + }, + + //// Power series + POWER(trans.get("Shape.Powerseries"), + trans.get("Shape.Powerseries.desc1"), + trans.get("Shape.Powerseries.desc2"), true) { + @Override + public boolean usesParameter() { // Range 0...1 + return true; + } + + @Override + public double defaultParameter() { + return 0.5; + } + + @Override + public double getRadius(double x, double radius, double length, double param) { + assert x >= 0; + assert x <= length; + assert radius >= 0; + assert param >= 0; + assert param <= 1; + if (param <= 0.00001) { + if (x <= 0.00001) + return 0; + else + return radius; + } + return radius * Math.pow(x / length, param); + } + + }, + + //// Parabolic series + PARABOLIC(trans.get("Shape.Parabolicseries"), + ////A parabolic series nose cone has a profile of a parabola. The shape parameter defines the segment of the parabola to utilize. The shape parameter 1.0 produces a <b>full parabola</b> which is tangent to the body tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a <b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone. + trans.get("Shape.Parabolicseries.desc1"), + ////A parabolic series transition has a profile of a parabola. The shape parameter defines the segment of the parabola to utilize. The shape parameter 1.0 produces a <b>full parabola</b> which is tangent to the body tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a <b>1/2 parabola</b> and 0 produces a <b>conical</b> transition. + trans.get("Shape.Parabolicseries.desc2")) { + + // In principle a parabolic transition is clippable, but the difference is + // negligible. + + @Override + public boolean usesParameter() { // Range 0...1 + return true; + } + + @Override + public double defaultParameter() { + return 1.0; + } + + @Override + public double getRadius(double x, double radius, double length, double param) { + assert x >= 0; + assert x <= length; + assert radius >= 0; + assert param >= 0; + assert param <= 1; + + return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param)); + } + }, + + //// Haack series + HAACK(trans.get("Shape.Haackseries"), + //// The Haack series nose cones are designed to minimize drag. The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes drag for fixed length and diameter, while a value of 0.333 produces an <b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume. + trans.get("Shape.Haackseries.desc1"), + //// The Haack series <i>nose cones</i> are designed to minimize drag. These transition shapes are their equivalents, but do not necessarily produce optimal drag for transitions. The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, while a value of 0.333 produces an <b>LV-Haack</b> shape. + trans.get("Shape.Haackseries.desc2"), true) { + + @Override + public boolean usesParameter() { + return true; + } + + @Override + public double maxParameter() { + return 1.0 / 3.0; // Range 0...1/3 + } + + @Override + public double getRadius(double x, double radius, double length, double param) { + assert x >= 0; + assert x <= length; + assert radius >= 0; + assert param >= 0; + assert param <= 2; + + double theta = Math.acos(1 - 2 * x / length); + if (MathUtil.equals(param, 0)) { + return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI); + } + return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI); + } + }, + + // POLYNOMIAL("Smooth polynomial", + // "A polynomial is fitted such that the nose cone profile is horizontal "+ + // "at the aft end of the transition. The angle at the tip is defined by "+ + // "the shape parameter.", + // "A polynomial is fitted such that the transition profile is horizontal "+ + // "at the aft end of the transition. The angle at the fore end is defined "+ + // "by the shape parameter.") { + // @Override + // public boolean usesParameter() { + // return true; + // } + // @Override + // public double maxParameter() { + // return 3.0; // Range 0...3 + // } + // @Override + // public double defaultParameter() { + // return 0.0; + // } + // public double getRadius(double x, double radius, double length, double param) { + // assert x >= 0; + // assert x <= length; + // assert radius >= 0; + // assert param >= 0; + // assert param <= 3; + // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x + // x = x/length; + // return radius*((((param-2)*x + (3-2*param))*x + param)*x); + // } + // } + ; + + // Privete fields of the shapes + private final String name; + private final String transitionDesc; + private final String noseconeDesc; + private final boolean canClip; + + // Non-clippable constructor + Shape(String name, String noseconeDesc, String transitionDesc) { + this(name, noseconeDesc, transitionDesc, false); + } + + // Clippable constructor + Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) { + this.name = name; + this.canClip = canClip; + this.noseconeDesc = noseconeDesc; + this.transitionDesc = transitionDesc; + } + + + /** + * Return the name of the transition shape name. + */ + public String getName() { + return name; + } + + /** + * Get a description of the Transition shape. + */ + public String getTransitionDescription() { + return transitionDesc; + } + + /** + * Get a description of the NoseCone shape. + */ + public String getNoseConeDescription() { + return noseconeDesc; + } + + /** + * Check whether the shape differs in clipped mode. The clipping should be + * enabled by default if possible. + */ + public boolean isClippable() { + return canClip; + } + + /** + * Return whether the shape uses the shape parameter. (Default false.) + */ + public boolean usesParameter() { + return false; + } + + /** + * Return the minimum value of the shape parameter. (Default 0.) + */ + public double minParameter() { + return 0.0; + } + + /** + * Return the maximum value of the shape parameter. (Default 1.) + */ + public double maxParameter() { + return 1.0; + } + + /** + * Return the default value of the shape parameter. (Default 0.) + */ + public double defaultParameter() { + return 0.0; + } + + /** + * Calculate the basic radius of a transition with the given radius, length and + * shape parameter at the point x from the tip of the component. It is assumed + * that the fore radius if zero and the aft radius is <code>radius >= 0</code>. + * Boattails are achieved by reversing the component. + * + * @param x Position from the tip of the component. + * @param radius Aft end radius >= 0. + * @param length Length of the transition >= 0. + * @param param Valid shape parameter. + * @return The basic radius at the given position. + */ + public abstract double getRadius(double x, double radius, double length, double param); + + + /** + * Returns the name of the shape (same as getName()). + */ + @Override + public String toString() { + return name; + } + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java b/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java new file mode 100644 index 00000000..24c773e5 --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java @@ -0,0 +1,185 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +import java.util.ArrayList; +import java.util.List; + +/** + * A set of trapezoidal fins. The root and tip chords are perpendicular to the rocket + * base line, while the leading and aft edges may be slanted. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + +public class TrapezoidFinSet extends FinSet { + private static final Translator trans = Application.getTranslator(); + + public static final double MAX_SWEEP_ANGLE = (89 * Math.PI / 180.0); + + /* + * sweep tipChord + * | |___________ + * | / | + * | / | + * | / | height + * / | + * __________/________________|_____________ + * length + * == rootChord + */ + + // rootChord == length + private double tipChord = 0; + private double height = 0; + private double sweep = 0; + + + public TrapezoidFinSet() { + this(3, 0.05, 0.05, 0.025, 0.03); + } + + // TODO: HIGH: height=0 -> CP = NaN + public TrapezoidFinSet(int fins, double rootChord, double tipChord, double sweep, + double height) { + super(); + + this.setFinCount(fins); + this.length = rootChord; + this.tipChord = tipChord; + this.sweep = sweep; + this.height = height; + } + + + public void setFinShape(double rootChord, double tipChord, double sweep, double height, + double thickness) { + if (this.length == rootChord && this.tipChord == tipChord && this.sweep == sweep && + this.height == height && this.thickness == thickness) + return; + this.length = rootChord; + this.tipChord = tipChord; + this.sweep = sweep; + this.height = height; + this.thickness = thickness; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public double getRootChord() { + return length; + } + + public void setRootChord(double r) { + if (length == r) + return; + length = Math.max(r, 0); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + public double getTipChord() { + return tipChord; + } + + public void setTipChord(double r) { + if (tipChord == r) + return; + tipChord = Math.max(r, 0); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + /** + * Get the sweep length. + */ + public double getSweep() { + return sweep; + } + + /** + * Set the sweep length. + */ + public void setSweep(double r) { + if (sweep == r) + return; + sweep = r; + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + /** + * Get the sweep angle. This is calculated from the true sweep and height, and is not + * stored separately. + */ + public double getSweepAngle() { + if (height == 0) { + if (sweep > 0) + return Math.PI / 2; + if (sweep < 0) + return -Math.PI / 2; + return 0; + } + return Math.atan(sweep / height); + } + + /** + * Sets the sweep by the sweep angle. The sweep is calculated and set by this method, + * and the angle itself is not stored. + */ + public void setSweepAngle(double r) { + if (r > MAX_SWEEP_ANGLE) + r = MAX_SWEEP_ANGLE; + if (r < -MAX_SWEEP_ANGLE) + r = -MAX_SWEEP_ANGLE; + double sweep = Math.tan(r) * height; + if (Double.isNaN(sweep) || Double.isInfinite(sweep)) + return; + setSweep(sweep); + } + + public double getHeight() { + return height; + } + + public void setHeight(double r) { + if (height == r) + return; + height = Math.max(r, 0); + fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); + } + + + + /** + * Returns the geometry of a trapezoidal fin. + */ + @Override + public Coordinate[] getFinPoints() { + List<Coordinate> list = new ArrayList<Coordinate>(4); + + list.add(Coordinate.NUL); + list.add(new Coordinate(sweep, height)); + if (tipChord > 0.0001) { + list.add(new Coordinate(sweep + tipChord, height)); + } + list.add(new Coordinate(MathUtil.max(length, 0.0001), 0)); + + return list.toArray(new Coordinate[list.size()]); + } + + /** + * Returns the span of a trapezoidal fin. + */ + @Override + public double getSpan() { + return height; + } + + + @Override + public String getComponentName() { + //// Trapezoidal fin set + return trans.get("TrapezoidFinSet.TrapezoidFinSet"); + } + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java b/core/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java new file mode 100644 index 00000000..5e22ec0c --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.rocketcomponent; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + + +public class TubeCoupler extends ThicknessRingComponent implements RadialParent { + private static final Translator trans = Application.getTranslator(); + + public TubeCoupler() { + setOuterRadiusAutomatic(true); + setThickness(0.002); + setLength(0.06); + } + + + // Make setter visible + @Override + public void setOuterRadiusAutomatic(boolean auto) { + super.setOuterRadiusAutomatic(auto); + } + + + @Override + public String getComponentName() { + //// Tube coupler + return trans.get("TubeCoupler.TubeCoupler"); + } + + @Override + public boolean allowsChildren() { + return true; + } + + /** + * Allow all InternalComponents to be added to this component. + */ + @Override + public boolean isCompatible(Class<? extends RocketComponent> type) { + return InternalComponent.class.isAssignableFrom(type); + } + + + @Override + public double getInnerRadius(double x) { + return getInnerRadius(); + } + + + @Override + public double getOuterRadius(double x) { + return getOuterRadius(); + } +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Visitable.java b/core/src/net/sf/openrocket/rocketcomponent/Visitable.java new file mode 100644 index 00000000..5f46b7ff --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Visitable.java @@ -0,0 +1,41 @@ +/* + * Visitable.java + */ +package net.sf.openrocket.rocketcomponent; + +/** + * This interface describes a portion of the Visitor pattern, using generics to assure type-safety. + * The elements of the concrete object hierarchy are only visitable by an associated hierarchy of visitors, + * while these visitors are only able to visit the elements of that hierarchy. + * + * The key concept regarding the Visitor pattern is to realize that Java will only "discriminate" the type of an + * object being called, not the type of an object being passed. + * + * In order for the type of two objects to be determinable to the JVM, each object must be the receiver of an + * invocation. Here, when accept is called on a Visitable, the concrete type of the Visitable becomes "known" but the + * concrete type of the argument is still unknown. <code>visit</code> is then called on the parameter object, passing + * the Visitable back, which has type and identity. Flow of control has now been 'double-dispatched' such that the + * type (and identity) of both objects are known. + * + * Specifically, this interface is to be implemented by every class in the RocketComponent hierarchy that + * can be visited AND which are sufficiently specialized from their super class. If they only provide + * constraints to their superclass (such as TubeCoupler), then the implementation of this interface at + * the superclass level is sufficient. + * + * Admittedly, the syntax is a bit contorted here, as it is necessarily self-referential for type-safety. + * + * <V> The visitor type + * <T> The visitable (the concrete class that implements this interface) + */ +public interface Visitable<V extends Visitor<V, T>, T extends Visitable<V, T>> { + + /** + * Any class in the hierarchy that allows itself to be visited will implement this method. The normal + * behavior is that the visitor will invoke this method of a Visitable, passing itself. The Visitable + * turns around calls the Visitor back. This idiom is also known as 'double-dispatching'. + * + * @param visitor the visitor that will be called back + */ + public void accept(V visitor); + +} diff --git a/core/src/net/sf/openrocket/rocketcomponent/Visitor.java b/core/src/net/sf/openrocket/rocketcomponent/Visitor.java new file mode 100644 index 00000000..edb39f2b --- /dev/null +++ b/core/src/net/sf/openrocket/rocketcomponent/Visitor.java @@ -0,0 +1,39 @@ +/* + * Visitor.java + */ +package net.sf.openrocket.rocketcomponent; + +/** + * This interface describes a portion of the Visitor pattern, using generics to assure type-safety. + * The elements of the concrete object hierarchy are only visitable by an associated hierarchy of visitors, + * while these visitors are only able to visit the elements of that hierarchy. + * + * The key concept regarding the Visitor pattern is to realize that Java will only "discriminate" the type of an + * object being called, not the type of an object being passed. + * + * In order for the type of two objects to be determinable to the JVM, each object must be the receiver of an + * invocation. Here, when accept is called on a Visitable, the concrete type of the Visitable becomes "known" but the + * concrete type of the argument is still unknown. <code>visit</code> is then called on the parameter object, passing + * the Visitable back, which has type and identity. Flow of control has now been 'double-dispatched' such that the + * type (and identity) of both objects are known. + * + * Specifically, this interface is to be implemented by every class in the RocketComponent hierarchy that + * can be visited AND which are sufficiently specialized from their super class. If they only provide + * constraints to their superclass (such as TubeCoupler), then the implementation of this interface at + * the superclass level is sufficient. + * + * Admittedly, the syntax is a bit contorted here, as it is necessarily self-referential for type-safety. + * + * <V> The visitor type (the concrete class that implements this interface) + * <T> The visitable + */ +public interface Visitor<V extends Visitor<V, T>, T extends Visitable<V, T>> { + + /** + * The callback method. This method is the 2nd leg of the double-dispatch, having been invoked from a + * corresponding <code>accept</code>. + * + * @param visitable the instance of the Visitable (the target of what is being visiting) + */ + void visit(T visitable); +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java b/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java new file mode 100644 index 00000000..85be9120 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java @@ -0,0 +1,232 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.motor.MotorInstanceConfiguration; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Quaternion; + +public abstract class AbstractSimulationStepper implements SimulationStepper { + + /** + * Compute the atmospheric conditions, allowing listeners to override. + * + * @param status the simulation status + * @return the atmospheric conditions to use + * @throws SimulationException if a listener throws SimulationException + */ + protected AtmosphericConditions modelAtmosphericConditions(SimulationStatus status) throws SimulationException { + AtmosphericConditions conditions; + + // Call pre-listener + conditions = SimulationListenerHelper.firePreAtmosphericModel(status); + if (conditions != null) { + return conditions; + } + + // Compute conditions + double altitude = status.getRocketPosition().z + status.getSimulationConditions().getLaunchSite().getAltitude(); + conditions = status.getSimulationConditions().getAtmosphericModel().getConditions(altitude); + + // Call post-listener + conditions = SimulationListenerHelper.firePostAtmosphericModel(status, conditions); + + checkNaN(conditions.getPressure()); + checkNaN(conditions.getTemperature()); + + return conditions; + } + + + + /** + * Compute the wind to use, allowing listeners to override. + * + * @param status the simulation status + * @return the wind conditions to use + * @throws SimulationException if a listener throws SimulationException + */ + protected Coordinate modelWindVelocity(SimulationStatus status) throws SimulationException { + Coordinate wind; + + // Call pre-listener + wind = SimulationListenerHelper.firePreWindModel(status); + if (wind != null) { + return wind; + } + + // Compute conditions + double altitude = status.getRocketPosition().z + status.getSimulationConditions().getLaunchSite().getAltitude(); + wind = status.getSimulationConditions().getWindModel().getWindVelocity(status.getSimulationTime(), altitude); + + // Call post-listener + wind = SimulationListenerHelper.firePostWindModel(status, wind); + + checkNaN(wind); + + return wind; + } + + + + /** + * Compute the gravity to use, allowing listeners to override. + * + * @param status the simulation status + * @return the gravitational acceleration to use + * @throws SimulationException if a listener throws SimulationException + */ + protected double modelGravity(SimulationStatus status) throws SimulationException { + double gravity; + + // Call pre-listener + gravity = SimulationListenerHelper.firePreGravityModel(status); + if (!Double.isNaN(gravity)) { + return gravity; + } + + // Compute conditions + gravity = status.getSimulationConditions().getGravityModel().getGravity(status.getRocketWorldPosition()); + + // Call post-listener + gravity = SimulationListenerHelper.firePostGravityModel(status, gravity); + + checkNaN(gravity); + + return gravity; + } + + + + /** + * Compute the mass data to use, allowing listeners to override. + * + * @param status the simulation status + * @return the mass data to use + * @throws SimulationException if a listener throws SimulationException + */ + protected MassData calculateMassData(SimulationStatus status) throws SimulationException { + MassData mass; + Coordinate cg; + double longitudinalInertia, rotationalInertia; + + // Call pre-listener + mass = SimulationListenerHelper.firePreMassCalculation(status); + if (mass != null) { + return mass; + } + + MassCalculator calc = status.getSimulationConditions().getMassCalculator(); + cg = calc.getCG(status.getConfiguration(), status.getMotorConfiguration()); + longitudinalInertia = calc.getLongitudinalInertia(status.getConfiguration(), status.getMotorConfiguration()); + rotationalInertia = calc.getRotationalInertia(status.getConfiguration(), status.getMotorConfiguration()); + mass = new MassData(cg, longitudinalInertia, rotationalInertia); + + // Call post-listener + mass = SimulationListenerHelper.firePostMassCalculation(status, mass); + + checkNaN(mass.getCG()); + checkNaN(mass.getLongitudinalInertia()); + checkNaN(mass.getRotationalInertia()); + + return mass; + } + + + + + + /** + * Calculate the average thrust produced by the motors in the current configuration, allowing + * listeners to override. The average is taken between <code>status.time</code> and + * <code>status.time + timestep</code>. + * <p> + * TODO: HIGH: This method does not take into account any moments generated by off-center motors. + * + * @param status the current simulation status. + * @param timestep the time step of the current iteration. + * @param acceleration the current (approximate) acceleration + * @param atmosphericConditions the current atmospheric conditions + * @param stepMotors whether to step the motors forward or work on a clone object + * @return the average thrust during the time step. + */ + protected double calculateThrust(SimulationStatus status, double timestep, + double acceleration, AtmosphericConditions atmosphericConditions, + boolean stepMotors) throws SimulationException { + double thrust; + + // Pre-listeners + thrust = SimulationListenerHelper.firePreThrustCalculation(status); + if (!Double.isNaN(thrust)) { + return thrust; + } + + Configuration configuration = status.getConfiguration(); + + // Iterate over the motors and calculate combined thrust + MotorInstanceConfiguration mic = status.getMotorConfiguration(); + if (!stepMotors) { + mic = mic.clone(); + } + mic.step(status.getSimulationTime() + timestep, acceleration, atmosphericConditions); + thrust = 0; + for (MotorId id : mic.getMotorIDs()) { + if (configuration.isComponentActive((RocketComponent) mic.getMotorMount(id))) { + MotorInstance motor = mic.getMotorInstance(id); + thrust += motor.getThrust(); + } + } + + // Post-listeners + thrust = SimulationListenerHelper.firePostThrustCalculation(status, thrust); + + checkNaN(thrust); + + return thrust; + } + + + /** + * Check that the provided value is not NaN. + * + * @param d the double value to check. + * @throws BugException if the value is NaN. + */ + protected void checkNaN(double d) { + if (Double.isNaN(d)) { + throw new BugException("Simulation resulted in not-a-number (NaN) value, please report a bug."); + } + } + + /** + * Check that the provided coordinate is not NaN. + * + * @param c the coordinate value to check. + * @throws BugException if the value is NaN. + */ + protected void checkNaN(Coordinate c) { + if (c.isNaN()) { + throw new BugException("Simulation resulted in not-a-number (NaN) value, please report a bug, c=" + c); + } + } + + + /** + * Check that the provided quaternion is not NaN. + * + * @param q the quaternion value to check. + * @throws BugException if the value is NaN. + */ + protected void checkNaN(Quaternion q) { + if (q.isNaN()) { + throw new BugException("Simulation resulted in not-a-number (NaN) value, please report a bug, q=" + q); + } + } +} diff --git a/core/src/net/sf/openrocket/simulation/AccelerationData.java b/core/src/net/sf/openrocket/simulation/AccelerationData.java new file mode 100644 index 00000000..31d19ba9 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/AccelerationData.java @@ -0,0 +1,91 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Quaternion; + +public final class AccelerationData { + + private Coordinate linearAccelerationRC; + private Coordinate rotationalAccelerationRC; + private Coordinate linearAccelerationWC; + private Coordinate rotationalAccelerationWC; + // Rotates from rocket coordinates to world coordinates + private final Quaternion rotation; + + + public AccelerationData(Coordinate linearAccelerationRC, Coordinate rotationalAccelerationRC, + Coordinate linearAccelerationWC, Coordinate rotationalAccelerationWC, + Quaternion rotation) { + + if ((linearAccelerationRC == null && linearAccelerationWC == null) || + (rotationalAccelerationRC == null && rotationalAccelerationWC == null) || + rotation == null) { + throw new IllegalArgumentException("Parameter is null: " + + " linearAccelerationRC=" + linearAccelerationRC + + " linearAccelerationWC=" + linearAccelerationWC + + " rotationalAccelerationRC=" + rotationalAccelerationRC + + " rotationalAccelerationWC=" + rotationalAccelerationWC + + " rotation=" + rotation); + } + this.linearAccelerationRC = linearAccelerationRC; + this.rotationalAccelerationRC = rotationalAccelerationRC; + this.linearAccelerationWC = linearAccelerationWC; + this.rotationalAccelerationWC = rotationalAccelerationWC; + this.rotation = rotation; + } + + + + + public Coordinate getLinearAccelerationRC() { + if (linearAccelerationRC == null) { + linearAccelerationRC = rotation.invRotate(linearAccelerationWC); + } + return linearAccelerationRC; + } + + public Coordinate getRotationalAccelerationRC() { + if (rotationalAccelerationRC == null) { + rotationalAccelerationRC = rotation.invRotate(rotationalAccelerationWC); + } + return rotationalAccelerationRC; + } + + public Coordinate getLinearAccelerationWC() { + if (linearAccelerationWC == null) { + linearAccelerationWC = rotation.rotate(linearAccelerationRC); + } + return linearAccelerationWC; + } + + public Coordinate getRotationalAccelerationWC() { + if (rotationalAccelerationWC == null) { + rotationalAccelerationWC = rotation.rotate(rotationalAccelerationRC); + } + return rotationalAccelerationWC; + } + + public Quaternion getRotation() { + return rotation; + } + + + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof AccelerationData)) + return false; + AccelerationData other = (AccelerationData) obj; + return (this.getLinearAccelerationRC().equals(other.getLinearAccelerationRC()) && + this.getRotationalAccelerationRC().equals(other.getRotationalAccelerationRC())); + } + + @Override + public int hashCode() { + return getLinearAccelerationRC().hashCode() ^ getRotationalAccelerationRC().hashCode(); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java new file mode 100644 index 00000000..002fd62c --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -0,0 +1,572 @@ +package net.sf.openrocket.simulation; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.motor.MotorInstanceConfiguration; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.simulation.exception.MotorIgnitionException; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.exception.SimulationLaunchException; +import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Pair; +import net.sf.openrocket.util.Quaternion; + + +public class BasicEventSimulationEngine implements SimulationEngine { + + private static final LogHelper log = Application.getLogger(); + + // TODO: MEDIUM: Allow selecting steppers + private SimulationStepper flightStepper = new RK4SimulationStepper(); + private SimulationStepper landingStepper = new BasicLandingStepper(); + + private SimulationStepper currentStepper; + + private SimulationStatus status; + + + @Override + public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException { + Set<MotorId> motorBurntOut = new HashSet<MotorId>(); + + // Set up flight data + FlightData flightData = new FlightData(); + + // Set up rocket configuration + Configuration configuration = setupConfiguration(simulationConditions); + MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration); + if (motorConfiguration.getMotorIDs().isEmpty()) { + throw new MotorIgnitionException("No motors defined in the simulation."); + } + + // Initialize the simulation + currentStepper = flightStepper; + status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData); + status = currentStepper.initialize(status); + + + SimulationListenerHelper.fireStartSimulation(status); + // Get originating position (in case listener has modified launch position) + Coordinate origin = status.getRocketPosition(); + Coordinate originVelocity = status.getRocketVelocity(); + + try { + double maxAlt = Double.NEGATIVE_INFINITY; + + // Start the simulation + while (handleEvents()) { + + // Take the step + double oldAlt = status.getRocketPosition().z; + + if (SimulationListenerHelper.firePreStep(status)) { + // Step at most to the next event + double maxStepTime = Double.MAX_VALUE; + FlightEvent nextEvent = status.getEventQueue().peek(); + if (nextEvent != null) { + maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001); + } + log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime()); + currentStepper.step(status, maxStepTime); + } + SimulationListenerHelper.firePostStep(status); + + + // Check for NaN values in the simulation status + checkNaN(); + + // Add altitude event + addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(), + status.getConfiguration().getRocket(), + new Pair<Double, Double>(oldAlt, status.getRocketPosition().z))); + + if (status.getRocketPosition().z > maxAlt) { + maxAlt = status.getRocketPosition().z; + } + + + // Position relative to start location + Coordinate relativePosition = status.getRocketPosition().sub(origin); + + // Add appropriate events + if (!status.isLiftoff()) { + + // Avoid sinking into ground before liftoff + if (relativePosition.z < 0) { + status.setRocketPosition(origin); + status.setRocketVelocity(originVelocity); + } + // Detect lift-off + if (relativePosition.z > 0.02) { + addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime())); + } + + } else { + + // Check ground hit after liftoff + if (status.getRocketPosition().z < 0) { + status.setRocketPosition(status.getRocketPosition().setZ(0)); + addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime())); + addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); + } + + } + + // Check for launch guide clearance + if (!status.isLaunchRodCleared() && + relativePosition.length() > status.getSimulationConditions().getLaunchRodLength()) { + addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null)); + } + + + // Check for apogee + if (!status.isApogeeReached() && status.getRocketPosition().z < maxAlt - 0.01) { + addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(), + status.getConfiguration().getRocket())); + } + + + // Check for burnt out motors + for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) { + MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId); + if (!motor.isActive() && motorBurntOut.add(motorId)) { + addEvent(new FlightEvent(FlightEvent.Type.BURNOUT, status.getSimulationTime(), + (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId), motorId)); + } + } + + } + + } catch (SimulationException e) { + SimulationListenerHelper.fireEndSimulation(status, e); + throw e; + } + + SimulationListenerHelper.fireEndSimulation(status, null); + + flightData.addBranch(status.getFlightData()); + + if (!flightData.getWarningSet().isEmpty()) { + log.info("Warnings at the end of simulation: " + flightData.getWarningSet()); + } + + // TODO: HIGH: Simulate branches + return flightData; + } + + + + private SimulationStatus initialStatus(Configuration configuration, + MotorInstanceConfiguration motorConfiguration, + SimulationConditions simulationConditions, FlightData flightData) { + + SimulationStatus init = new SimulationStatus(); + init.setSimulationConditions(simulationConditions); + init.setConfiguration(configuration); + init.setMotorConfiguration(motorConfiguration); + + init.setSimulationTime(0); + init.setPreviousTimeStep(simulationConditions.getTimeStep()); + init.setRocketPosition(Coordinate.NUL); + init.setRocketVelocity(Coordinate.NUL); + init.setRocketWorldPosition(simulationConditions.getLaunchSite()); + + // Initialize to roll angle with least stability w.r.t. the wind + Quaternion o; + FlightConditions cond = new FlightConditions(configuration); + simulationConditions.getAerodynamicCalculator().getWorstCP(configuration, cond, null); + double angle = -cond.getTheta() - simulationConditions.getLaunchRodDirection(); + o = Quaternion.rotation(new Coordinate(0, 0, angle)); + + // Launch rod angle and direction + o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, simulationConditions.getLaunchRodAngle(), 0))); + o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, 0, simulationConditions.getLaunchRodDirection()))); + + init.setRocketOrientationQuaternion(o); + init.setRocketRotationVelocity(Coordinate.NUL); + + + /* + * Calculate the effective launch rod length taking into account launch lugs. + * If no lugs are found, assume a tower launcher of full length. + */ + double length = simulationConditions.getLaunchRodLength(); + double lugPosition = Double.NaN; + for (RocketComponent c : configuration) { + if (c instanceof LaunchLug) { + double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x; + if (Double.isNaN(lugPosition) || pos > lugPosition) { + lugPosition = pos; + } + } + } + if (!Double.isNaN(lugPosition)) { + double maxX = 0; + for (Coordinate c : configuration.getBounds()) { + if (c.x > maxX) + maxX = c.x; + } + if (maxX >= lugPosition) { + length = Math.max(0, length - (maxX - lugPosition)); + } + } + init.setEffectiveLaunchRodLength(length); + + + + init.setSimulationStartWallTime(System.nanoTime()); + + init.setMotorIgnited(false); + init.setLiftoff(false); + init.setLaunchRodCleared(false); + init.setApogeeReached(false); + + init.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket())); + + init.setFlightData(new FlightDataBranch("MAIN", FlightDataType.TYPE_TIME)); + init.setWarnings(flightData.getWarningSet()); + + return init; + } + + + + /** + * Create a rocket configuration from the launch conditions. + * + * @param simulation the launch conditions. + * @return a rocket configuration with all stages attached. + */ + private Configuration setupConfiguration(SimulationConditions simulation) { + Configuration configuration = new Configuration(simulation.getRocket()); + configuration.setAllStages(); + configuration.setMotorConfigurationID(simulation.getMotorConfigurationID()); + + return configuration; + } + + + + /** + * Create a new motor instance configuration for the rocket configuration. + * + * @param configuration the rocket configuration. + * @return a new motor instance configuration with all motors in place. + */ + private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) { + MotorInstanceConfiguration motors = new MotorInstanceConfiguration(); + final String motorId = configuration.getMotorConfigurationID(); + + Iterator<MotorMount> iterator = configuration.motorIterator(); + while (iterator.hasNext()) { + MotorMount mount = iterator.next(); + RocketComponent component = (RocketComponent) mount; + Motor motor = mount.getMotor(motorId); + + if (motor != null) { + Coordinate[] positions = component.toAbsolute(mount.getMotorPosition(motorId)); + for (int i = 0; i < positions.length; i++) { + Coordinate position = positions[i]; + MotorId id = new MotorId(component.getID(), i + 1); + motors.addMotor(id, motor.getInstance(), mount, position); + } + } + } + return motors; + } + + /** + * Handles events occurring during the flight from the event queue. + * Each event that has occurred before or at the current simulation time is + * processed. Suitable events are also added to the flight data. + */ + private boolean handleEvents() throws SimulationException { + boolean ret = true; + FlightEvent event; + + for (event = nextEvent(); event != null; event = nextEvent()) { + + // Call simulation listeners, allow aborting event handling + if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) { + continue; + } + + if (event.getType() != FlightEvent.Type.ALTITUDE) { + log.verbose("BasicEventSimulationEngine: Handling event " + event); + } + + if (event.getType() == FlightEvent.Type.IGNITION) { + MotorMount mount = (MotorMount) event.getSource(); + MotorId motorId = (MotorId) event.getData(); + MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId); + if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) { + continue; + } + } + + if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) { + RecoveryDevice device = (RecoveryDevice) event.getSource(); + if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) { + continue; + } + } + + + + // Check for motor ignition events, add ignition events to queue + for (MotorId id : status.getMotorConfiguration().getMotorIDs()) { + MotorMount mount = status.getMotorConfiguration().getMotorMount(id); + RocketComponent component = (RocketComponent) mount; + + if (mount.getIgnitionEvent().isActivationEvent(event, component)) { + addEvent(new FlightEvent(FlightEvent.Type.IGNITION, + status.getSimulationTime() + mount.getIgnitionDelay(), + component, id)); + } + } + + + // Check for recovery device deployment, add events to queue + Iterator<RocketComponent> rci = status.getConfiguration().iterator(); + while (rci.hasNext()) { + RocketComponent c = rci.next(); + if (!(c instanceof RecoveryDevice)) + continue; + if (((RecoveryDevice) c).getDeployEvent().isActivationEvent(event, c)) { + // Delay event by at least 1ms to allow stage separation to occur first + addEvent(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, + event.getTime() + Math.max(0.001, ((RecoveryDevice) c).getDeployDelay()), c)); + } + } + + + // Handle event + switch (event.getType()) { + + case LAUNCH: { + status.getFlightData().addEvent(event); + break; + } + + case IGNITION: { + // Ignite the motor + MotorMount mount = (MotorMount) event.getSource(); + RocketComponent component = (RocketComponent) mount; + MotorId motorId = (MotorId) event.getData(); + MotorInstanceConfiguration config = status.getMotorConfiguration(); + config.setMotorIgnitionTime(motorId, event.getTime()); + status.setMotorIgnited(true); + status.getFlightData().addEvent(event); + + // Add stage separation event if appropriate + int n = component.getStageNumber(); + if (n < component.getRocket().getStageCount() - 1) { + if (status.getConfiguration().isStageActive(n + 1)) { + addEvent(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, event.getTime(), + component.getStage())); + } + } + break; + } + + case LIFTOFF: { + // Mark lift-off as occurred + status.setLiftoff(true); + status.getFlightData().addEvent(event); + break; + } + + case LAUNCHROD: { + // Mark launch rod as cleared + status.setLaunchRodCleared(true); + status.getFlightData().addEvent(event); + break; + } + + case BURNOUT: { + // If motor burnout occurs without lift-off, abort + if (!status.isLiftoff()) { + throw new SimulationLaunchException("Motor burnout without liftoff."); + } + // Add ejection charge event + String id = status.getConfiguration().getMotorConfigurationID(); + MotorMount mount = (MotorMount) event.getSource(); + double delay = mount.getMotorDelay(id); + if (delay != Motor.PLUGGED) { + addEvent(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, status.getSimulationTime() + delay, + event.getSource(), event.getData())); + } + status.getFlightData().addEvent(event); + break; + } + + case EJECTION_CHARGE: { + status.getFlightData().addEvent(event); + break; + } + + case STAGE_SEPARATION: { + // TODO: HIGH: Store lower stages to be simulated later + RocketComponent stage = event.getSource(); + int n = stage.getStageNumber(); + status.getConfiguration().setToStage(n); + status.getFlightData().addEvent(event); + break; + } + + case APOGEE: + // Mark apogee as reached + status.setApogeeReached(true); + status.getFlightData().addEvent(event); + break; + + case RECOVERY_DEVICE_DEPLOYMENT: + RocketComponent c = event.getSource(); + int n = c.getStageNumber(); + // Ignore event if stage not active + if (status.getConfiguration().isStageActive(n)) { + // TODO: HIGH: Check stage activeness for other events as well? + + // Check whether any motor in the active stages is active anymore + for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) { + int stage = ((RocketComponent) status.getMotorConfiguration(). + getMotorMount(motorId)).getStageNumber(); + if (!status.getConfiguration().isStageActive(stage)) + continue; + if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive()) + continue; + status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING); + } + + // Check for launch rod + if (!status.isLaunchRodCleared()) { + status.getWarnings().add(Warning.fromString("Recovery device device deployed while on " + + "the launch guide.")); + } + + // Check current velocity + if (status.getRocketVelocity().length() > 20) { + // TODO: LOW: Custom warning. + status.getWarnings().add(Warning.fromString("Recovery device deployment at high " + + "speed (" + + UnitGroup.UNITS_VELOCITY.toStringUnit(status.getRocketVelocity().length()) + + ").")); + } + + status.setLiftoff(true); + status.getDeployedRecoveryDevices().add((RecoveryDevice) c); + + this.currentStepper = this.landingStepper; + this.status = currentStepper.initialize(status); + + status.getFlightData().addEvent(event); + } + break; + + case GROUND_HIT: + status.getFlightData().addEvent(event); + break; + + case SIMULATION_END: + ret = false; + status.getFlightData().addEvent(event); + break; + + case ALTITUDE: + break; + } + + } + + + // If no motor has ignited, abort + if (!status.isMotorIgnited()) { + throw new MotorIgnitionException("No motors ignited."); + } + + return ret; + } + + + /** + * Add a flight event to the event queue unless a listener aborts adding it. + * + * @param event the event to add to the queue. + */ + private void addEvent(FlightEvent event) throws SimulationException { + if (SimulationListenerHelper.fireAddFlightEvent(status, event)) { + status.getEventQueue().add(event); + } + } + + + + /** + * Return the next flight event to handle, or null if no more events should be handled. + * This method jumps the simulation time forward in case no motors have been ignited. + * The flight event is removed from the event queue. + * + * @param status the simulation status + * @return the flight event to handle, or null + */ + private FlightEvent nextEvent() { + EventQueue queue = status.getEventQueue(); + FlightEvent event = queue.peek(); + if (event == null) + return null; + + // Jump to event if no motors have been ignited + if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) { + status.setSimulationTime(event.getTime()); + } + if (event.getTime() <= status.getSimulationTime()) { + return queue.poll(); + } else { + return null; + } + } + + + + private void checkNaN() throws SimulationException { + double d = 0; + boolean b = false; + d += status.getSimulationTime(); + d += status.getPreviousTimeStep(); + b |= status.getRocketPosition().isNaN(); + b |= status.getRocketVelocity().isNaN(); + b |= status.getRocketOrientationQuaternion().isNaN(); + b |= status.getRocketRotationVelocity().isNaN(); + d += status.getEffectiveLaunchRodLength(); + + if (Double.isNaN(d) || b) { + log.error("Simulation resulted in NaN value:" + + " simulationTime=" + status.getSimulationTime() + + " previousTimeStep=" + status.getPreviousTimeStep() + + " rocketPosition=" + status.getRocketPosition() + + " rocketVelocity=" + status.getRocketVelocity() + + " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() + + " rocketRotationVelocity=" + status.getRocketRotationVelocity() + + " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength()); + throw new SimulationException("Simulation resulted in not-a-number (NaN) value, please report a bug."); + } + } + + +} diff --git a/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java b/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java new file mode 100644 index 00000000..dc67e853 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/BasicLandingStepper.java @@ -0,0 +1,138 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.GeodeticComputationStrategy; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.WorldCoordinate; + +public class BasicLandingStepper extends AbstractSimulationStepper { + + private static final double RECOVERY_TIME_STEP = 0.5; + + @Override + public SimulationStatus initialize(SimulationStatus status) throws SimulationException { + return status; + } + + @Override + public void step(SimulationStatus status, double maxTimeStep) throws SimulationException { + double totalCD = 0; + double refArea = status.getConfiguration().getReferenceArea(); + + // Get the atmospheric conditions + AtmosphericConditions atmosphere = modelAtmosphericConditions(status); + + //// Local wind speed and direction + Coordinate windSpeed = modelWindVelocity(status); + Coordinate airSpeed = status.getRocketVelocity().add(windSpeed); + + // Get total CD + double mach = airSpeed.length() / atmosphere.getMachSpeed(); + for (RecoveryDevice c : status.getDeployedRecoveryDevices()) { + totalCD += c.getCD(mach) * c.getArea() / refArea; + } + + // Compute drag force + double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2()); + double dragForce = totalCD * dynP * refArea; + MassData massData = calculateMassData(status); + double mass = massData.getCG().weight; + + + // Compute drag acceleration + Coordinate linearAcceleration; + if (airSpeed.length() > 0.001) { + linearAcceleration = airSpeed.normalize().multiply(-dragForce / mass); + } else { + linearAcceleration = Coordinate.NUL; + } + + // Add effect of gravity + double gravity = modelGravity(status); + linearAcceleration = linearAcceleration.sub(0, 0, gravity); + + + // Add coriolis acceleration + Coordinate coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation().getCoriolisAcceleration( + status.getRocketWorldPosition(), status.getRocketVelocity()); + linearAcceleration = linearAcceleration.add(coriolisAcceleration); + + + + // Select time step + double timeStep = MathUtil.min(0.5 / linearAcceleration.length(), RECOVERY_TIME_STEP); + + // Perform Euler integration + status.setRocketPosition(status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). + add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2))); + status.setRocketVelocity(status.getRocketVelocity().add(linearAcceleration.multiply(timeStep))); + status.setSimulationTime(status.getSimulationTime() + timeStep); + + + // Update the world coordinate + WorldCoordinate w = status.getSimulationConditions().getLaunchSite(); + w = status.getSimulationConditions().getGeodeticComputation().addCoordinate(w, status.getRocketPosition()); + status.setRocketWorldPosition(w); + + + // Store data + FlightDataBranch data = status.getFlightData(); + boolean extra = status.getSimulationConditions().isCalculateExtras(); + data.addPoint(); + + data.setValue(FlightDataType.TYPE_TIME, status.getSimulationTime()); + data.setValue(FlightDataType.TYPE_ALTITUDE, status.getRocketPosition().z); + data.setValue(FlightDataType.TYPE_POSITION_X, status.getRocketPosition().x); + data.setValue(FlightDataType.TYPE_POSITION_Y, status.getRocketPosition().y); + if (extra) { + data.setValue(FlightDataType.TYPE_POSITION_XY, + MathUtil.hypot(status.getRocketPosition().x, status.getRocketPosition().y)); + data.setValue(FlightDataType.TYPE_POSITION_DIRECTION, + Math.atan2(status.getRocketPosition().y, status.getRocketPosition().x)); + + data.setValue(FlightDataType.TYPE_VELOCITY_XY, + MathUtil.hypot(status.getRocketVelocity().x, status.getRocketVelocity().y)); + data.setValue(FlightDataType.TYPE_ACCELERATION_XY, + MathUtil.hypot(linearAcceleration.x, linearAcceleration.y)); + + data.setValue(FlightDataType.TYPE_ACCELERATION_TOTAL, linearAcceleration.length()); + + double Re = airSpeed.length() * + status.getConfiguration().getLength() / + atmosphere.getKinematicViscosity(); + data.setValue(FlightDataType.TYPE_REYNOLDS_NUMBER, Re); + } + + + data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); + data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); + if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { + data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, coriolisAcceleration.length()); + } + + + data.setValue(FlightDataType.TYPE_VELOCITY_Z, status.getRocketVelocity().z); + data.setValue(FlightDataType.TYPE_ACCELERATION_Z, linearAcceleration.z); + + data.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, airSpeed.length()); + data.setValue(FlightDataType.TYPE_MACH_NUMBER, mach); + + data.setValue(FlightDataType.TYPE_MASS, mass); + + data.setValue(FlightDataType.TYPE_THRUST_FORCE, 0); + data.setValue(FlightDataType.TYPE_DRAG_FORCE, dragForce); + + data.setValue(FlightDataType.TYPE_WIND_VELOCITY, windSpeed.length()); + data.setValue(FlightDataType.TYPE_AIR_TEMPERATURE, atmosphere.getTemperature()); + data.setValue(FlightDataType.TYPE_AIR_PRESSURE, atmosphere.getPressure()); + data.setValue(FlightDataType.TYPE_SPEED_OF_SOUND, atmosphere.getMachSpeed()); + + data.setValue(FlightDataType.TYPE_TIME_STEP, timeStep); + data.setValue(FlightDataType.TYPE_COMPUTATION_TIME, + (System.nanoTime() - status.getSimulationStartWallTime()) / 1000000000.0); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/EventQueue.java b/core/src/net/sf/openrocket/simulation/EventQueue.java new file mode 100644 index 00000000..1ac9fb82 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/EventQueue.java @@ -0,0 +1,65 @@ +package net.sf.openrocket.simulation; + +import java.util.PriorityQueue; + +import net.sf.openrocket.util.Monitorable; + +/** + * A sorted queue of FlightEvent objects. This queue maintains the events in time order + * and also keeps a modification count for the queue. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class EventQueue extends PriorityQueue<FlightEvent> implements Monitorable { + + private int modID = 0; + + public EventQueue() { + super(); + } + + public EventQueue(PriorityQueue<? extends FlightEvent> c) { + super(c); + } + + @Override + public boolean add(FlightEvent e) { + modID++; + return super.add(e); + } + + @Override + public void clear() { + modID++; + super.clear(); + } + + @Override + public boolean offer(FlightEvent e) { + modID++; + return super.offer(e); + } + + @Override + public FlightEvent poll() { + modID++; + return super.poll(); + } + + @Override + public boolean remove(Object o) { + modID++; + return super.remove(o); + } + + public int getModID() { + return modID; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + // TODO Auto-generated method stub + return super.clone(); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/FlightData.java b/core/src/net/sf/openrocket/simulation/FlightData.java new file mode 100644 index 00000000..839c6584 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/FlightData.java @@ -0,0 +1,309 @@ +package net.sf.openrocket.simulation; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Mutable; + +/** + * A collection of various flight data. This is the result of a simulation, or importing + * data into the software. The data includes: + * <ul> + * <li>A number of generally interesting values of a simulation, such as max. altitude and velocity + * <li>A number (or zero) of flight data branches containing the actual data + * <li>A WarningSet including warnings that occurred during simulation + * </ul> + * <p> + * A FlightData object can be made immutable by calling {@link #immute()}. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class FlightData { + private static final LogHelper log = Application.getLogger(); + + /** + * An immutable FlightData object with NaN data. + */ + public static final FlightData NaN_DATA; + static { + FlightData data = new FlightData(); + data.immute(); + NaN_DATA = data; + } + + private Mutable mutable = new Mutable(); + + private final ArrayList<FlightDataBranch> branches = new ArrayList<FlightDataBranch>(); + + private final WarningSet warnings = new WarningSet(); + + private double maxAltitude = Double.NaN; + private double maxVelocity = Double.NaN; + private double maxAcceleration = Double.NaN; + private double maxMachNumber = Double.NaN; + private double timeToApogee = Double.NaN; + private double flightTime = Double.NaN; + private double groundHitVelocity = Double.NaN; + private double launchRodVelocity = Double.NaN; + + + /** + * Create a FlightData object with no content. The resulting object is mutable. + */ + public FlightData() { + + } + + + /** + * Construct a FlightData object with no data branches but the specified + * summary information. The resulting object is mutable. + * + * @param maxAltitude maximum altitude. + * @param maxVelocity maximum velocity. + * @param maxAcceleration maximum acceleration. + * @param maxMachNumber maximum Mach number. + * @param timeToApogee time to apogee. + * @param flightTime total flight time. + * @param groundHitVelocity ground hit velocity. + * @param launchRodVelocity TODO + */ + public FlightData(double maxAltitude, double maxVelocity, double maxAcceleration, + double maxMachNumber, double timeToApogee, double flightTime, + double groundHitVelocity, double launchRodVelocity) { + this.maxAltitude = maxAltitude; + this.maxVelocity = maxVelocity; + this.maxAcceleration = maxAcceleration; + this.maxMachNumber = maxMachNumber; + this.timeToApogee = timeToApogee; + this.flightTime = flightTime; + this.groundHitVelocity = groundHitVelocity; + this.launchRodVelocity = launchRodVelocity; + } + + + /** + * Create a FlightData object with the specified branches. The resulting object is mutable. + * + * @param branches the branches. + */ + public FlightData(FlightDataBranch... branches) { + this(); + + for (FlightDataBranch b : branches) + this.addBranch(b); + + calculateIntrestingValues(); + } + + + + + /** + * Returns the warning set associated with this object. This WarningSet cannot be + * set, so simulations must use this warning set to store their warnings. + * The returned WarningSet should not be modified otherwise. + * + * @return the warnings generated during this simulation. + */ + public WarningSet getWarningSet() { + return warnings; + } + + + public void addBranch(FlightDataBranch branch) { + mutable.check(); + + branch.immute(); + branches.add(branch); + + if (branches.size() == 1) { + calculateIntrestingValues(); + } + } + + public int getBranchCount() { + return branches.size(); + } + + public FlightDataBranch getBranch(int n) { + return branches.get(n); + } + + + + public double getMaxAltitude() { + return maxAltitude; + } + + public double getMaxVelocity() { + return maxVelocity; + } + + /** + * NOTE: This value only takes into account flight phase. + */ + public double getMaxAcceleration() { + return maxAcceleration; + } + + public double getMaxMachNumber() { + return maxMachNumber; + } + + public double getTimeToApogee() { + return timeToApogee; + } + + public double getFlightTime() { + return flightTime; + } + + public double getGroundHitVelocity() { + return groundHitVelocity; + } + + public double getLaunchRodVelocity() { + return launchRodVelocity; + } + + + + /** + * Calculate the max. altitude/velocity/acceleration, time to apogee, flight time + * and ground hit velocity. + */ + private void calculateIntrestingValues() { + if (branches.isEmpty()) + return; + + FlightDataBranch branch = branches.get(0); + maxAltitude = branch.getMaximum(FlightDataType.TYPE_ALTITUDE); + maxVelocity = branch.getMaximum(FlightDataType.TYPE_VELOCITY_TOTAL); + maxMachNumber = branch.getMaximum(FlightDataType.TYPE_MACH_NUMBER); + + flightTime = branch.getLast(FlightDataType.TYPE_TIME); + if (branch.getLast(FlightDataType.TYPE_ALTITUDE) < 10) { + groundHitVelocity = branch.getLast(FlightDataType.TYPE_VELOCITY_TOTAL); + } else { + groundHitVelocity = Double.NaN; + } + + + // Time to apogee + List<Double> time = branch.get(FlightDataType.TYPE_TIME); + List<Double> altitude = branch.get(FlightDataType.TYPE_ALTITUDE); + + if (time == null || altitude == null) { + timeToApogee = Double.NaN; + maxAcceleration = Double.NaN; + return; + } + int index = 0; + for (Double alt : altitude) { + if (alt != null) { + if (MathUtil.equals(alt, maxAltitude)) + break; + } + + index++; + } + if (index < time.size()) + timeToApogee = time.get(index); + else + timeToApogee = Double.NaN; + + + // Launch rod velocity + eventloop: for (FlightEvent event : branch.getEvents()) { + if (event.getType() == FlightEvent.Type.LAUNCHROD) { + double t = event.getTime(); + List<Double> velocity = branch.get(FlightDataType.TYPE_VELOCITY_TOTAL); + if (velocity == null) { + break; + } + for (int i = 0; i < velocity.size(); i++) { + if (time.get(i) >= t) { + launchRodVelocity = velocity.get(i); + break eventloop; + } + } + } + } + + // Max. acceleration (must be after apogee time) + if (branch.get(FlightDataType.TYPE_ACCELERATION_TOTAL) != null) { + maxAcceleration = calculateMaxAcceleration(); + } else { + maxAcceleration = Double.NaN; + } + + log.debug("Computed flight values:" + + " maxAltitude=" + maxAltitude + + " maxVelocity=" + maxVelocity + + " maxAcceleration=" + maxAcceleration + + " maxMachNumber=" + maxMachNumber + + " timeToApogee=" + timeToApogee + + " flightTime=" + flightTime + + " groundHitVelocity=" + groundHitVelocity + + " launchRodVelocity=" + launchRodVelocity); + } + + + public void immute() { + mutable.immute(); + warnings.immute(); + for (FlightDataBranch b : branches) { + b.immute(); + } + } + + + public boolean isMutable() { + return mutable.isMutable(); + } + + + + /** + * Find the maximum acceleration before apogee. + */ + private double calculateMaxAcceleration() { + + // End check at first recovery device deployment + double endTime = Double.MAX_VALUE; + + FlightDataBranch branch = this.getBranch(0); + for (FlightEvent event : branch.getEvents()) { + if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) { + if (event.getTime() < endTime) { + endTime = event.getTime(); + } + } + } + + List<Double> time = branch.get(FlightDataType.TYPE_TIME); + List<Double> acceleration = branch.get(FlightDataType.TYPE_ACCELERATION_TOTAL); + + if (time == null || acceleration == null) { + return Double.NaN; + } + + double max = 0; + + for (int i = 0; i < time.size(); i++) { + if (time.get(i) >= endTime) { + break; + } + double a = acceleration.get(i); + if (a > max) + max = a; + } + + return max; + } +} diff --git a/core/src/net/sf/openrocket/simulation/FlightDataBranch.java b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java new file mode 100644 index 00000000..f3eb3d9c --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/FlightDataBranch.java @@ -0,0 +1,255 @@ +package net.sf.openrocket.simulation; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.sf.openrocket.util.ArrayList; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.Mutable; + +/** + * A single branch of flight data. The data is ordered based on some variable, typically time. + * It also contains flight events that have occurred during simulation. + * <p> + * After instantiating a FlightDataBranch data and new variable types can be added to the branch. + * A new data point (a value for each variable defined) is created using {@link #addPoint()} after + * which the value for each variable type can be set using {@link #setValue(FlightDataType, double)}. + * Each variable type does NOT have to be set, unset values will default to NaN. New variable types + * not defined in the constructor can be added using {@link #setValue(FlightDataType, double)}, they + * will be created and all previous values will be set to NaN. + * <p> + * After populating a FlightDataBranch object it can be made immutable by calling {@link #immute()}. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class FlightDataBranch implements Monitorable { + + /** The name of this flight data branch. */ + private final String branchName; + + private final Map<FlightDataType, ArrayList<Double>> values = + new LinkedHashMap<FlightDataType, ArrayList<Double>>(); + + private final Map<FlightDataType, Double> maxValues = new HashMap<FlightDataType, Double>(); + private final Map<FlightDataType, Double> minValues = new HashMap<FlightDataType, Double>(); + + + private final ArrayList<FlightEvent> events = new ArrayList<FlightEvent>(); + + private Mutable mutable = new Mutable(); + + private int modID = 0; + + /** + * Sole constructor. Defines the name of the FlightDataBranch and at least one variable type. + * + * @param name the name of this FlightDataBranch. + * @param types data types to include (must include at least one type). + */ + public FlightDataBranch(String name, FlightDataType... types) { + if (types.length == 0) { + throw new IllegalArgumentException("Must specify at least one data type."); + } + + this.branchName = name; + + for (FlightDataType t : types) { + if (values.containsKey(t)) { + throw new IllegalArgumentException("Value type " + t + " specified multiple " + + "times in constructor."); + } + + values.put(t, new ArrayList<Double>()); + minValues.put(t, Double.NaN); + maxValues.put(t, Double.NaN); + } + } + + + + /** + * Adds a new point into the data branch. The value for all types is set to NaN by default. + * + * @throws IllegalStateException if this object has been made immutable. + */ + public void addPoint() { + mutable.check(); + + for (FlightDataType t : values.keySet()) { + values.get(t).add(Double.NaN); + } + modID++; + } + + + /** + * Set the value for a specific data type at the latest point. New variable types can be + * added to the FlightDataBranch transparently. + * + * @param type the variable to set. + * @param value the value to set. + * @throws IllegalStateException if this object has been made immutable. + */ + public void setValue(FlightDataType type, double value) { + mutable.check(); + + ArrayList<Double> list = values.get(type); + if (list == null) { + + list = new ArrayList<Double>(); + int n = getLength(); + for (int i = 0; i < n; i++) { + list.add(Double.NaN); + } + values.put(type, list); + minValues.put(type, value); + maxValues.put(type, value); + + } + list.set(list.size() - 1, value); + double min = minValues.get(type); + double max = maxValues.get(type); + + if (Double.isNaN(min) || (value < min)) { + minValues.put(type, value); + } + if (Double.isNaN(max) || (value > max)) { + maxValues.put(type, value); + } + modID++; + } + + + /** + * Return the branch name. + */ + public String getBranchName() { + return branchName; + } + + /** + * Return the variable types included in this branch. The types are sorted in their + * natural order. + */ + public FlightDataType[] getTypes() { + FlightDataType[] array = values.keySet().toArray(new FlightDataType[0]); + Arrays.sort(array); + return array; + } + + /** + * Return the number of data points in this branch. + */ + public int getLength() { + for (FlightDataType t : values.keySet()) { + return values.get(t).size(); + } + return 0; + } + + /** + * Return an array of values for the specified variable type. + * + * @param type the variable type. + * @return a list of the variable values, or <code>null</code> if + * the variable type hasn't been added to this branch. + */ + public List<Double> get(FlightDataType type) { + ArrayList<Double> list = values.get(type); + if (list == null) + return null; + return list.clone(); + } + + /** + * Return the last value of the specified type in the branch, or NaN if the type is + * unavailable. + * + * @param type the parameter type. + * @return the last value in this branch, or NaN. + */ + public double getLast(FlightDataType type) { + ArrayList<Double> list = values.get(type); + if (list == null || list.isEmpty()) + return Double.NaN; + return list.get(list.size() - 1); + } + + /** + * Return the minimum value of the specified type in the branch, or NaN if the type + * is unavailable. + * + * @param type the parameter type. + * @return the minimum value in this branch, or NaN. + */ + public double getMinimum(FlightDataType type) { + Double v = minValues.get(type); + if (v == null) + return Double.NaN; + return v; + } + + /** + * Return the maximum value of the specified type in the branch, or NaN if the type + * is unavailable. + * + * @param type the parameter type. + * @return the maximum value in this branch, or NaN. + */ + public double getMaximum(FlightDataType type) { + Double v = maxValues.get(type); + if (v == null) + return Double.NaN; + return v; + } + + + /** + * Add a flight event to this branch. + * + * @param event the event to add. + * @throws IllegalStateException if this branch has been made immutable. + */ + public void addEvent(FlightEvent event) { + mutable.check(); + events.add(event.resetSourceAndData()); + modID++; + } + + + /** + * Return the list of events. + * + * @return the list of events during the flight. + */ + public List<FlightEvent> getEvents() { + return events.clone(); + } + + + /** + * Make this FlightDataBranch immutable. Any calls to the set methods that would + * modify this object will after this call throw an <code>IllegalStateException</code>. + */ + public void immute() { + mutable.immute(); + } + + + /** + * Return whether this branch is still mutable. + */ + public boolean isMutable() { + return mutable.isMutable(); + } + + + @Override + public int getModID() { + return modID; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/FlightDataType.java b/core/src/net/sf/openrocket/simulation/FlightDataType.java new file mode 100644 index 00000000..26862420 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/FlightDataType.java @@ -0,0 +1,260 @@ +package net.sf.openrocket.simulation; + +import java.util.HashMap; +import java.util.Map; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.unit.UnitGroup; + +/** + * A class defining a storable simulation variable type. This class defined numerous ready + * types, and allows also creating new types with any name. When retrieving types based on + * a name, you should use {@link #getType(String, UnitGroup)} to return the default unit type, + * or a new type if the name does not currently exist. + * <p> + * Each type has a type name (description), a unit group and a priority. The type is identified + * purely by its name case-insensitively. The unit group provides the units for the type. + * The priority is used to order the types. The pre-existing types are defined specific priority + * numbers, and other types have a default priority number that is after all other types. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class FlightDataType implements Comparable<FlightDataType> { + private static final Translator trans = Application.getTranslator(); + + /** Priority of custom-created variables */ + private static final int DEFAULT_PRIORITY = 999; + + /** List of existing types. MUST BE DEFINED BEFORE ANY TYPES!! */ + private static final Map<String, FlightDataType> EXISTING_TYPES = new HashMap<String, FlightDataType>(); + + + + //// Time + public static final FlightDataType TYPE_TIME = newType(trans.get("FlightDataType.TYPE_TIME"), UnitGroup.UNITS_FLIGHT_TIME, 1); + + + //// Vertical position and motion + //// Altitude + public static final FlightDataType TYPE_ALTITUDE = newType(trans.get("FlightDataType.TYPE_ALTITUDE"), UnitGroup.UNITS_DISTANCE, 10); + //// Vertical velocity + public static final FlightDataType TYPE_VELOCITY_Z = newType(trans.get("FlightDataType.TYPE_VELOCITY_Z"), UnitGroup.UNITS_VELOCITY, 11); + //// Vertical acceleration + public static final FlightDataType TYPE_ACCELERATION_Z = newType(trans.get("FlightDataType.TYPE_ACCELERATION_Z"), UnitGroup.UNITS_ACCELERATION, 12); + + + //// Total motion + //// Total velocity + public static final FlightDataType TYPE_VELOCITY_TOTAL = newType(trans.get("FlightDataType.TYPE_VELOCITY_TOTAL"), UnitGroup.UNITS_VELOCITY, 20); + //// Total acceleration + public static final FlightDataType TYPE_ACCELERATION_TOTAL = newType(trans.get("FlightDataType.TYPE_ACCELERATION_TOTAL"), UnitGroup.UNITS_ACCELERATION, 21); + + + //// Lateral position and motion + //// Position upwind + public static final FlightDataType TYPE_POSITION_X = newType(trans.get("FlightDataType.TYPE_POSITION_X"), UnitGroup.UNITS_DISTANCE, 30); + //// Position parallel to wind + public static final FlightDataType TYPE_POSITION_Y = newType(trans.get("FlightDataType.TYPE_POSITION_Y"), UnitGroup.UNITS_DISTANCE, 31); + //// Lateral distance + public static final FlightDataType TYPE_POSITION_XY = newType(trans.get("FlightDataType.TYPE_POSITION_XY"), UnitGroup.UNITS_DISTANCE, 32); + //// Lateral direction + public static final FlightDataType TYPE_POSITION_DIRECTION = newType(trans.get("FlightDataType.TYPE_POSITION_DIRECTION"), UnitGroup.UNITS_ANGLE, 33); + //// Lateral velocity + public static final FlightDataType TYPE_VELOCITY_XY = newType(trans.get("FlightDataType.TYPE_VELOCITY_XY"), UnitGroup.UNITS_VELOCITY, 34); + //// Lateral acceleration + public static final FlightDataType TYPE_ACCELERATION_XY = newType(trans.get("FlightDataType.TYPE_ACCELERATION_XY"), UnitGroup.UNITS_ACCELERATION, 35); + //// Latitude + public static final FlightDataType TYPE_LATITUDE = newType(trans.get("FlightDataType.TYPE_LATITUDE"), UnitGroup.UNITS_ANGLE, 36); + //// Longitude + public static final FlightDataType TYPE_LONGITUDE = newType(trans.get("FlightDataType.TYPE_LONGITUDE"), UnitGroup.UNITS_ANGLE, 37); + + //// Angular motion + //// Angle of attack + public static final FlightDataType TYPE_AOA = newType(trans.get("FlightDataType.TYPE_AOA"), UnitGroup.UNITS_ANGLE, 40); + //// Roll rate + public static final FlightDataType TYPE_ROLL_RATE = newType(trans.get("FlightDataType.TYPE_ROLL_RATE"), UnitGroup.UNITS_ROLL, 41); + //// Pitch rate + public static final FlightDataType TYPE_PITCH_RATE = newType(trans.get("FlightDataType.TYPE_PITCH_RATE"), UnitGroup.UNITS_ROLL, 42); + //// Yaw rate + public static final FlightDataType TYPE_YAW_RATE = newType(trans.get("FlightDataType.TYPE_YAW_RATE"), UnitGroup.UNITS_ROLL, 43); + + + //// Stability information + //// Mass + public static final FlightDataType TYPE_MASS = newType(trans.get("FlightDataType.TYPE_MASS"), UnitGroup.UNITS_MASS, 50); + //// Longitudinal moment of inertia + public static final FlightDataType TYPE_LONGITUDINAL_INERTIA = newType(trans.get("FlightDataType.TYPE_LONGITUDINAL_INERTIA"), UnitGroup.UNITS_INERTIA, 51); + //// Rotational moment of inertia + public static final FlightDataType TYPE_ROTATIONAL_INERTIA = newType(trans.get("FlightDataType.TYPE_ROTATIONAL_INERTIA"), UnitGroup.UNITS_INERTIA, 52); + //// CP location + public static final FlightDataType TYPE_CP_LOCATION = newType(trans.get("FlightDataType.TYPE_CP_LOCATION"), UnitGroup.UNITS_LENGTH, 53); + //// CG location + public static final FlightDataType TYPE_CG_LOCATION = newType(trans.get("FlightDataType.TYPE_CG_LOCATION"), UnitGroup.UNITS_LENGTH, 54); + //// Stability margin calibers + public static final FlightDataType TYPE_STABILITY = newType(trans.get("FlightDataType.TYPE_STABILITY"), UnitGroup.UNITS_COEFFICIENT, 55); + + + //// Characteristic numbers + //// Mach number + public static final FlightDataType TYPE_MACH_NUMBER = newType(trans.get("FlightDataType.TYPE_MACH_NUMBER"), UnitGroup.UNITS_COEFFICIENT, 60); + //// Reynolds number + public static final FlightDataType TYPE_REYNOLDS_NUMBER = newType(trans.get("FlightDataType.TYPE_REYNOLDS_NUMBER"), UnitGroup.UNITS_COEFFICIENT, 61); + + + //// Thrust and drag + //// Thrust + public static final FlightDataType TYPE_THRUST_FORCE = newType(trans.get("FlightDataType.TYPE_THRUST_FORCE"), UnitGroup.UNITS_FORCE, 70); + //// Drag force + public static final FlightDataType TYPE_DRAG_FORCE = newType(trans.get("FlightDataType.TYPE_DRAG_FORCE"), UnitGroup.UNITS_FORCE, 71); + //// Drag coefficient + public static final FlightDataType TYPE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 72); + //// Axial drag coefficient + public static final FlightDataType TYPE_AXIAL_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_AXIAL_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 73); + + + //// Component drag coefficients + //// Friction drag coefficient + public static final FlightDataType TYPE_FRICTION_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_FRICTION_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 80); + //// Pressure drag coefficient + public static final FlightDataType TYPE_PRESSURE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_PRESSURE_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 81); + //// Base drag coefficient + public static final FlightDataType TYPE_BASE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_BASE_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 82); + + + //// Other coefficients + //// Normal force coefficient + public static final FlightDataType TYPE_NORMAL_FORCE_COEFF = newType(trans.get("FlightDataType.TYPE_NORMAL_FORCE_COEFF"), UnitGroup.UNITS_COEFFICIENT, 90); + //// Pitch moment coefficient + public static final FlightDataType TYPE_PITCH_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_PITCH_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 91); + //// Yaw moment coefficient + public static final FlightDataType TYPE_YAW_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_YAW_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 92); + //// Side force coefficient + public static final FlightDataType TYPE_SIDE_FORCE_COEFF = newType(trans.get("FlightDataType.TYPE_SIDE_FORCE_COEFF"), UnitGroup.UNITS_COEFFICIENT, 93); + //// Roll moment coefficient + public static final FlightDataType TYPE_ROLL_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 94); + //// Roll forcing coefficient + public static final FlightDataType TYPE_ROLL_FORCING_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_FORCING_COEFF"), UnitGroup.UNITS_COEFFICIENT, 95); + //// Roll damping coefficient + public static final FlightDataType TYPE_ROLL_DAMPING_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_DAMPING_COEFF"), UnitGroup.UNITS_COEFFICIENT, 96); + + //// Pitch damping coefficient + public static final FlightDataType TYPE_PITCH_DAMPING_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 97); + //// Yaw damping coefficient + public static final FlightDataType TYPE_YAW_DAMPING_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_YAW_DAMPING_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 98); + + //// Coriolis acceleration + public static final FlightDataType TYPE_CORIOLIS_ACCELERATION = newType(trans.get("FlightDataType.TYPE_CORIOLIS_ACCELERATION"), UnitGroup.UNITS_ACCELERATION, 99); + + + //// Reference length + area + //// Reference length + public static final FlightDataType TYPE_REFERENCE_LENGTH = newType(trans.get("FlightDataType.TYPE_REFERENCE_LENGTH"), UnitGroup.UNITS_LENGTH, 100); + //// Reference area + public static final FlightDataType TYPE_REFERENCE_AREA = newType(trans.get("FlightDataType.TYPE_REFERENCE_AREA"), UnitGroup.UNITS_AREA, 101); + + + //// Orientation + //// Vertical orientation (zenith) + public static final FlightDataType TYPE_ORIENTATION_THETA = newType(trans.get("FlightDataType.TYPE_ORIENTATION_THETA"), UnitGroup.UNITS_ANGLE, 106); + //// Lateral orientation (azimuth) + public static final FlightDataType TYPE_ORIENTATION_PHI = newType(trans.get("FlightDataType.TYPE_ORIENTATION_PHI"), UnitGroup.UNITS_ANGLE, 107); + + + //// Atmospheric conditions + //// Wind velocity + public static final FlightDataType TYPE_WIND_VELOCITY = newType(trans.get("FlightDataType.TYPE_WIND_VELOCITY"), UnitGroup.UNITS_VELOCITY, 110); + //// Air temperature + public static final FlightDataType TYPE_AIR_TEMPERATURE = newType(trans.get("FlightDataType.TYPE_AIR_TEMPERATURE"), UnitGroup.UNITS_TEMPERATURE, 111); + //// Air pressure + public static final FlightDataType TYPE_AIR_PRESSURE = newType(trans.get("FlightDataType.TYPE_AIR_PRESSURE"), UnitGroup.UNITS_PRESSURE, 112); + //// Speed of sound + public static final FlightDataType TYPE_SPEED_OF_SOUND = newType(trans.get("FlightDataType.TYPE_SPEED_OF_SOUND"), UnitGroup.UNITS_VELOCITY, 113); + + //// Simulation information + //// Simulation time step + public static final FlightDataType TYPE_TIME_STEP = newType(trans.get("FlightDataType.TYPE_TIME_STEP"), UnitGroup.UNITS_TIME_STEP, 200); + //// Computation time + public static final FlightDataType TYPE_COMPUTATION_TIME = newType(trans.get("FlightDataType.TYPE_COMPUTATION_TIME"), UnitGroup.UNITS_SHORT_TIME, 201); + + + + /** + * Return a {@link FlightDataType} based on a string description. This returns known data types + * if possible, or a new type otherwise. + * + * @param s the string description of the type. + * @param u the unit group the new type should belong to if a new group is created. + * @return a data type. + */ + public static synchronized FlightDataType getType(String s, UnitGroup u) { + FlightDataType type = EXISTING_TYPES.get(s.toLowerCase()); + if (type != null) { + return type; + } + type = newType(s, u, DEFAULT_PRIORITY); + return type; + } + + /** + * Used while initializing the class. + */ + private static synchronized FlightDataType newType(String s, UnitGroup u, int priority) { + FlightDataType type = new FlightDataType(s, u, priority); + EXISTING_TYPES.put(s.toLowerCase(), type); + return type; + } + + + private final String name; + private final UnitGroup units; + private final int priority; + private final int hashCode; + + + private FlightDataType(String typeName, UnitGroup units, int priority) { + if (typeName == null) + throw new IllegalArgumentException("typeName is null"); + if (units == null) + throw new IllegalArgumentException("units is null"); + this.name = typeName; + this.units = units; + this.priority = priority; + this.hashCode = this.name.toLowerCase().hashCode(); + } + + + + + public String getName() { + return name; + } + + public UnitGroup getUnitGroup() { + return units; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof FlightDataType)) + return false; + return this.name.equalsIgnoreCase(((FlightDataType) other).name); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public int compareTo(FlightDataType o) { + if (this.priority != o.priority) + return this.priority - o.priority; + return this.name.compareToIgnoreCase(o.name); + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/simulation/FlightEvent.java b/core/src/net/sf/openrocket/simulation/FlightEvent.java new file mode 100644 index 00000000..ead84eec --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/FlightEvent.java @@ -0,0 +1,171 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.startup.Application; + + +/** + * A class that defines an event during the flight of a rocket. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class FlightEvent implements Comparable<FlightEvent> { + private static final Translator trans = Application.getTranslator(); + + /** + * The type of the flight event. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + public enum Type { + /** + * Rocket launch. + */ + //// Launch + LAUNCH(trans.get("FlightEvent.Type.LAUNCH")), + /** + * Ignition of a motor. Source is the motor mount the motor of which has ignited, + * and the data is the MotorId of the motor instance. + */ + //// Motor ignition + IGNITION(trans.get("FlightEvent.Type.IGNITION")), + /** + * When the motor has lifted off the ground. + */ + //// Lift-off + LIFTOFF(trans.get("FlightEvent.Type.LIFTOFF")), + /** + * Launch rod has been cleared. + */ + //// Launch rod clearance + LAUNCHROD(trans.get("FlightEvent.Type.LAUNCHROD")), + /** + * Burnout of a motor. Source is the motor mount the motor of which has burnt out, + * and the data is the MotorId of the motor instance. + */ + //// Motor burnout + BURNOUT(trans.get("FlightEvent.Type.BURNOUT")), + /** + * Ejection charge of a motor fired. Source is the motor mount the motor of + * which has exploded its ejection charge, and data is the MotorId of the motor instance. + */ + //// Ejection charge + EJECTION_CHARGE(trans.get("FlightEvent.Type.EJECTION_CHARGE")), + /** + * Separation of a stage. Source is the stage which has separated all lower stages. + */ + //// Stage separation + STAGE_SEPARATION(trans.get("FlightEvent.Type.STAGE_SEPARATION")), + /** + * Apogee has been reached. + */ + //// Apogee + APOGEE(trans.get("FlightEvent.Type.APOGEE")), + /** + * Opening of a recovery device. Source is the RecoveryComponent which has opened. + */ + //// Recovery device deployment + RECOVERY_DEVICE_DEPLOYMENT(trans.get("FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT")), + /** + * Ground has been hit after flight. + */ + //// Ground hit + GROUND_HIT(trans.get("FlightEvent.Type.GROUND_HIT")), + + /** + * End of simulation. Placing this to the queue will end the simulation. + */ + //// Simulation end + SIMULATION_END(trans.get("FlightEvent.Type.SIMULATION_END")), + + /** + * A change in altitude has occurred. Data is a <code>Pair<Double,Double></code> + * which contains the old and new altitudes. + */ + //// Altitude change + ALTITUDE(trans.get("FlightEvent.Type.ALTITUDE")); + + private final String name; + + private Type(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + private final Type type; + private final double time; + private final RocketComponent source; + private final Object data; + + + public FlightEvent(Type type, double time) { + this(type, time, null); + } + + public FlightEvent(Type type, double time, RocketComponent source) { + this(type, time, source, null); + } + + public FlightEvent(Type type, double time, RocketComponent source, Object data) { + this.type = type; + this.time = time; + this.source = source; + this.data = data; + } + + + + public Type getType() { + return type; + } + + public double getTime() { + return time; + } + + public RocketComponent getSource() { + return source; + } + + public Object getData() { + return data; + } + + + /** + * Return a new FlightEvent with the same information as the current event + * but with <code>null</code> source. This is used to avoid memory leakage by + * retaining references to obsolete components. + * + * @return a new FlightEvent with same type, time and data. + */ + public FlightEvent resetSourceAndData() { + return new FlightEvent(type, time, null, null); + } + + + /** + * Compares this event to another event depending on the event time. Secondary + * sorting is performed based on the event type ordinal. + */ + @Override + public int compareTo(FlightEvent o) { + if (this.time < o.time) + return -1; + if (this.time > o.time) + return 1; + + return this.type.ordinal() - o.type.ordinal(); + } + + @Override + public String toString() { + return "FlightEvent[type=" + type.name() + ",time=" + time + ",source=" + source + "]"; + } +} diff --git a/core/src/net/sf/openrocket/simulation/MassData.java b/core/src/net/sf/openrocket/simulation/MassData.java new file mode 100644 index 00000000..1d910df0 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/MassData.java @@ -0,0 +1,69 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +/** + * An immutable value object containing the mass data of a rocket. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class MassData { + + private final Coordinate cg; + private final double longitudinalInertia; + private final double rotationalInertia; + + + public MassData(Coordinate cg, double longitudinalInertia, double rotationalInertia) { + if (cg == null) { + throw new IllegalArgumentException("cg is null"); + } + this.cg = cg; + this.longitudinalInertia = longitudinalInertia; + this.rotationalInertia = rotationalInertia; + } + + + + + public Coordinate getCG() { + return cg; + } + + public double getLongitudinalInertia() { + return longitudinalInertia; + } + + public double getRotationalInertia() { + return rotationalInertia; + } + + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof MassData)) + return false; + + MassData other = (MassData) obj; + return (this.cg.equals(other.cg) && MathUtil.equals(this.longitudinalInertia, other.longitudinalInertia) && + MathUtil.equals(this.rotationalInertia, other.rotationalInertia)); + } + + + @Override + public int hashCode() { + return (int) (cg.hashCode() ^ Double.doubleToLongBits(longitudinalInertia) ^ Double.doubleToLongBits(rotationalInertia)); + } + + + @Override + public String toString() { + return "MassData [cg=" + cg + ", longitudinalInertia=" + longitudinalInertia + + ", rotationalInertia=" + rotationalInertia + "]"; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/RK4SimulationStatus.java b/core/src/net/sf/openrocket/simulation/RK4SimulationStatus.java new file mode 100644 index 00000000..505ff77c --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/RK4SimulationStatus.java @@ -0,0 +1,74 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.util.Coordinate; + +public class RK4SimulationStatus extends SimulationStatus { + private Coordinate launchRodDirection; + + private double previousAcceleration = 0; + private AtmosphericConditions previousAtmosphericConditions; + + // Used for determining when to store aerodynamic computation warnings: + private double maxZVelocity = 0; + private double startWarningTime = -1; + + + public void setLaunchRodDirection(Coordinate launchRodDirection) { + this.launchRodDirection = launchRodDirection; + } + + + public Coordinate getLaunchRodDirection() { + return launchRodDirection; + } + + + + public double getPreviousAcceleration() { + return previousAcceleration; + } + + + public void setPreviousAcceleration(double previousAcceleration) { + this.previousAcceleration = previousAcceleration; + } + + + public AtmosphericConditions getPreviousAtmosphericConditions() { + return previousAtmosphericConditions; + } + + + public void setPreviousAtmosphericConditions( + AtmosphericConditions previousAtmosphericConditions) { + this.previousAtmosphericConditions = previousAtmosphericConditions; + } + + + public double getMaxZVelocity() { + return maxZVelocity; + } + + + public void setMaxZVelocity(double maxZVelocity) { + this.maxZVelocity = maxZVelocity; + } + + + public double getStartWarningTime() { + return startWarningTime; + } + + + public void setStartWarningTime(double startWarningTime) { + this.startWarningTime = startWarningTime; + } + + + @Override + public RK4SimulationStatus clone() { + return (RK4SimulationStatus) super.clone(); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java b/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java new file mode 100644 index 00000000..6b7aad9d --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/RK4SimulationStepper.java @@ -0,0 +1,725 @@ +package net.sf.openrocket.simulation; + +import java.util.Arrays; +import java.util.Random; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.simulation.exception.SimulationCalculationException; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.GeodeticComputationStrategy; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Quaternion; +import net.sf.openrocket.util.Rotation2D; +import net.sf.openrocket.util.WorldCoordinate; + +public class RK4SimulationStepper extends AbstractSimulationStepper { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + + /** Random value with which to XOR the random seed value */ + private static final int SEED_RANDOMIZATION = 0x23E3A01F; + + + /** + * A recommended reasonably accurate time step. + */ + public static final double RECOMMENDED_TIME_STEP = 0.05; + + /** + * A recommended maximum angle step value. + */ + public static final double RECOMMENDED_ANGLE_STEP = 3 * Math.PI / 180; + + /** + * A random amount that is added to pitch and yaw coefficients, plus or minus. + */ + public static final double PITCH_YAW_RANDOM = 0.0005; + + /** + * Maximum roll step allowed. This is selected as an uneven division of the full + * circle so that the simulation will sample the most wind directions + */ + private static final double MAX_ROLL_STEP_ANGLE = 2 * 28.32 * Math.PI / 180; + // private static final double MAX_ROLL_STEP_ANGLE = 8.32 * Math.PI/180; + + private static final double MAX_ROLL_RATE_CHANGE = 2 * Math.PI / 180; + private static final double MAX_PITCH_CHANGE = 4 * Math.PI / 180; + + private static final double MIN_TIME_STEP = 0.001; + + + private Random random; + + + + + @Override + public RK4SimulationStatus initialize(SimulationStatus original) { + + RK4SimulationStatus status = new RK4SimulationStatus(); + + status.copyFrom(original); + + SimulationConditions sim = original.getSimulationConditions(); + + status.setLaunchRodDirection(new Coordinate( + Math.sin(sim.getLaunchRodAngle()) * Math.cos(sim.getLaunchRodDirection()), + Math.sin(sim.getLaunchRodAngle()) * Math.sin(sim.getLaunchRodDirection()), + Math.cos(sim.getLaunchRodAngle()) + )); + + this.random = new Random(original.getSimulationConditions().getRandomSeed() ^ SEED_RANDOMIZATION); + + return status; + } + + + + + @Override + public void step(SimulationStatus simulationStatus, double maxTimeStep) throws SimulationException { + + RK4SimulationStatus status = (RK4SimulationStatus) simulationStatus; + DataStore store = new DataStore(); + + //////// Perform RK4 integration: //////// + + RK4SimulationStatus status2; + RK4Parameters k1, k2, k3, k4; + + /* + * Start with previous time step which is used to compute the initial thrust estimate. + * Don't make it longer than maxTimeStep, but at least MIN_TIME_STEP. + */ + store.timestep = status.getPreviousTimeStep(); + store.timestep = MathUtil.max(MathUtil.min(store.timestep, maxTimeStep), MIN_TIME_STEP); + checkNaN(store.timestep); + + /* + * Compute the initial thrust estimate. This is used for the first time step computation. + */ + store.thrustForce = calculateThrust(status, store.timestep, status.getPreviousAcceleration(), + status.getPreviousAtmosphericConditions(), false); + + + /* + * Perform RK4 integration. Decide the time step length after the first step. + */ + + //// First position, k1 = f(t, y) + + k1 = computeParameters(status, store); + + /* + * Select the actual time step to use. It is the minimum of the following: + * dt[0]: the user-specified time step (or 1/5th of it if still on the launch rod) + * dt[1]: the value of maxTimeStep + * dt[2]: the maximum pitch step angle limit + * dt[3]: the maximum roll step angle limit + * dt[4]: the maximum roll rate change limit + * dt[5]: the maximum pitch change limit + * dt[6]: 1/10th of the launch rod length if still on the launch rod + * dt[7]: 1.50 times the previous time step + * + * The limits #5 and #6 are required since near the steady-state roll rate the roll rate + * may oscillate significantly even between the sub-steps of the RK4 integration. + * + * The step is still at least 1/20th of the user-selected time step. + */ + double[] dt = new double[8]; + Arrays.fill(dt, Double.MAX_VALUE); + + dt[0] = status.getSimulationConditions().getTimeStep(); + dt[1] = maxTimeStep; + dt[2] = status.getSimulationConditions().getMaximumAngleStep() / store.lateralPitchRate; + dt[3] = Math.abs(MAX_ROLL_STEP_ANGLE / store.flightConditions.getRollRate()); + dt[4] = Math.abs(MAX_ROLL_RATE_CHANGE / store.rollAcceleration); + dt[5] = Math.abs(MAX_PITCH_CHANGE / store.lateralPitchAcceleration); + if (!status.isLaunchRodCleared()) { + dt[0] /= 5.0; + dt[6] = status.getSimulationConditions().getLaunchRodLength() / k1.v.length() / 10; + } + dt[7] = 1.5 * status.getPreviousTimeStep(); + + store.timestep = Double.MAX_VALUE; + int limitingValue = -1; + for (int i = 0; i < dt.length; i++) { + if (dt[i] < store.timestep) { + store.timestep = dt[i]; + limitingValue = i; + } + } + + double minTimeStep = status.getSimulationConditions().getTimeStep() / 20; + if (store.timestep < minTimeStep) { + log.verbose("Too small time step " + store.timestep + " (limiting factor " + limitingValue + "), using " + + minTimeStep + " instead."); + store.timestep = minTimeStep; + } else { + log.verbose("Selected time step " + store.timestep + " (limiting factor " + limitingValue + ")"); + } + checkNaN(store.timestep); + + /* + * Compute the correct thrust for this time step. If the original thrust estimate differs more + * than 10% from the true value then recompute the RK4 step 1. The 10% error in step 1 is + * diminished by it affecting only 1/6th of the total, so it's an acceptable error. + */ + double thrustEstimate = store.thrustForce; + store.thrustForce = calculateThrust(status, store.timestep, store.longitudinalAcceleration, + store.atmosphericConditions, true); + double thrustDiff = Math.abs(store.thrustForce - thrustEstimate); + // Log if difference over 1%, recompute if over 10% + if (thrustDiff > 0.01 * thrustEstimate) { + if (thrustDiff > 0.1 * thrustEstimate + 0.001) { + log.debug("Thrust estimate differs from correct value by " + + (Math.rint(1000 * (thrustDiff + 0.000001) / thrustEstimate) / 10.0) + "%," + + " estimate=" + thrustEstimate + + " correct=" + store.thrustForce + + " timestep=" + store.timestep + + ", recomputing k1 parameters"); + k1 = computeParameters(status, store); + } else { + log.verbose("Thrust estimate differs from correct value by " + + (Math.rint(1000 * (thrustDiff + 0.000001) / thrustEstimate) / 10.0) + "%," + + " estimate=" + thrustEstimate + + " correct=" + store.thrustForce + + " timestep=" + store.timestep + + ", error acceptable"); + } + } + + // Store data + // TODO: MEDIUM: Store acceleration etc of entire RK4 step, store should be cloned or something... + storeData(status, store); + + + //// Second position, k2 = f(t + h/2, y + k1*h/2) + + status2 = status.clone(); + status2.setSimulationTime(status.getSimulationTime() + store.timestep / 2); + status2.setRocketPosition(status.getRocketPosition().add(k1.v.multiply(store.timestep / 2))); + status2.setRocketVelocity(status.getRocketVelocity().add(k1.a.multiply(store.timestep / 2))); + status2.setRocketOrientationQuaternion(status.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(k1.rv.multiply(store.timestep / 2)))); + status2.setRocketRotationVelocity(status.getRocketRotationVelocity().add(k1.ra.multiply(store.timestep / 2))); + + k2 = computeParameters(status2, store); + + + //// Third position, k3 = f(t + h/2, y + k2*h/2) + + status2 = status.clone(); + status2.setSimulationTime(status.getSimulationTime() + store.timestep / 2); + status2.setRocketPosition(status.getRocketPosition().add(k2.v.multiply(store.timestep / 2))); + status2.setRocketVelocity(status.getRocketVelocity().add(k2.a.multiply(store.timestep / 2))); + status2.setRocketOrientationQuaternion(status2.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(k2.rv.multiply(store.timestep / 2)))); + status2.setRocketRotationVelocity(status.getRocketRotationVelocity().add(k2.ra.multiply(store.timestep / 2))); + + k3 = computeParameters(status2, store); + + + //// Fourth position, k4 = f(t + h, y + k3*h) + + status2 = status.clone(); + status2.setSimulationTime(status.getSimulationTime() + store.timestep); + status2.setRocketPosition(status.getRocketPosition().add(k3.v.multiply(store.timestep))); + status2.setRocketVelocity(status.getRocketVelocity().add(k3.a.multiply(store.timestep))); + status2.setRocketOrientationQuaternion(status2.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(k3.rv.multiply(store.timestep)))); + status2.setRocketRotationVelocity(status.getRocketRotationVelocity().add(k3.ra.multiply(store.timestep))); + + k4 = computeParameters(status2, store); + + + //// Sum all together, y(n+1) = y(n) + h*(k1 + 2*k2 + 2*k3 + k4)/6 + + + + Coordinate deltaV, deltaP, deltaR, deltaO; + deltaV = k2.a.add(k3.a).multiply(2).add(k1.a).add(k4.a).multiply(store.timestep / 6); + deltaP = k2.v.add(k3.v).multiply(2).add(k1.v).add(k4.v).multiply(store.timestep / 6); + deltaR = k2.ra.add(k3.ra).multiply(2).add(k1.ra).add(k4.ra).multiply(store.timestep / 6); + deltaO = k2.rv.add(k3.rv).multiply(2).add(k1.rv).add(k4.rv).multiply(store.timestep / 6); + + + + status.setRocketVelocity(status.getRocketVelocity().add(deltaV)); + status.setRocketPosition(status.getRocketPosition().add(deltaP)); + status.setRocketRotationVelocity(status.getRocketRotationVelocity().add(deltaR)); + status.setRocketOrientationQuaternion(status.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(deltaO)).normalizeIfNecessary()); + + WorldCoordinate w = status.getSimulationConditions().getLaunchSite(); + w = status.getSimulationConditions().getGeodeticComputation().addCoordinate(w, status.getRocketPosition()); + status.setRocketWorldPosition(w); + + status.setSimulationTime(status.getSimulationTime() + store.timestep); + + status.setPreviousTimeStep(store.timestep); + + // Verify that values don't run out of range + if (status.getRocketVelocity().length2() > 1e18 || + status.getRocketPosition().length2() > 1e18 || + status.getRocketRotationVelocity().length2() > 1e18) { + throw new SimulationCalculationException(trans.get("error.valuesTooLarge")); + } + } + + + + + + private RK4Parameters computeParameters(RK4SimulationStatus status, DataStore dataStore) + throws SimulationException { + RK4Parameters params = new RK4Parameters(); + + // if (dataStore == null) { + // dataStore = new DataStore(); + // } + + calculateAcceleration(status, dataStore); + params.a = dataStore.linearAcceleration; + params.ra = dataStore.angularAcceleration; + params.v = status.getRocketVelocity(); + params.rv = status.getRocketRotationVelocity(); + + checkNaN(params.a); + checkNaN(params.ra); + checkNaN(params.v); + checkNaN(params.rv); + + return params; + } + + + + + + /** + * Calculate the linear and angular acceleration at the given status. The results + * are stored in the fields {@link #linearAcceleration} and {@link #angularAcceleration}. + * + * @param status the status of the rocket. + * @throws SimulationException + */ + private void calculateAcceleration(RK4SimulationStatus status, DataStore store) throws SimulationException { + + // Call pre-listeners + store.accelerationData = SimulationListenerHelper.firePreAccelerationCalculation(status); + if (store.accelerationData != null) { + return; + } + + // Compute the forces affecting the rocket + calculateForces(status, store); + + // Calculate mass data + store.massData = calculateMassData(status); + + + // Calculate the forces from the aerodynamic coefficients + + double dynP = (0.5 * store.flightConditions.getAtmosphericConditions().getDensity() * + MathUtil.pow2(store.flightConditions.getVelocity())); + double refArea = store.flightConditions.getRefArea(); + double refLength = store.flightConditions.getRefLength(); + + + // Linear forces in rocket coordinates + store.dragForce = store.forces.getCaxial() * dynP * refArea; + double fN = store.forces.getCN() * dynP * refArea; + double fSide = store.forces.getCside() * dynP * refArea; + + double forceZ = store.thrustForce - store.dragForce; + + store.linearAcceleration = new Coordinate(-fN / store.massData.getCG().weight, + -fSide / store.massData.getCG().weight, + forceZ / store.massData.getCG().weight); + + store.linearAcceleration = store.thetaRotation.rotateZ(store.linearAcceleration); + + // Convert into rocket world coordinates + store.linearAcceleration = status.getRocketOrientationQuaternion().rotate(store.linearAcceleration); + + // add effect of gravity + store.gravity = modelGravity(status); + store.linearAcceleration = store.linearAcceleration.sub(0, 0, store.gravity); + + // add effect of Coriolis acceleration + store.coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation() + .getCoriolisAcceleration(status.getRocketWorldPosition(), status.getRocketVelocity()); + store.linearAcceleration = store.linearAcceleration.add(store.coriolisAcceleration); + + // If still on the launch rod, project acceleration onto launch rod direction and + // set angular acceleration to zero. + if (!status.isLaunchRodCleared()) { + + store.linearAcceleration = status.getLaunchRodDirection().multiply( + store.linearAcceleration.dot(status.getLaunchRodDirection())); + store.angularAcceleration = Coordinate.NUL; + store.rollAcceleration = 0; + store.lateralPitchAcceleration = 0; + + } else { + + // Shift moments to CG + double Cm = store.forces.getCm() - store.forces.getCN() * store.massData.getCG().x / refLength; + double Cyaw = store.forces.getCyaw() - store.forces.getCside() * store.massData.getCG().x / refLength; + + // Compute moments + double momX = -Cyaw * dynP * refArea * refLength; + double momY = Cm * dynP * refArea * refLength; + double momZ = store.forces.getCroll() * dynP * refArea * refLength; + + // Compute acceleration in rocket coordinates + store.angularAcceleration = new Coordinate(momX / store.massData.getLongitudinalInertia(), + momY / store.massData.getLongitudinalInertia(), + momZ / store.massData.getRotationalInertia()); + + store.rollAcceleration = store.angularAcceleration.z; + // TODO: LOW: This should be hypot, but does it matter? + store.lateralPitchAcceleration = MathUtil.max(Math.abs(store.angularAcceleration.x), + Math.abs(store.angularAcceleration.y)); + + store.angularAcceleration = store.thetaRotation.rotateZ(store.angularAcceleration); + + // Convert to world coordinates + store.angularAcceleration = status.getRocketOrientationQuaternion().rotate(store.angularAcceleration); + + } + + // Call post-listeners + store.accelerationData = SimulationListenerHelper.firePostAccelerationCalculation(status, store.accelerationData); + } + + + /** + * Calculate the aerodynamic forces into the data store. This method also handles + * whether to include aerodynamic computation warnings or not. + */ + private void calculateForces(RK4SimulationStatus status, DataStore store) throws SimulationException { + + // Call pre-listeners + store.forces = SimulationListenerHelper.firePreAerodynamicCalculation(status); + if (store.forces != null) { + return; + } + + // Compute flight conditions + calculateFlightConditions(status, store); + + /* + * Check whether to store warnings or not. Warnings are ignored when on the + * launch rod or 0.25 seconds after departure, and when the velocity has dropped + * below 20% of the max. velocity. + */ + WarningSet warnings = status.getWarnings(); + status.setMaxZVelocity(MathUtil.max(status.getMaxZVelocity(), status.getRocketVelocity().z)); + + if (!status.isLaunchRodCleared()) { + warnings = null; + } else { + if (status.getRocketVelocity().z < 0.2 * status.getMaxZVelocity()) + warnings = null; + if (status.getStartWarningTime() < 0) + status.setStartWarningTime(status.getSimulationTime() + 0.25); + } + if (status.getSimulationTime() < status.getStartWarningTime()) + warnings = null; + + + // Calculate aerodynamic forces + store.forces = status.getSimulationConditions().getAerodynamicCalculator() + .getAerodynamicForces(status.getConfiguration(), store.flightConditions, warnings); + + + // Add very small randomization to yaw & pitch moments to prevent over-perfect flight + // TODO: HIGH: This should rather be performed as a listener + store.forces.setCm(store.forces.getCm() + (PITCH_YAW_RANDOM * 2 * (random.nextDouble() - 0.5))); + store.forces.setCyaw(store.forces.getCyaw() + (PITCH_YAW_RANDOM * 2 * (random.nextDouble() - 0.5))); + + + // Call post-listeners + store.forces = SimulationListenerHelper.firePostAerodynamicCalculation(status, store.forces); + } + + + + /** + * Calculate and return the flight conditions for the current rocket status. + * Listeners can override these if necessary. + * <p> + * Additionally the fields thetaRotation and lateralPitchRate are defined in + * the data store, and can be used after calling this method. + */ + private void calculateFlightConditions(RK4SimulationStatus status, DataStore store) + throws SimulationException { + + // Call pre listeners, allow complete override + store.flightConditions = SimulationListenerHelper.firePreFlightConditions( + status); + if (store.flightConditions != null) { + // Compute the store values + store.thetaRotation = new Rotation2D(store.flightConditions.getTheta()); + store.lateralPitchRate = Math.hypot(store.flightConditions.getPitchRate(), store.flightConditions.getYawRate()); + return; + } + + + + //// Atmospheric conditions + AtmosphericConditions atmosphere = modelAtmosphericConditions(status); + store.flightConditions = new FlightConditions(status.getConfiguration()); + store.flightConditions.setAtmosphericConditions(atmosphere); + + + //// Local wind speed and direction + Coordinate windSpeed = modelWindVelocity(status); + Coordinate airSpeed = status.getRocketVelocity().add(windSpeed); + airSpeed = status.getRocketOrientationQuaternion().invRotate(airSpeed); + + + // Lateral direction: + double len = MathUtil.hypot(airSpeed.x, airSpeed.y); + if (len > 0.0001) { + store.thetaRotation = new Rotation2D(airSpeed.y / len, airSpeed.x / len); + store.flightConditions.setTheta(Math.atan2(airSpeed.y, airSpeed.x)); + } else { + store.thetaRotation = Rotation2D.ID; + store.flightConditions.setTheta(0); + } + + double velocity = airSpeed.length(); + store.flightConditions.setVelocity(velocity); + if (velocity > 0.01) { + // aoa must be calculated from the monotonous cosine + // sine can be calculated by a simple division + store.flightConditions.setAOA(Math.acos(airSpeed.z / velocity), len / velocity); + } else { + store.flightConditions.setAOA(0); + } + + + // Roll, pitch and yaw rate + Coordinate rot = status.getRocketOrientationQuaternion().invRotate(status.getRocketRotationVelocity()); + rot = store.thetaRotation.invRotateZ(rot); + + store.flightConditions.setRollRate(rot.z); + if (len < 0.001) { + store.flightConditions.setPitchRate(0); + store.flightConditions.setYawRate(0); + store.lateralPitchRate = 0; + } else { + store.flightConditions.setPitchRate(rot.y); + store.flightConditions.setYawRate(rot.x); + // TODO: LOW: set this as power of two? + store.lateralPitchRate = MathUtil.hypot(rot.x, rot.y); + } + + + // Call post listeners + FlightConditions c = SimulationListenerHelper.firePostFlightConditions( + status, store.flightConditions); + if (c != store.flightConditions) { + // Listeners changed the values, recalculate data store + store.flightConditions = c; + store.thetaRotation = new Rotation2D(store.flightConditions.getTheta()); + store.lateralPitchRate = Math.hypot(store.flightConditions.getPitchRate(), store.flightConditions.getYawRate()); + } + + } + + + + private void storeData(RK4SimulationStatus status, DataStore store) { + + FlightDataBranch data = status.getFlightData(); + boolean extra = status.getSimulationConditions().isCalculateExtras(); + + data.addPoint(); + data.setValue(FlightDataType.TYPE_TIME, status.getSimulationTime()); + data.setValue(FlightDataType.TYPE_ALTITUDE, status.getRocketPosition().z); + data.setValue(FlightDataType.TYPE_POSITION_X, status.getRocketPosition().x); + data.setValue(FlightDataType.TYPE_POSITION_Y, status.getRocketPosition().y); + + data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); + data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); + if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { + data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, store.coriolisAcceleration.length()); + } + + if (extra) { + data.setValue(FlightDataType.TYPE_POSITION_XY, + MathUtil.hypot(status.getRocketPosition().x, status.getRocketPosition().y)); + data.setValue(FlightDataType.TYPE_POSITION_DIRECTION, + Math.atan2(status.getRocketPosition().y, status.getRocketPosition().x)); + + data.setValue(FlightDataType.TYPE_VELOCITY_XY, + MathUtil.hypot(status.getRocketVelocity().x, status.getRocketVelocity().y)); + + if (store.linearAcceleration != null) { + data.setValue(FlightDataType.TYPE_ACCELERATION_XY, + MathUtil.hypot(store.linearAcceleration.x, store.linearAcceleration.y)); + + data.setValue(FlightDataType.TYPE_ACCELERATION_TOTAL, store.linearAcceleration.length()); + } + + if (store.flightConditions != null) { + double Re = (store.flightConditions.getVelocity() * + status.getConfiguration().getLength() / + store.flightConditions.getAtmosphericConditions().getKinematicViscosity()); + data.setValue(FlightDataType.TYPE_REYNOLDS_NUMBER, Re); + } + } + + data.setValue(FlightDataType.TYPE_VELOCITY_Z, status.getRocketVelocity().z); + if (store.linearAcceleration != null) { + data.setValue(FlightDataType.TYPE_ACCELERATION_Z, store.linearAcceleration.z); + } + + if (store.flightConditions != null) { + data.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, status.getRocketVelocity().length()); + data.setValue(FlightDataType.TYPE_MACH_NUMBER, store.flightConditions.getMach()); + } + + if (store.massData != null) { + data.setValue(FlightDataType.TYPE_CG_LOCATION, store.massData.getCG().x); + } + if (status.isLaunchRodCleared()) { + // Don't include CP and stability with huge launch AOA + if (store.forces != null) { + data.setValue(FlightDataType.TYPE_CP_LOCATION, store.forces.getCP().x); + } + if (store.forces != null && store.flightConditions != null && store.massData != null) { + data.setValue(FlightDataType.TYPE_STABILITY, + (store.forces.getCP().x - store.massData.getCG().x) / store.flightConditions.getRefLength()); + } + } + if (store.massData != null) { + data.setValue(FlightDataType.TYPE_MASS, store.massData.getCG().weight); + data.setValue(FlightDataType.TYPE_LONGITUDINAL_INERTIA, store.massData.getLongitudinalInertia()); + data.setValue(FlightDataType.TYPE_ROTATIONAL_INERTIA, store.massData.getRotationalInertia()); + } + + data.setValue(FlightDataType.TYPE_THRUST_FORCE, store.thrustForce); + data.setValue(FlightDataType.TYPE_DRAG_FORCE, store.dragForce); + + if (status.isLaunchRodCleared() && store.forces != null) { + if (store.massData != null && store.flightConditions != null) { + data.setValue(FlightDataType.TYPE_PITCH_MOMENT_COEFF, + store.forces.getCm() - store.forces.getCN() * store.massData.getCG().x / store.flightConditions.getRefLength()); + data.setValue(FlightDataType.TYPE_YAW_MOMENT_COEFF, + store.forces.getCyaw() - store.forces.getCside() * store.massData.getCG().x / store.flightConditions.getRefLength()); + } + data.setValue(FlightDataType.TYPE_NORMAL_FORCE_COEFF, store.forces.getCN()); + data.setValue(FlightDataType.TYPE_SIDE_FORCE_COEFF, store.forces.getCside()); + data.setValue(FlightDataType.TYPE_ROLL_MOMENT_COEFF, store.forces.getCroll()); + data.setValue(FlightDataType.TYPE_ROLL_FORCING_COEFF, store.forces.getCrollForce()); + data.setValue(FlightDataType.TYPE_ROLL_DAMPING_COEFF, store.forces.getCrollDamp()); + data.setValue(FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF, + store.forces.getPitchDampingMoment()); + } + + if (store.forces != null) { + data.setValue(FlightDataType.TYPE_DRAG_COEFF, store.forces.getCD()); + data.setValue(FlightDataType.TYPE_AXIAL_DRAG_COEFF, store.forces.getCaxial()); + data.setValue(FlightDataType.TYPE_FRICTION_DRAG_COEFF, store.forces.getFrictionCD()); + data.setValue(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, store.forces.getPressureCD()); + data.setValue(FlightDataType.TYPE_BASE_DRAG_COEFF, store.forces.getBaseCD()); + } + + if (store.flightConditions != null) { + data.setValue(FlightDataType.TYPE_REFERENCE_LENGTH, store.flightConditions.getRefLength()); + data.setValue(FlightDataType.TYPE_REFERENCE_AREA, store.flightConditions.getRefArea()); + + data.setValue(FlightDataType.TYPE_PITCH_RATE, store.flightConditions.getPitchRate()); + data.setValue(FlightDataType.TYPE_YAW_RATE, store.flightConditions.getYawRate()); + data.setValue(FlightDataType.TYPE_ROLL_RATE, store.flightConditions.getRollRate()); + + data.setValue(FlightDataType.TYPE_AOA, store.flightConditions.getAOA()); + } + + + if (extra) { + Coordinate c = status.getRocketOrientationQuaternion().rotateZ(); + double theta = Math.atan2(c.z, MathUtil.hypot(c.x, c.y)); + double phi = Math.atan2(c.y, c.x); + if (phi < -(Math.PI - 0.0001)) + phi = Math.PI; + data.setValue(FlightDataType.TYPE_ORIENTATION_THETA, theta); + data.setValue(FlightDataType.TYPE_ORIENTATION_PHI, phi); + } + + data.setValue(FlightDataType.TYPE_WIND_VELOCITY, store.windSpeed); + + if (store.flightConditions != null) { + data.setValue(FlightDataType.TYPE_AIR_TEMPERATURE, + store.flightConditions.getAtmosphericConditions().getTemperature()); + data.setValue(FlightDataType.TYPE_AIR_PRESSURE, + store.flightConditions.getAtmosphericConditions().getPressure()); + data.setValue(FlightDataType.TYPE_SPEED_OF_SOUND, + store.flightConditions.getAtmosphericConditions().getMachSpeed()); + } + + + data.setValue(FlightDataType.TYPE_TIME_STEP, store.timestep); + data.setValue(FlightDataType.TYPE_COMPUTATION_TIME, + (System.nanoTime() - status.getSimulationStartWallTime()) / 1000000000.0); + } + + + + + private static class RK4Parameters { + /** Linear acceleration */ + public Coordinate a; + /** Linear velocity */ + public Coordinate v; + /** Rotational acceleration */ + public Coordinate ra; + /** Rotational velocity */ + public Coordinate rv; + } + + private static class DataStore { + public double timestep = Double.NaN; + + public AccelerationData accelerationData; + + public AtmosphericConditions atmosphericConditions; + + public FlightConditions flightConditions; + + public double longitudinalAcceleration = Double.NaN; + + public MassData massData; + + public Coordinate coriolisAcceleration; + + public Coordinate linearAcceleration; + public Coordinate angularAcceleration; + + // set by calculateFlightConditions and calculateAcceleration: + public AerodynamicForces forces; + public double windSpeed = Double.NaN; + public double gravity = Double.NaN; + public double thrustForce = Double.NaN; + public double dragForce = Double.NaN; + public double lateralPitchRate = Double.NaN; + + public double rollAcceleration = Double.NaN; + public double lateralPitchAcceleration = Double.NaN; + + public Rotation2D thetaRotation; + + } + +} diff --git a/core/src/net/sf/openrocket/simulation/SimulationConditions.java b/core/src/net/sf/openrocket/simulation/SimulationConditions.java new file mode 100644 index 00000000..f2412717 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/SimulationConditions.java @@ -0,0 +1,293 @@ +package net.sf.openrocket.simulation; + +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.aerodynamics.AerodynamicCalculator; +import net.sf.openrocket.masscalc.MassCalculator; +import net.sf.openrocket.models.atmosphere.AtmosphericModel; +import net.sf.openrocket.models.gravity.GravityModel; +import net.sf.openrocket.models.wind.WindModel; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.simulation.listeners.SimulationListener; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.GeodeticComputationStrategy; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.WorldCoordinate; + +/** + * A holder class for the simulation conditions. These include conditions that do not change + * during the flight of a rocket, for example launch rod parameters, atmospheric models, + * aerodynamic calculators etc. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationConditions implements Monitorable, Cloneable { + + private Rocket rocket; + private String motorID = null; + + + private double launchRodLength = 1; + + /** Launch rod angle >= 0, radians from vertical */ + private double launchRodAngle = 0; + + /** Launch rod direction, 0 = upwind, PI = downwind. */ + private double launchRodDirection = 0; + + // TODO: Depreciate these and use worldCoordinate only. + //private double launchAltitude = 0; + //private double launchLatitude = 45; + //private double launchLongitude = 0; + private WorldCoordinate launchSite = new WorldCoordinate(0, 0, 0); + private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL; + + + private WindModel windModel; + private AtmosphericModel atmosphericModel; + private GravityModel gravityModel; + + private AerodynamicCalculator aerodynamicCalculator; + private MassCalculator massCalculator; + + + private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP; + private double maximumAngleStep = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP; + + /* Whether to calculate additional data or only primary simulation figures */ + private boolean calculateExtras = true; + + + private List<SimulationListener> simulationListeners = new ArrayList<SimulationListener>(); + + + private int randomSeed = 0; + + private int modID = 0; + private int modIDadd = 0; + + + + + public AerodynamicCalculator getAerodynamicCalculator() { + return aerodynamicCalculator; + } + + + public void setAerodynamicCalculator(AerodynamicCalculator aerodynamicCalculator) { + if (this.aerodynamicCalculator != null) + this.modIDadd += this.aerodynamicCalculator.getModID(); + this.modID++; + this.aerodynamicCalculator = aerodynamicCalculator; + } + + public MassCalculator getMassCalculator() { + return massCalculator; + } + + + public void setMassCalculator(MassCalculator massCalculator) { + if (this.massCalculator != null) + this.modIDadd += this.massCalculator.getModID(); + this.modID++; + this.massCalculator = massCalculator; + } + + + public Rocket getRocket() { + return rocket; + } + + + public void setRocket(Rocket rocket) { + if (this.rocket != null) + this.modIDadd += this.rocket.getModID(); + this.modID++; + this.rocket = rocket; + } + + + public String getMotorConfigurationID() { + return motorID; + } + + + public void setMotorConfigurationID(String motorID) { + this.motorID = motorID; + this.modID++; + } + + + public double getLaunchRodLength() { + return launchRodLength; + } + + + public void setLaunchRodLength(double launchRodLength) { + this.launchRodLength = launchRodLength; + this.modID++; + } + + + public double getLaunchRodAngle() { + return launchRodAngle; + } + + + public void setLaunchRodAngle(double launchRodAngle) { + this.launchRodAngle = launchRodAngle; + this.modID++; + } + + + public double getLaunchRodDirection() { + return launchRodDirection; + } + + + public void setLaunchRodDirection(double launchRodDirection) { + this.launchRodDirection = launchRodDirection; + this.modID++; + } + + + public WorldCoordinate getLaunchSite() { + return this.launchSite; + } + + public void setLaunchSite(WorldCoordinate site) { + if (this.launchSite.equals(site)) + return; + this.launchSite = site; + this.modID++; + } + + + public GeodeticComputationStrategy getGeodeticComputation() { + return geodeticComputation; + } + + public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) { + if (this.geodeticComputation == geodeticComputation) + return; + if (geodeticComputation == null) { + throw new IllegalArgumentException("strategy cannot be null"); + } + this.geodeticComputation = geodeticComputation; + this.modID++; + } + + + public WindModel getWindModel() { + return windModel; + } + + + public void setWindModel(WindModel windModel) { + if (this.windModel != null) + this.modIDadd += this.windModel.getModID(); + this.modID++; + this.windModel = windModel; + } + + + public AtmosphericModel getAtmosphericModel() { + return atmosphericModel; + } + + + public void setAtmosphericModel(AtmosphericModel atmosphericModel) { + if (this.atmosphericModel != null) + this.modIDadd += this.atmosphericModel.getModID(); + this.modID++; + this.atmosphericModel = atmosphericModel; + } + + + public GravityModel getGravityModel() { + return gravityModel; + } + + + public void setGravityModel(GravityModel gravityModel) { + //if (this.gravityModel != null) + // this.modIDadd += this.gravityModel.getModID(); + this.modID++; + this.gravityModel = gravityModel; + } + + + public double getTimeStep() { + return timeStep; + } + + + public void setTimeStep(double timeStep) { + this.timeStep = timeStep; + this.modID++; + } + + + public double getMaximumAngleStep() { + return maximumAngleStep; + } + + + public void setMaximumAngleStep(double maximumAngle) { + this.maximumAngleStep = maximumAngle; + this.modID++; + } + + + public boolean isCalculateExtras() { + return calculateExtras; + } + + + public void setCalculateExtras(boolean calculateExtras) { + this.calculateExtras = calculateExtras; + this.modID++; + } + + + + public int getRandomSeed() { + return randomSeed; + } + + + public void setRandomSeed(int randomSeed) { + this.randomSeed = randomSeed; + this.modID++; + } + + + + + // TODO: HIGH: Make cleaner + public List<SimulationListener> getSimulationListenerList() { + return simulationListeners; + } + + + @Override + public int getModID() { + //return (modID + modIDadd + rocket.getModID() + windModel.getModID() + atmosphericModel.getModID() + + // gravityModel.getModID() + aerodynamicCalculator.getModID() + massCalculator.getModID()); + return (modID + modIDadd + rocket.getModID() + windModel.getModID() + atmosphericModel.getModID() + + aerodynamicCalculator.getModID() + massCalculator.getModID()); + } + + + @Override + public SimulationConditions clone() { + try { + // TODO: HIGH: Deep clone models + return (SimulationConditions) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } + } + +} diff --git a/core/src/net/sf/openrocket/simulation/SimulationEngine.java b/core/src/net/sf/openrocket/simulation/SimulationEngine.java new file mode 100644 index 00000000..b70e82bc --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/SimulationEngine.java @@ -0,0 +1,24 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.simulation.exception.SimulationException; + +/** + * A simulation engine that controls the flow of a simulation. This typically maintains + * flight events and related actions, while continuously calling a SimulationStepper to + * move the rocket forward step by step. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public interface SimulationEngine { + + /** + * Simulate the flight of a rocket. + * + * @param simulation the simulation conditions which to simulate. + * @return a FlightData object containing the simulated data. + * @throws SimulationException if an error occurs during simulation + */ + public FlightData simulate(SimulationConditions simulation) + throws SimulationException; + +} diff --git a/core/src/net/sf/openrocket/simulation/SimulationOptions.java b/core/src/net/sf/openrocket/simulation/SimulationOptions.java new file mode 100644 index 00000000..7734ec5d --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/SimulationOptions.java @@ -0,0 +1,538 @@ +package net.sf.openrocket.simulation; + +import java.util.ArrayList; +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; +import java.util.Random; + +import net.sf.openrocket.aerodynamics.BarrowmanCalculator; +import net.sf.openrocket.masscalc.BasicMassCalculator; +import net.sf.openrocket.models.atmosphere.AtmosphericModel; +import net.sf.openrocket.models.atmosphere.ExtendedISAModel; +import net.sf.openrocket.models.gravity.GravityModel; +import net.sf.openrocket.models.gravity.WGSGravityModel; +import net.sf.openrocket.models.wind.PinkNoiseWindModel; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.ChangeSource; +import net.sf.openrocket.util.GeodeticComputationStrategy; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.StateChangeListener; +import net.sf.openrocket.util.Utils; +import net.sf.openrocket.util.WorldCoordinate; + +/** + * A class holding simulation options in basic parameter form and which functions + * as a ChangeSource. A SimulationConditions instance is generated from this class + * using {@link #toSimulationConditions()}. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationOptions implements ChangeSource, Cloneable { + + public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI / 3; + + /** + * The ISA standard atmosphere. + */ + private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel(); + + + private final Rocket rocket; + private String motorID = null; + + + /* + * NOTE: When adding/modifying parameters, they must also be added to the + * equals and copyFrom methods!! + */ + + // TODO: HIGH: Fetch default values from Prefs! + + private double launchRodLength = 1; + + /** Launch rod angle > 0, radians from vertical */ + private double launchRodAngle = 0; + + /** Launch rod direction, 0 = upwind, PI = downwind. */ + private double launchRodDirection = 0; + + + private double windAverage = 2.0; + private double windTurbulence = 0.1; + + + /* + * SimulationOptions maintains the launch site parameters as separate double values, + * and converts them into a WorldCoordinate when converting to SimulationConditions. + */ + private double launchAltitude = 0; + private double launchLatitude = 45; + private double launchLongitude = 0; + private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL; + + private boolean useISA = true; + private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE; + private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE; + + + private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP; + private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP; + + private int randomSeed = new Random().nextInt(); + + private boolean calculateExtras = true; + + + private List<EventListener> listeners = new ArrayList<EventListener>(); + + + + public SimulationOptions(Rocket rocket) { + this.rocket = rocket; + } + + + public Rocket getRocket() { + return rocket; + } + + + public String getMotorConfigurationID() { + return motorID; + } + + /** + * Set the motor configuration ID. This must be a valid motor configuration ID of + * the rocket, otherwise the configuration is set to <code>null</code>. + * + * @param id the configuration to set. + */ + public void setMotorConfigurationID(String id) { + if (id != null) + id = id.intern(); + if (!rocket.isMotorConfigurationID(id)) + id = null; + if (id == motorID) + return; + motorID = id; + fireChangeEvent(); + } + + + public double getLaunchRodLength() { + return launchRodLength; + } + + public void setLaunchRodLength(double launchRodLength) { + if (MathUtil.equals(this.launchRodLength, launchRodLength)) + return; + this.launchRodLength = launchRodLength; + fireChangeEvent(); + } + + + public double getLaunchRodAngle() { + return launchRodAngle; + } + + public void setLaunchRodAngle(double launchRodAngle) { + launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE); + if (MathUtil.equals(this.launchRodAngle, launchRodAngle)) + return; + this.launchRodAngle = launchRodAngle; + fireChangeEvent(); + } + + + public double getLaunchRodDirection() { + return launchRodDirection; + } + + public void setLaunchRodDirection(double launchRodDirection) { + launchRodDirection = MathUtil.reduce180(launchRodDirection); + if (MathUtil.equals(this.launchRodDirection, launchRodDirection)) + return; + this.launchRodDirection = launchRodDirection; + fireChangeEvent(); + } + + + + public double getWindSpeedAverage() { + return windAverage; + } + + public void setWindSpeedAverage(double windAverage) { + if (MathUtil.equals(this.windAverage, windAverage)) + return; + this.windAverage = MathUtil.max(windAverage, 0); + fireChangeEvent(); + } + + + public double getWindSpeedDeviation() { + return windAverage * windTurbulence; + } + + public void setWindSpeedDeviation(double windDeviation) { + if (windAverage < 0.1) { + windAverage = 0.1; + } + setWindTurbulenceIntensity(windDeviation / windAverage); + } + + + /** + * Return the wind turbulence intensity (standard deviation / average). + * + * @return the turbulence intensity + */ + public double getWindTurbulenceIntensity() { + return windTurbulence; + } + + /** + * Set the wind standard deviation to match the given turbulence intensity. + * + * @param intensity the turbulence intensity + */ + public void setWindTurbulenceIntensity(double intensity) { + // Does not check equality so that setWindSpeedDeviation can be sure of event firing + this.windTurbulence = intensity; + fireChangeEvent(); + } + + + + + + public double getLaunchAltitude() { + return launchAltitude; + } + + public void setLaunchAltitude(double altitude) { + if (MathUtil.equals(this.launchAltitude, altitude)) + return; + this.launchAltitude = altitude; + fireChangeEvent(); + } + + + public double getLaunchLatitude() { + return launchLatitude; + } + + public void setLaunchLatitude(double launchLatitude) { + launchLatitude = MathUtil.clamp(launchLatitude, -90, 90); + if (MathUtil.equals(this.launchLatitude, launchLatitude)) + return; + this.launchLatitude = launchLatitude; + fireChangeEvent(); + } + + public double getLaunchLongitude() { + return launchLongitude; + } + + public void setLaunchLongitude(double launchLongitude) { + launchLongitude = MathUtil.clamp(launchLongitude, -180, 180); + if (MathUtil.equals(this.launchLongitude, launchLongitude)) + return; + this.launchLongitude = launchLongitude; + fireChangeEvent(); + } + + + public GeodeticComputationStrategy getGeodeticComputation() { + return geodeticComputation; + } + + public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) { + if (this.geodeticComputation == geodeticComputation) + return; + if (geodeticComputation == null) { + throw new IllegalArgumentException("strategy cannot be null"); + } + this.geodeticComputation = geodeticComputation; + fireChangeEvent(); + } + + + public boolean isISAAtmosphere() { + return useISA; + } + + public void setISAAtmosphere(boolean isa) { + if (isa == useISA) + return; + useISA = isa; + fireChangeEvent(); + } + + + public double getLaunchTemperature() { + return launchTemperature; + } + + + + public void setLaunchTemperature(double launchTemperature) { + if (MathUtil.equals(this.launchTemperature, launchTemperature)) + return; + this.launchTemperature = launchTemperature; + fireChangeEvent(); + } + + + + public double getLaunchPressure() { + return launchPressure; + } + + + + public void setLaunchPressure(double launchPressure) { + if (MathUtil.equals(this.launchPressure, launchPressure)) + return; + this.launchPressure = launchPressure; + fireChangeEvent(); + } + + + /** + * Returns an atmospheric model corresponding to the launch conditions. The + * atmospheric models may be shared between different calls. + * + * @return an AtmosphericModel object. + */ + private AtmosphericModel getAtmosphericModel() { + if (useISA) { + return ISA_ATMOSPHERIC_MODEL; + } + return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure); + } + + + public double getTimeStep() { + return timeStep; + } + + public void setTimeStep(double timeStep) { + if (MathUtil.equals(this.timeStep, timeStep)) + return; + this.timeStep = timeStep; + fireChangeEvent(); + } + + public double getMaximumStepAngle() { + return maximumAngle; + } + + public void setMaximumStepAngle(double maximumAngle) { + maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180); + if (MathUtil.equals(this.maximumAngle, maximumAngle)) + return; + this.maximumAngle = maximumAngle; + fireChangeEvent(); + } + + + + public boolean getCalculateExtras() { + return calculateExtras; + } + + + + public void setCalculateExtras(boolean calculateExtras) { + if (this.calculateExtras == calculateExtras) + return; + this.calculateExtras = calculateExtras; + fireChangeEvent(); + } + + + + public int getRandomSeed() { + return randomSeed; + } + + public void setRandomSeed(int randomSeed) { + if (this.randomSeed == randomSeed) { + return; + } + this.randomSeed = randomSeed; + /* + * This does not fire an event since we don't want to invalidate simulation results + * due to changing the seed value. This needs to be revisited if the user is ever + * allowed to select the seed value. + */ + // fireChangeEvent(); + } + + /** + * Randomize the random seed value. + */ + public void randomizeSeed() { + this.randomSeed = new Random().nextInt(); + // fireChangeEvent(); + } + + + + @Override + public SimulationOptions clone() { + try { + SimulationOptions copy = (SimulationOptions) super.clone(); + copy.listeners = new ArrayList<EventListener>(); + return copy; + } catch (CloneNotSupportedException e) { + throw new BugException(e); + } + } + + + public void copyFrom(SimulationOptions src) { + + if (this.rocket == src.rocket) { + + this.motorID = src.motorID; + + } else { + + if (src.rocket.hasMotors(src.motorID)) { + // Try to find a matching motor ID + String motorDesc = src.rocket.getMotorConfigurationDescription(src.motorID); + String matchID = null; + + for (String id : this.rocket.getMotorConfigurationIDs()) { + if (motorDesc.equals(this.rocket.getMotorConfigurationDescription(id))) { + matchID = id; + break; + } + } + + this.motorID = matchID; + } else { + this.motorID = null; + } + } + + this.launchAltitude = src.launchAltitude; + this.launchLatitude = src.launchLatitude; + this.launchLongitude = src.launchLongitude; + this.launchPressure = src.launchPressure; + this.launchRodAngle = src.launchRodAngle; + this.launchRodDirection = src.launchRodDirection; + this.launchRodLength = src.launchRodLength; + this.launchTemperature = src.launchTemperature; + this.maximumAngle = src.maximumAngle; + this.timeStep = src.timeStep; + this.windAverage = src.windAverage; + this.windTurbulence = src.windTurbulence; + this.calculateExtras = src.calculateExtras; + this.randomSeed = src.randomSeed; + + fireChangeEvent(); + } + + + + /** + * Compares whether the two simulation conditions are equal. The two are considered + * equal if the rocket, motor id and all variables are equal. + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof SimulationOptions)) + return false; + SimulationOptions o = (SimulationOptions) other; + return ((this.rocket == o.rocket) && + Utils.equals(this.motorID, o.motorID) && + MathUtil.equals(this.launchAltitude, o.launchAltitude) && + MathUtil.equals(this.launchLatitude, o.launchLatitude) && + MathUtil.equals(this.launchLongitude, o.launchLongitude) && + MathUtil.equals(this.launchPressure, o.launchPressure) && + MathUtil.equals(this.launchRodAngle, o.launchRodAngle) && + MathUtil.equals(this.launchRodDirection, o.launchRodDirection) && + MathUtil.equals(this.launchRodLength, o.launchRodLength) && + MathUtil.equals(this.launchTemperature, o.launchTemperature) && + MathUtil.equals(this.maximumAngle, o.maximumAngle) && + MathUtil.equals(this.timeStep, o.timeStep) && + MathUtil.equals(this.windAverage, o.windAverage) && + MathUtil.equals(this.windTurbulence, o.windTurbulence) && + this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed); + } + + /** + * Hashcode method compatible with {@link #equals(Object)}. + */ + @Override + public int hashCode() { + if (motorID == null) + return rocket.hashCode(); + return rocket.hashCode() + motorID.hashCode(); + } + + @Override + public void addChangeListener(EventListener listener) { + listeners.add(listener); + } + + @Override + public void removeChangeListener(EventListener listener) { + listeners.remove(listener); + } + + private final EventObject event = new EventObject(this); + + private void fireChangeEvent() { + + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] list = listeners.toArray(new EventListener[0]); + for (EventListener l : list) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(event); + } + } + } + + + // TODO: HIGH: Clean up + public SimulationConditions toSimulationConditions() { + SimulationConditions conditions = new SimulationConditions(); + + conditions.setRocket((Rocket) getRocket().copy()); + conditions.setMotorConfigurationID(getMotorConfigurationID()); + conditions.setLaunchRodLength(getLaunchRodLength()); + conditions.setLaunchRodAngle(getLaunchRodAngle()); + conditions.setLaunchRodDirection(getLaunchRodDirection()); + conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude())); + conditions.setGeodeticComputation(getGeodeticComputation()); + conditions.setRandomSeed(randomSeed); + + PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed); + windModel.setAverage(getWindSpeedAverage()); + windModel.setStandardDeviation(getWindSpeedDeviation()); + conditions.setWindModel(windModel); + + conditions.setAtmosphericModel(getAtmosphericModel()); + + GravityModel gravityModel = new WGSGravityModel(); + + conditions.setGravityModel(gravityModel); + + conditions.setAerodynamicCalculator(new BarrowmanCalculator()); + conditions.setMassCalculator(new BasicMassCalculator()); + + conditions.setTimeStep(getTimeStep()); + conditions.setMaximumAngleStep(getMaximumStepAngle()); + + conditions.setCalculateExtras(getCalculateExtras()); + + return conditions; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/SimulationStatus.java b/core/src/net/sf/openrocket/simulation/SimulationStatus.java new file mode 100644 index 00000000..9a39cffd --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/SimulationStatus.java @@ -0,0 +1,389 @@ +package net.sf.openrocket.simulation; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import net.sf.openrocket.aerodynamics.WarningSet; +import net.sf.openrocket.motor.MotorInstanceConfiguration; +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.Monitorable; +import net.sf.openrocket.util.MonitorableSet; +import net.sf.openrocket.util.Quaternion; +import net.sf.openrocket.util.WorldCoordinate; + +/** + * A holder class for the dynamic status during the rocket's flight. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationStatus implements Cloneable, Monitorable { + + /* + * NOTE! All fields must be added to copyFrom() method!! + */ + + private SimulationConditions simulationConditions; + private Configuration configuration; + private MotorInstanceConfiguration motorConfiguration; + private FlightDataBranch flightData; + + private double time; + + private double previousTimeStep; + + private Coordinate position; + private WorldCoordinate worldPosition; + private Coordinate velocity; + + private Quaternion orientation; + private Coordinate rotationVelocity; + + private double effectiveLaunchRodLength; + + + /** Nanosecond time when the simulation was started. */ + private long simulationStartWallTime = Long.MIN_VALUE; + + + /** Set to true when a motor has ignited. */ + private boolean motorIgnited = false; + + /** Set to true when the rocket has risen from the ground. */ + private boolean liftoff = false; + + /** Set to true when the launch rod has been cleared. */ + private boolean launchRodCleared = false; + + /** Set to true when apogee has been detected. */ + private boolean apogeeReached = false; + + /** Contains a list of deployed recovery devices. */ + private MonitorableSet<RecoveryDevice> deployedRecoveryDevices = new MonitorableSet<RecoveryDevice>(); + + /** The flight event queue */ + private final EventQueue eventQueue = new EventQueue(); + + private WarningSet warnings; + + /** Available for special purposes by the listeners. */ + private final Map<String, Object> extraData = new HashMap<String, Object>(); + + + private int modID = 0; + private int modIDadd = 0; + + + public void setSimulationTime(double time) { + this.time = time; + this.modID++; + } + + + public double getSimulationTime() { + return time; + } + + + public void setConfiguration(Configuration configuration) { + if (this.configuration != null) + this.modIDadd += this.configuration.getModID(); + this.modID++; + this.configuration = configuration; + } + + + public Configuration getConfiguration() { + return configuration; + } + + + public void setMotorConfiguration(MotorInstanceConfiguration motorConfiguration) { + if (this.motorConfiguration != null) + this.modIDadd += this.motorConfiguration.getModID(); + this.modID++; + this.motorConfiguration = motorConfiguration; + } + + + public MotorInstanceConfiguration getMotorConfiguration() { + return motorConfiguration; + } + + + public void setFlightData(FlightDataBranch flightData) { + if (this.flightData != null) + this.modIDadd += this.flightData.getModID(); + this.modID++; + this.flightData = flightData; + } + + + public FlightDataBranch getFlightData() { + return flightData; + } + + + public double getPreviousTimeStep() { + return previousTimeStep; + } + + + public void setPreviousTimeStep(double previousTimeStep) { + this.previousTimeStep = previousTimeStep; + this.modID++; + } + + + public void setRocketPosition(Coordinate position) { + this.position = position; + this.modID++; + } + + + public Coordinate getRocketPosition() { + return position; + } + + public void setRocketWorldPosition(WorldCoordinate wc) { + this.worldPosition = wc; + this.modID++; + } + + public WorldCoordinate getRocketWorldPosition() { + return worldPosition; + } + + public void setRocketVelocity(Coordinate velocity) { + this.velocity = velocity; + this.modID++; + } + + + public Coordinate getRocketVelocity() { + return velocity; + } + + + + + + public Quaternion getRocketOrientationQuaternion() { + return orientation; + } + + + public void setRocketOrientationQuaternion(Quaternion orientation) { + this.orientation = orientation; + this.modID++; + } + + + public Coordinate getRocketRotationVelocity() { + return rotationVelocity; + } + + + public void setRocketRotationVelocity(Coordinate rotation) { + this.rotationVelocity = rotation; + } + + + public void setEffectiveLaunchRodLength(double effectiveLaunchRodLength) { + this.effectiveLaunchRodLength = effectiveLaunchRodLength; + this.modID++; + } + + + public double getEffectiveLaunchRodLength() { + return effectiveLaunchRodLength; + } + + + public void setSimulationStartWallTime(long simulationStartWallTime) { + this.simulationStartWallTime = simulationStartWallTime; + this.modID++; + } + + + public long getSimulationStartWallTime() { + return simulationStartWallTime; + } + + + public void setMotorIgnited(boolean motorIgnited) { + this.motorIgnited = motorIgnited; + this.modID++; + } + + + public boolean isMotorIgnited() { + return motorIgnited; + } + + + public void setLiftoff(boolean liftoff) { + this.liftoff = liftoff; + this.modID++; + } + + + public boolean isLiftoff() { + return liftoff; + } + + + public void setLaunchRodCleared(boolean launchRod) { + this.launchRodCleared = launchRod; + this.modID++; + } + + + public boolean isLaunchRodCleared() { + return launchRodCleared; + } + + + public void setApogeeReached(boolean apogeeReached) { + this.apogeeReached = apogeeReached; + this.modID++; + } + + + public boolean isApogeeReached() { + return apogeeReached; + } + + + public Set<RecoveryDevice> getDeployedRecoveryDevices() { + return deployedRecoveryDevices; + } + + + public void setWarnings(WarningSet warnings) { + if (this.warnings != null) + this.modIDadd += this.warnings.getModID(); + this.modID++; + this.warnings = warnings; + } + + + public WarningSet getWarnings() { + return warnings; + } + + + public EventQueue getEventQueue() { + return eventQueue; + } + + + public void setSimulationConditions(SimulationConditions simulationConditions) { + if (this.simulationConditions != null) + this.modIDadd += this.simulationConditions.getModID(); + this.modID++; + this.simulationConditions = simulationConditions; + } + + + public SimulationConditions getSimulationConditions() { + return simulationConditions; + } + + + /** + * Store extra data available for use by simulation listeners. The data can be retrieved + * using {@link #getExtraData(String)}. + * + * @param key the data key + * @param value the value to store + */ + public void putExtraData(String key, Object value) { + extraData.put(key, value); + } + + /** + * Retrieve extra data stored by simulation listeners. This data map is initially empty. + * Data can be stored using {@link #putExtraData(String, Object)}. + * + * @param key the data key to retrieve + * @return the data, or <code>null</code> if nothing has been set for the key + */ + public Object getExtraData(String key) { + return extraData.get(key); + } + + + /** + * Returns a copy of this object. The general purpose is that the conditions, + * rocket configuration, flight data etc. point to the same objects. However, + * subclasses are allowed to deep-clone specific objects, such as those pertaining + * to the current orientation of the rocket. The purpose is to allow creating intermediate + * copies of this object used during step computation. + * + * TODO: HIGH: Deep cloning required for branch saving. + */ + @Override + public SimulationStatus clone() { + try { + SimulationStatus clone = (SimulationStatus) super.clone(); + return clone; + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException?!?", e); + } + } + + + /** + * Copies the data from the provided object to this object. Most included object are + * deep-cloned, except for the flight data object. + * + * @param orig the object from which to copy + */ + public void copyFrom(SimulationStatus orig) { + this.simulationConditions = orig.simulationConditions.clone(); + this.configuration = orig.configuration.clone(); + this.motorConfiguration = orig.motorConfiguration.clone(); + this.flightData = orig.flightData; + this.time = orig.time; + this.previousTimeStep = orig.previousTimeStep; + this.position = orig.position; + this.worldPosition = orig.worldPosition; + this.velocity = orig.velocity; + this.orientation = orig.orientation; + this.rotationVelocity = orig.rotationVelocity; + this.effectiveLaunchRodLength = orig.effectiveLaunchRodLength; + this.simulationStartWallTime = orig.simulationStartWallTime; + this.motorIgnited = orig.motorIgnited; + this.liftoff = orig.liftoff; + this.launchRodCleared = orig.launchRodCleared; + this.apogeeReached = orig.apogeeReached; + + this.deployedRecoveryDevices.clear(); + this.deployedRecoveryDevices.addAll(orig.deployedRecoveryDevices); + + this.eventQueue.clear(); + this.eventQueue.addAll(orig.eventQueue); + + this.warnings = orig.warnings; + + this.extraData.clear(); + this.extraData.putAll(orig.extraData); + + this.modID = orig.modID; + this.modIDadd = orig.modIDadd; + } + + + @Override + public int getModID() { + return (modID + modIDadd + simulationConditions.getModID() + configuration.getModID() + + motorConfiguration.getModID() + flightData.getModID() + deployedRecoveryDevices.getModID() + + eventQueue.getModID() + warnings.getModID()); + } + + +} diff --git a/core/src/net/sf/openrocket/simulation/SimulationStepper.java b/core/src/net/sf/openrocket/simulation/SimulationStepper.java new file mode 100644 index 00000000..90855c3c --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/SimulationStepper.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.simulation; + +import net.sf.openrocket.simulation.exception.SimulationException; + +public interface SimulationStepper { + + /** + * Initialize a simulation using this simulation stepper based on the provided + * current simulation status and launch conditions. + * + * @param status the current simulation status. + * @return a SimulationStatus suitable for simulating with this simulation stepper. + */ + public SimulationStatus initialize(SimulationStatus status) throws SimulationException; + + /** + * Perform one simulation time step. + * + * @param status the current simulation status, of a type returned by {@link #initialize(SimulationStatus)}. + * @param maxTimeStep the maximum time step to take. This is an upper bound and can be used to limit a stepper + * from stepping over upcoming flight events (motor ignition etc). + */ + public void step(SimulationStatus status, double maxTimeStep) throws SimulationException; + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java b/core/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java new file mode 100644 index 00000000..fd9e60ac --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java @@ -0,0 +1,19 @@ +package net.sf.openrocket.simulation.exception; + +/** + * An exception signifying that the simulation failed because no motors were + * defined or ignited in the rocket. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class MotorIgnitionException extends SimulationLaunchException { + + public MotorIgnitionException(String message) { + super(message); + } + + public MotorIgnitionException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationCalculationException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationCalculationException.java new file mode 100644 index 00000000..40f86321 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationCalculationException.java @@ -0,0 +1,26 @@ +package net.sf.openrocket.simulation.exception; + +/** + * An exception that indicates that a computation problem has occurred during + * the simulation, for example that some values have exceed reasonable bounds. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationCalculationException extends SimulationException { + + public SimulationCalculationException() { + } + + public SimulationCalculationException(String message) { + super(message); + } + + public SimulationCalculationException(Throwable cause) { + super(cause); + } + + public SimulationCalculationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java new file mode 100644 index 00000000..e2293432 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.simulation.exception; + + +/** + * An exception signifying that a simulation was cancelled. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationCancelledException extends SimulationException { + + public SimulationCancelledException() { + + } + + public SimulationCancelledException(String message) { + super(message); + } + + public SimulationCancelledException(Throwable cause) { + super(cause); + } + + public SimulationCancelledException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java new file mode 100644 index 00000000..f180f892 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationException.java @@ -0,0 +1,21 @@ +package net.sf.openrocket.simulation.exception; + +public class SimulationException extends Exception { + + public SimulationException() { + + } + + public SimulationException(String message) { + super(message); + } + + public SimulationException(Throwable cause) { + super(cause); + } + + public SimulationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java new file mode 100644 index 00000000..df9f3862 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java @@ -0,0 +1,19 @@ +package net.sf.openrocket.simulation.exception; + +/** + * An exception signifying that a problem occurred at launch, for example + * that no motors were defined or no motors ignited. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationLaunchException extends SimulationException { + + public SimulationLaunchException(String message) { + super(message); + } + + public SimulationLaunchException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java new file mode 100644 index 00000000..d6bd737a --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java @@ -0,0 +1,21 @@ +package net.sf.openrocket.simulation.exception; + + +public class SimulationListenerException extends SimulationException { + + public SimulationListenerException() { + } + + public SimulationListenerException(String message) { + super(message); + } + + public SimulationListenerException(Throwable cause) { + super(cause); + } + + public SimulationListenerException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java b/core/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java new file mode 100644 index 00000000..66618258 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java @@ -0,0 +1,30 @@ +package net.sf.openrocket.simulation.exception; + + +/** + * A exception that signifies that the attempted simulation is not supported. + * The reason for not being supported may be due to unsupported combination of + * simulator/calculator, unsupported rocket structure or other reasons. + * <p> + * This exception signifies a fatal problem in the simulation; for non-fatal conditions + * add a warning to the simulation results. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationNotSupportedException extends SimulationException { + + public SimulationNotSupportedException() { + } + + public SimulationNotSupportedException(String message) { + super(message); + } + + public SimulationNotSupportedException(Throwable cause) { + super(cause); + } + + public SimulationNotSupportedException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java new file mode 100644 index 00000000..ebb2ad94 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java @@ -0,0 +1,169 @@ +package net.sf.openrocket.simulation.listeners; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.AccelerationData; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.MassData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.Coordinate; + + +/** + * An abstract base class for implementing simulation listeners. This class implements all + * of the simulation listener interfaces using methods that have no effect on the simulation. + * The recommended way of implementing simulation listeners is to extend this class. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class AbstractSimulationListener implements SimulationListener, SimulationComputationListener, + SimulationEventListener { + + //// SimulationListener //// + + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + // No-op + } + + @Override + public void endSimulation(SimulationStatus status, SimulationException exception) { + // No-op + } + + @Override + public boolean preStep(SimulationStatus status) throws SimulationException { + return true; + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + // No-op + } + + /** + * {@inheritDoc} + * <p> + * <em>This implementation of the method always returns <code>false</code>.</em> + */ + @Override + public boolean isSystemListener() { + return false; + } + + + + + //// SimulationEventListener //// + + @Override + public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return true; + } + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + return true; + } + + @Override + public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException { + return true; + } + + @Override + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { + return true; + } + + + + //// SimulationComputationListener //// + + @Override + public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException { + return null; + } + + @Override + public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException { + return null; + } + + @Override + public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException { + return null; + } + + @Override + public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException { + return null; + } + + @Override + public double preGravityModel(SimulationStatus status) throws SimulationException { + return Double.NaN; + } + + @Override + public MassData preMassCalculation(SimulationStatus status) throws SimulationException { + return null; + } + + @Override + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException { + return Double.NaN; + } + + @Override + public Coordinate preWindModel(SimulationStatus status) throws SimulationException { + return null; + } + + @Override + public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException { + return null; + } + + @Override + public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException { + return null; + } + + @Override + public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException { + return null; + } + + @Override + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { + return null; + } + + @Override + public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException { + return Double.NaN; + } + + @Override + public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException { + return null; + } + + @Override + public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { + return Double.NaN; + } + + @Override + public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { + return null; + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java new file mode 100644 index 00000000..be46c50f --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java @@ -0,0 +1,67 @@ +package net.sf.openrocket.simulation.listeners; + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.simulation.AccelerationData; +import net.sf.openrocket.simulation.MassData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.util.Coordinate; + +/** + * An interface containing listener callbacks relating to different computational aspects performed + * during flight. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public interface SimulationComputationListener extends SimulationListener { + + + //////// Computation/modeling related callbacks //////// + + public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException; + + public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) + throws SimulationException; + + public AtmosphericConditions preAtmosphericModel(SimulationStatus status) + throws SimulationException; + + public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) + throws SimulationException; + + + public Coordinate preWindModel(SimulationStatus status) throws SimulationException; + + public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException; + + + public double preGravityModel(SimulationStatus status) throws SimulationException; + + public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException; + + + public FlightConditions preFlightConditions(SimulationStatus status) + throws SimulationException; + + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) + throws SimulationException; + + + public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) + throws SimulationException; + + public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) + throws SimulationException; + + public MassData preMassCalculation(SimulationStatus status) throws SimulationException; + + public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException; + + + public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException; + + public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException; + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java new file mode 100644 index 00000000..6cbb87f3 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java @@ -0,0 +1,61 @@ +package net.sf.openrocket.simulation.listeners; + +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; + +public interface SimulationEventListener { + + + /** + * Called before adding a flight event to the event queue. + * + * @param status the simulation status + * @param event the event that is being added + * @return <code>true</code> to add the event, + * <code>false</code> to abort adding event to event queue + */ + public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; + + + + /** + * Called before handling a flight event. + * + * @param status the simulation status + * @param event the event that is taking place + * @return <code>true</code> to continue handling the event, + * <code>false</code> to abort handling + */ + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; + + + /** + * Motor ignition event. + * + * @param status the simulation status + * @param motorId the motor id in the MotorInstanceConfiguration + * @param mount the motor mount containing the motor + * @param instance the motor instance being ignited + * @return <code>true</code> to ignite the motor, <code>false</code> to abort ignition + */ + public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, + MotorInstance instance) throws SimulationException; + + + /** + * Recovery device deployment. + * + * @param status the simulation status + * @param recoveryDevice the recovery device that is being deployed. + * @return <code>true</code> to deploy the recovery device, <code>false</code> to abort deployment + */ + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) + throws SimulationException; + + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java new file mode 100644 index 00000000..98b28aad --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationListener.java @@ -0,0 +1,60 @@ +package net.sf.openrocket.simulation.listeners; + +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; + + + +public interface SimulationListener { + + /** + * Called when starting a simulation. + * + * @param status the simulation status. + */ + public void startSimulation(SimulationStatus status) throws SimulationException; + + + /** + * Called when ending a simulation. This is called either when the simulation ends normally + * (due to an end simulation event) or when a SimulationException is thrown. + * <p> + * This method cannot throw a SimulationException, since the simulation is already being ended. + * + * @param status the simulation status. + * @param exception the exception that caused ending the simulation, or <code>null</code> if ending normally. + */ + public void endSimulation(SimulationStatus status, SimulationException exception); + + + /** + * Called before a simulation step is taken. This method may also prevent the normal + * stepping method from being called. + * + * @param status the simulation status. + * @return <code>true</code> to continue normally, <code>false</code> to skip taking the step + */ + public boolean preStep(SimulationStatus status) throws SimulationException; + + + /** + * Called immediately after a simulation step has been taken. This method is called whether the + * {@link #preStep(SimulationStatus)} aborted the step or not. + * + * @param status the simulation status. + */ + public void postStep(SimulationStatus status) throws SimulationException; + + + /** + * Return whether this is a system listener. System listeners are used internally for various + * purposes by OpenRocket. User-written listeners should always return <code>false</code>. + * <p> + * System listeners do not cause warnings to be added to the simulation results when they affect + * the simulation. + * + * @return whether this is a system listener + */ + public boolean isSystemListener(); + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java b/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java new file mode 100644 index 00000000..be658f15 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java @@ -0,0 +1,668 @@ +package net.sf.openrocket.simulation.listeners; + + +import net.sf.openrocket.aerodynamics.AerodynamicForces; +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.aerodynamics.Warning; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.MotorId; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.rocketcomponent.MotorMount; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.AccelerationData; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.MassData; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; + +/** + * Helper methods for firing events to simulation listeners. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class SimulationListenerHelper { + + private static final LogHelper log = Application.getLogger(); + + //////// SimulationListener methods //////// + + + /** + * Fire startSimulation event. + */ + public static void fireStartSimulation(SimulationStatus status) + throws SimulationException { + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + l.startSimulation(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + } + } + + + /** + * Fire endSimulation event. + */ + public static void fireEndSimulation(SimulationStatus status, SimulationException exception) { + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + l.endSimulation(status, exception); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + } + } + + + + + /** + * Fire preStep event. + * + * @return <code>true</code> to handle step normally, <code>false</code> to skip the step. + */ + public static boolean firePreStep(SimulationStatus status) + throws SimulationException { + boolean b; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + b = l.preStep(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (b == false) { + warn(status, l); + return false; + } + } + return true; + } + + + /** + * Fire postStep event. + */ + public static void firePostStep(SimulationStatus status) + throws SimulationException { + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + l.postStep(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + } + } + + + + //////// SimulationEventListener methods //////// + + /** + * Fire an add flight event event. + * + * @return <code>true</code> to add the event normally, <code>false</code> to skip adding the event. + */ + public static boolean fireAddFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + boolean b; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationEventListener) { + b = ((SimulationEventListener) l).addFlightEvent(status, event); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (b == false) { + warn(status, l); + return false; + } + } + } + return true; + } + + /** + * Fire a handle flight event event. + * + * @return <code>true</code> to handle the event normally, <code>false</code> to skip event. + */ + public static boolean fireHandleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + boolean b; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationEventListener) { + b = ((SimulationEventListener) l).handleFlightEvent(status, event); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (b == false) { + warn(status, l); + return false; + } + } + } + return true; + } + + /** + * Fire motor ignition event. + * + * @return <code>true</code> to handle the event normally, <code>false</code> to skip event. + */ + public static boolean fireMotorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, + MotorInstance instance) throws SimulationException { + boolean b; + int modID = status.getModID(); // Contains also motor instance + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationEventListener) { + b = ((SimulationEventListener) l).motorIgnition(status, motorId, mount, instance); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (b == false) { + warn(status, l); + return false; + } + } + } + return true; + } + + + /** + * Fire recovery device deployment event. + * + * @return <code>true</code> to handle the event normally, <code>false</code> to skip event. + */ + public static boolean fireRecoveryDeviceDeployment(SimulationStatus status, RecoveryDevice device) + throws SimulationException { + boolean b; + int modID = status.getModID(); // Contains also motor instance + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationEventListener) { + b = ((SimulationEventListener) l).recoveryDeviceDeployment(status, device); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (b == false) { + warn(status, l); + return false; + } + } + } + return true; + } + + + //////// SimulationComputationalListener methods //////// + + /** + * Fire preAtmosphericModel event. + * + * @return <code>null</code> normally, or overriding atmospheric conditions. + */ + public static AtmosphericConditions firePreAtmosphericModel(SimulationStatus status) + throws SimulationException { + AtmosphericConditions conditions; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + conditions = ((SimulationComputationListener) l).preAtmosphericModel(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (conditions != null) { + warn(status, l); + return conditions; + } + } + } + return null; + } + + /** + * Fire postAtmosphericModel event. + * + * @return the atmospheric conditions to use. + */ + public static AtmosphericConditions firePostAtmosphericModel(SimulationStatus status, AtmosphericConditions conditions) + throws SimulationException { + AtmosphericConditions c; + AtmosphericConditions clone = conditions.clone(); + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + c = ((SimulationComputationListener) l).postAtmosphericModel(status, clone); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (c != null && !c.equals(conditions)) { + warn(status, l); + conditions = c; + clone = conditions.clone(); + } + } + } + return conditions; + } + + + + /** + * Fire preWindModel event. + * + * @return <code>null</code> normally, or overriding wind. + */ + public static Coordinate firePreWindModel(SimulationStatus status) + throws SimulationException { + Coordinate wind; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + wind = ((SimulationComputationListener) l).preWindModel(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (wind != null) { + warn(status, l); + return wind; + } + } + } + return null; + } + + /** + * Fire postWindModel event. + * + * @return the wind to use. + */ + public static Coordinate firePostWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { + Coordinate w; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + w = ((SimulationComputationListener) l).postWindModel(status, wind); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (w != null && !w.equals(wind)) { + warn(status, l); + wind = w; + } + } + } + return wind; + } + + + + /** + * Fire preGravityModel event. + * + * @return <code>NaN</code> normally, or overriding gravity. + */ + public static double firePreGravityModel(SimulationStatus status) + throws SimulationException { + double gravity; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + gravity = ((SimulationComputationListener) l).preGravityModel(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (!Double.isNaN(gravity)) { + warn(status, l); + return gravity; + } + } + } + return Double.NaN; + } + + /** + * Fire postGravityModel event. + * + * @return the gravity to use. + */ + public static double firePostGravityModel(SimulationStatus status, double gravity) throws SimulationException { + double g; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + g = ((SimulationComputationListener) l).postGravityModel(status, gravity); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (!Double.isNaN(g) && !MathUtil.equals(g, gravity)) { + warn(status, l); + gravity = g; + } + } + } + return gravity; + } + + + + + /** + * Fire preFlightConditions event. + * + * @return <code>null</code> normally, or overriding flight conditions. + */ + public static FlightConditions firePreFlightConditions(SimulationStatus status) + throws SimulationException { + FlightConditions conditions; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + conditions = ((SimulationComputationListener) l).preFlightConditions(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (conditions != null) { + warn(status, l); + return conditions; + } + } + } + return null; + } + + /** + * Fire postFlightConditions event. + * + * @return the flight conditions to use: either <code>conditions</code> or a new object + * containing the modified conditions. + */ + public static FlightConditions firePostFlightConditions(SimulationStatus status, FlightConditions conditions) + throws SimulationException { + FlightConditions c; + FlightConditions clone = conditions.clone(); + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + c = ((SimulationComputationListener) l).postFlightConditions(status, clone); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (c != null && !c.equals(conditions)) { + warn(status, l); + conditions = c; + clone = conditions.clone(); + } + } + } + return conditions; + } + + + + + /** + * Fire preAerodynamicCalculation event. + * + * @return <code>null</code> normally, or overriding aerodynamic forces. + */ + public static AerodynamicForces firePreAerodynamicCalculation(SimulationStatus status) + throws SimulationException { + AerodynamicForces forces; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + forces = ((SimulationComputationListener) l).preAerodynamicCalculation(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (forces != null) { + warn(status, l); + return forces; + } + } + } + return null; + } + + /** + * Fire postAerodynamicCalculation event. + * + * @return the aerodynamic forces to use. + */ + public static AerodynamicForces firePostAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) + throws SimulationException { + AerodynamicForces f; + AerodynamicForces clone = forces.clone(); + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + f = ((SimulationComputationListener) l).postAerodynamicCalculation(status, clone); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (f != null && !f.equals(forces)) { + warn(status, l); + forces = f; + clone = forces.clone(); + } + } + } + return forces; + } + + + + + + /** + * Fire preMassCalculation event. + * + * @return <code>null</code> normally, or overriding mass data. + */ + public static MassData firePreMassCalculation(SimulationStatus status) + throws SimulationException { + MassData mass; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + mass = ((SimulationComputationListener) l).preMassCalculation(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (mass != null) { + warn(status, l); + return mass; + } + } + } + return null; + } + + /** + * Fire postMassCalculation event. + * + * @return the aerodynamic forces to use. + */ + public static MassData firePostMassCalculation(SimulationStatus status, MassData mass) throws SimulationException { + MassData m; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + m = ((SimulationComputationListener) l).postMassCalculation(status, mass); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (m != null && !m.equals(mass)) { + warn(status, l); + mass = m; + } + } + } + return mass; + } + + + + + /** + * Fire preThrustComputation event. + * + * @return <code>NaN</code> normally, or overriding thrust. + */ + public static double firePreThrustCalculation(SimulationStatus status) + throws SimulationException { + double thrust; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + thrust = ((SimulationComputationListener) l).preSimpleThrustCalculation(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (!Double.isNaN(thrust)) { + warn(status, l); + return thrust; + } + } + } + return Double.NaN; + } + + /** + * Fire postThrustComputation event. + * + * @return the thrust value to use. + */ + public static double firePostThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { + double t; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + t = ((SimulationComputationListener) l).postSimpleThrustCalculation(status, thrust); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (!Double.isNaN(t) && !MathUtil.equals(t, thrust)) { + warn(status, l); + thrust = t; + } + } + } + return thrust; + } + + + + + + /** + * Fire preMassCalculation event. + * + * @return <code>null</code> normally, or overriding mass data. + */ + public static AccelerationData firePreAccelerationCalculation(SimulationStatus status) throws SimulationException { + AccelerationData acceleration; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + acceleration = ((SimulationComputationListener) l).preAccelerationCalculation(status); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (acceleration != null) { + warn(status, l); + return acceleration; + } + } + } + return null; + } + + /** + * Fire postMassCalculation event. + * + * @return the aerodynamic forces to use. + */ + public static AccelerationData firePostAccelerationCalculation(SimulationStatus status, + AccelerationData acceleration) throws SimulationException { + AccelerationData a; + int modID = status.getModID(); + + for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { + if (l instanceof SimulationComputationListener) { + a = ((SimulationComputationListener) l).postAccelerationCalculation(status, acceleration); + if (modID != status.getModID()) { + warn(status, l); + modID = status.getModID(); + } + if (a != null && !a.equals(acceleration)) { + warn(status, l); + acceleration = a; + } + } + } + return acceleration; + } + + + + + private static void warn(SimulationStatus status, SimulationListener listener) { + if (!listener.isSystemListener()) { + log.info("Non-system listener " + listener + " affected the simulation"); + status.getWarnings().add(Warning.LISTENERS_AFFECTED); + } + } +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/AirStart.java b/core/src/net/sf/openrocket/simulation/listeners/example/AirStart.java new file mode 100644 index 00000000..97ce93a2 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/example/AirStart.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.simulation.listeners.example; + +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; +import net.sf.openrocket.util.Coordinate; + +/** + * Simulation listener that launches a rocket from a specific altitude. + * <p> + * The altitude is read from the system property "openrocket.airstart.altitude" + * if defined, otherwise a default altitude of 1000 meters is used. + */ +public class AirStart extends AbstractSimulationListener { + + /** Default launch altitude */ + private static final double DEFAULT_ALTITUDE = 1000.0; + + @Override + public void startSimulation(SimulationStatus status) throws SimulationException { + + // Get the launch altitude + double altitude; + String arg = System.getProperty("openrocket.airstart.altitude"); + try { + altitude = Double.parseDouble(arg); + } catch (RuntimeException e) { + altitude = DEFAULT_ALTITUDE; + } + + // Modify launch position + Coordinate position = status.getRocketPosition(); + position = position.add(0, 0, altitude); + status.setRocketPosition(position); + + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/CSVSaveListener.java b/core/src/net/sf/openrocket/simulation/listeners/example/CSVSaveListener.java new file mode 100644 index 00000000..05a55fa2 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/example/CSVSaveListener.java @@ -0,0 +1,295 @@ +package net.sf.openrocket.simulation.listeners.example; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintStream; +import java.util.Iterator; + +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + + +public class CSVSaveListener extends AbstractSimulationListener { + + private static enum Types { + TIME { + @Override + public double getValue(SimulationStatus status) { + return status.getSimulationTime(); + } + }, + POSITION_X { + @Override + public double getValue(SimulationStatus status) { + return status.getRocketPosition().x; + } + }, + POSITION_Y { + @Override + public double getValue(SimulationStatus status) { + return status.getRocketPosition().y; + } + }, + ALTITUDE { + @Override + public double getValue(SimulationStatus status) { + return status.getRocketPosition().z; + } + }, + VELOCITY_X { + @Override + public double getValue(SimulationStatus status) { + return status.getRocketVelocity().x; + } + }, + VELOCITY_Y { + @Override + public double getValue(SimulationStatus status) { + return status.getRocketVelocity().y; + } + }, + VELOCITY_Z { + @Override + public double getValue(SimulationStatus status) { + return status.getRocketVelocity().z; + } + }, + THETA { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_ORIENTATION_THETA); + } + }, + PHI { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_ORIENTATION_PHI); + } + }, + AOA { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_AOA); + } + }, + ROLLRATE { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_ROLL_RATE); + } + }, + PITCHRATE { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_PITCH_RATE); + } + }, + + PITCHMOMENT { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_PITCH_MOMENT_COEFF); + } + }, + YAWMOMENT { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_YAW_MOMENT_COEFF); + } + }, + ROLLMOMENT { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_ROLL_MOMENT_COEFF); + } + }, + NORMALFORCE { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_NORMAL_FORCE_COEFF); + } + }, + SIDEFORCE { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_SIDE_FORCE_COEFF); + } + }, + AXIALFORCE { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_DRAG_FORCE); + } + }, + WINDSPEED { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_WIND_VELOCITY); + } + }, + PITCHDAMPING { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF); + } + }, + CA { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_AXIAL_DRAG_COEFF); + } + }, + CD { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_DRAG_COEFF); + } + }, + CDpressure { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_PRESSURE_DRAG_COEFF); + } + }, + CDfriction { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_FRICTION_DRAG_COEFF); + } + }, + CDbase { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_BASE_DRAG_COEFF); + } + }, + MACH { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_MACH_NUMBER); + } + }, + RE { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_REYNOLDS_NUMBER); + } + }, + + CONTROL_ANGLE { + @Override + public double getValue(SimulationStatus status) { + Iterator<RocketComponent> iterator = + status.getConfiguration().getRocket().iterator(); + FinSet fin = null; + + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + if (c instanceof FinSet && c.getName().equals("CONTROL")) { + fin = (FinSet) c; + break; + } + } + if (fin == null) + return 0; + return fin.getCantAngle(); + } + }, + + MASS { + @Override + public double getValue(SimulationStatus status) { + return status.getFlightData().getLast(FlightDataType.TYPE_MASS); + } + } + + ; + + public abstract double getValue(SimulationStatus status); + } + + + public static final String FILENAME_FORMAT = "simulation-%03d.csv"; + + private File file; + private PrintStream output = null; + + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + + if (event.getType() == FlightEvent.Type.LAUNCH) { + int n = 1; + + if (output != null) { + System.err.println("WARNING: Ending simulation logging to CSV file " + + "(SIMULATION_END not encountered)."); + output.close(); + output = null; + } + + do { + file = new File(String.format(FILENAME_FORMAT, n)); + n++; + } while (file.exists()); + + System.err.println("Opening file " + file + " for CSV output."); + try { + output = new PrintStream(file); + } catch (FileNotFoundException e) { + System.err.println("ERROR OPENING FILE: " + e); + } + + final Types[] types = Types.values(); + StringBuilder s = new StringBuilder("# " + types[0].toString()); + for (int i = 1; i < types.length; i++) { + s.append("," + types[i].toString()); + } + output.println(s); + + } else if (event.getType() == FlightEvent.Type.SIMULATION_END && output != null) { + + System.err.println("Ending simulation logging to CSV file: " + file); + output.close(); + output = null; + + } else if (event.getType() != FlightEvent.Type.ALTITUDE) { + + if (output != null) { + output.println("# Event " + event); + } else { + System.err.println("WARNING: Event " + event + " encountered without open file"); + } + + } + + return true; + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + + final Types[] types = Types.values(); + StringBuilder s; + + if (output != null) { + + s = new StringBuilder("" + types[0].getValue(status)); + for (int i = 1; i < types.length; i++) { + s.append("," + types[i].getValue(status)); + } + output.println(s); + + } else { + + System.err.println("WARNING: stepTaken called with no open file " + + "(t=" + status.getSimulationTime() + ")"); + } + + } +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/PrintSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/example/PrintSimulationListener.java new file mode 100644 index 00000000..ac070d2b --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/example/PrintSimulationListener.java @@ -0,0 +1,34 @@ +package net.sf.openrocket.simulation.listeners.example; + +import net.sf.openrocket.simulation.FlightDataBranch; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + + +public class PrintSimulationListener extends AbstractSimulationListener { + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { + System.out.println("*** handleEvent *** " + event.toString() + + " position=" + status.getRocketPosition() + " velocity=" + status.getRocketVelocity()); + return true; + } + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + FlightDataBranch data = status.getFlightData(); + System.out.printf("*** stepTaken *** time=%.3f position=" + status.getRocketPosition() + + " velocity=" + status.getRocketVelocity() + "=%.3f\n", status.getSimulationTime(), status.getRocketVelocity().length()); + System.out.printf(" thrust=%.3fN drag==%.3fN mass=%.3fkg " + + "accZ=%.3fm/s2 acc=%.3fm/s2\n", + data.getLast(FlightDataType.TYPE_THRUST_FORCE), + data.getLast(FlightDataType.TYPE_DRAG_FORCE), + data.getLast(FlightDataType.TYPE_MASS), + data.getLast(FlightDataType.TYPE_ACCELERATION_Z), + data.getLast(FlightDataType.TYPE_ACCELERATION_TOTAL)); + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/RollControlListener.java b/core/src/net/sf/openrocket/simulation/listeners/example/RollControlListener.java new file mode 100644 index 00000000..80993218 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/example/RollControlListener.java @@ -0,0 +1,128 @@ +package net.sf.openrocket.simulation.listeners.example; + +import net.sf.openrocket.aerodynamics.FlightConditions; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.simulation.FlightDataType; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; +import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.MathUtil; + +/** + * An example listener that applies a PI-controller to adjust the cant of fins + * named "CONTROL" to stop the rocket from rolling. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class RollControlListener extends AbstractSimulationListener { + + // Name of control fin set + private static final String CONTROL_FIN_NAME = "CONTROL"; + + // Define custom flight data type + private static final FlightDataType FIN_CANT_TYPE = FlightDataType.getType("Control fin cant", + UnitGroup.UNITS_ANGLE); + + // Simulation time at which PID controller is activated + private static final double START_TIME = 0.5; + + // Desired roll rate (rad/sec) + private static final double SETPOINT = 0.0; + + // Maximum control fin turn rate (rad/sec) + private static final double TURNRATE = 10 * Math.PI / 180; + + // Maximum control fin angle (rad) + private static final double MAX_ANGLE = 15 * Math.PI / 180; + + + /* + * PID parameters + * + * At M=0.3 KP oscillation threshold between 0.35 and 0.4. Good KI=3 + * At M=0.6 KP oscillation threshold between 0.07 and 0.08 Good KI=2 + * At M=0.9 KP oscillation threshold between 0.013 and 0.014 Good KI=0.5 + */ + private static final double KP = 0.007; + private static final double KI = 0.2; + + + + + private double rollrate; + + private double prevTime = 0; + private double intState = 0; + + private double finPosition = 0; + + + + @Override + public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) { + // Store the current roll rate for later use + rollrate = flightConditions.getRollRate(); + return null; + } + + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + + // Activate PID controller only after a specific time + if (status.getSimulationTime() < START_TIME) { + prevTime = status.getSimulationTime(); + return; + } + + // Find the fin set named CONTROL + FinSet finset = null; + for (RocketComponent c : status.getConfiguration()) { + if ((c instanceof FinSet) && (c.getName().equals(CONTROL_FIN_NAME))) { + finset = (FinSet) c; + break; + } + } + if (finset == null) { + throw new SimulationException("A fin set with name '" + CONTROL_FIN_NAME + "' was not found"); + } + + + // Determine time step + double deltaT = status.getSimulationTime() - prevTime; + prevTime = status.getSimulationTime(); + + + // PID controller + double error = SETPOINT - rollrate; + + double p = KP * error; + intState += error * deltaT; + double i = KI * intState; + + double value = p + i; + + + // Clamp the fin angle between -MAX_ANGLE and MAX_ANGLE + if (Math.abs(value) > MAX_ANGLE) { + System.err.printf("Attempting to set angle %.1f at t=%.3f, clamping.\n", + value * 180 / Math.PI, status.getSimulationTime()); + value = MathUtil.clamp(value, -MAX_ANGLE, MAX_ANGLE); + } + + + // Limit the fin turn rate + if (finPosition < value) { + finPosition = Math.min(finPosition + TURNRATE * deltaT, value); + } else { + finPosition = Math.max(finPosition - TURNRATE * deltaT, value); + } + + // Set the control fin cant and store the data + finset.setCantAngle(finPosition); + status.getFlightData().setValue(FIN_CANT_TYPE, finPosition); + + } +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java b/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java new file mode 100644 index 00000000..1dd74436 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java @@ -0,0 +1,61 @@ +package net.sf.openrocket.simulation.listeners.example; + +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + +/** + * A simulation listener that stops the simulation after a specified number of steps or + * after a specified abount of simulation time. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class StopSimulationListener extends AbstractSimulationListener { + + private final int REPORT = 500; + + private final double stopTime; + private final int stopStep; + + private int step = 0; + + private long startTime = -1; + private long time = -1; + + public StopSimulationListener(double t, int n) { + stopTime = t; + stopStep = n; + } + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { + + if (event.getType() == FlightEvent.Type.LAUNCH) { + System.out.println("Simulation starting."); + time = System.nanoTime(); + startTime = System.nanoTime(); + } + + return true; + } + + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + step++; + if ((step % REPORT) == 0) { + long t = System.nanoTime(); + + System.out.printf("Step %4d, time=%.3f, took %d us/step (avg. %d us/step)\n", + step, status.getSimulationTime(), (t - time) / 1000 / REPORT, (t - startTime) / 1000 / step); + time = t; + } + if (status.getSimulationTime() >= stopTime || step >= stopStep) { + System.out.printf("Stopping simulation, step=%d time=%.3f\n", step, status.getSimulationTime()); + status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, + status.getSimulationTime(), null)); + } + } + +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/system/ApogeeEndListener.java b/core/src/net/sf/openrocket/simulation/listeners/system/ApogeeEndListener.java new file mode 100644 index 00000000..60081224 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/system/ApogeeEndListener.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.simulation.listeners.system; + +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + + +/** + * A simulation listeners that ends the simulation when apogee is reached. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class ApogeeEndListener extends AbstractSimulationListener { + + public static final ApogeeEndListener INSTANCE = new ApogeeEndListener(); + + @Override + public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { + if (event.getType() == FlightEvent.Type.APOGEE) { + status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); + } + return true; + } + + @Override + public boolean isSystemListener() { + return true; + } +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/system/InterruptListener.java b/core/src/net/sf/openrocket/simulation/listeners/system/InterruptListener.java new file mode 100644 index 00000000..e2460098 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/system/InterruptListener.java @@ -0,0 +1,31 @@ +package net.sf.openrocket.simulation.listeners.system; + +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationCancelledException; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + + +/** + * A simulation listener that throws a {@link SimulationCancelledException} if + * this thread has been interrupted. The conditions is checked every time a step + * is taken. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class InterruptListener extends AbstractSimulationListener { + + public static final InterruptListener INSTANCE = new InterruptListener(); + + @Override + public void postStep(SimulationStatus status) throws SimulationException { + if (Thread.interrupted()) { + throw new SimulationCancelledException("The simulation was interrupted."); + } + } + + @Override + public boolean isSystemListener() { + return true; + } +} diff --git a/core/src/net/sf/openrocket/simulation/listeners/system/RecoveryDeviceDeploymentEndListener.java b/core/src/net/sf/openrocket/simulation/listeners/system/RecoveryDeviceDeploymentEndListener.java new file mode 100644 index 00000000..b6bac333 --- /dev/null +++ b/core/src/net/sf/openrocket/simulation/listeners/system/RecoveryDeviceDeploymentEndListener.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.simulation.listeners.system; + +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.simulation.FlightEvent; +import net.sf.openrocket.simulation.SimulationStatus; +import net.sf.openrocket.simulation.exception.SimulationException; +import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; + + +/** + * A simulation listeners that ends the simulation when apogee is reached. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class RecoveryDeviceDeploymentEndListener extends AbstractSimulationListener { + + public static final RecoveryDeviceDeploymentEndListener INSTANCE = new RecoveryDeviceDeploymentEndListener(); + + @Override + public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { + status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); + return true; + } + + @Override + public boolean isSystemListener() { + return true; + } +} diff --git a/core/src/net/sf/openrocket/startup/Application.java b/core/src/net/sf/openrocket/startup/Application.java new file mode 100644 index 00000000..7718b67e --- /dev/null +++ b/core/src/net/sf/openrocket/startup/Application.java @@ -0,0 +1,164 @@ +package net.sf.openrocket.startup; + +import net.sf.openrocket.database.MotorDatabase; +import net.sf.openrocket.l10n.ClassBasedTranslator; +import net.sf.openrocket.l10n.DebugTranslator; +import net.sf.openrocket.l10n.ExceptionSuppressingTranslator; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.LogLevel; +import net.sf.openrocket.logging.LogLevelBufferLogger; +import net.sf.openrocket.logging.PrintStreamLogger; + +/** + * A class that provides singleton instances / beans for other classes to utilize. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public final class Application { + + private static LogHelper logger; + private static LogLevelBufferLogger logBuffer; + + private static Translator baseTranslator = new DebugTranslator(null); + + private static MotorDatabase motorSetDatabase; + + private static Preferences preferences; + + private static ExceptionHandler exceptionHandler; + + // Initialize the logger to something sane for testing without executing Startup + static { + setLogOutputLevel(LogLevel.DEBUG); + } + + /** + * Return whether to use additional safety code checks. + */ + public static boolean useSafetyChecks() { + // Currently default to false unless openrocket.debug.safetycheck is defined + String s = System.getProperty("openrocket.debug.safetycheck"); + if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) { + return true; + } + return false; + } + + /** + * Retrieve the logger to be used in logging. By default this returns + * a logger that outputs to stdout/stderr even if not separately initialized, + * useful for development and debugging. + */ + public static LogHelper getLogger() { + return logger; + } + + /** + * Set the logger to be used in logging. Note that calling this will only have effect + * on not-yet loaded classes, as the instance is stored in a static variable. + */ + public static void setLogger(LogHelper logger) { + Application.logger = logger; + } + + + + /** + * Return the log buffer. + * + * @return the logBuffer or null if not initialized + */ + public static LogLevelBufferLogger getLogBuffer() { + return logBuffer; + } + + /** + * Set the log buffer logger. The logger must be separately configured + * to receive the logging. + */ + public static void setLogBuffer(LogLevelBufferLogger logBuffer) { + Application.logBuffer = logBuffer; + } + + + /** + * Set the logging to output the specified log level and upwards to standard output. + * + * @param level the minimum logging level to output. + */ + public static void setLogOutputLevel(LogLevel level) { + logger = new PrintStreamLogger(); + for (LogLevel l : LogLevel.values()) { + if (l.atLeast(level)) { + ((PrintStreamLogger) logger).setOutput(l, System.out); + } + } + + } + + + /** + * Return the translator to use for obtaining translated strings. + * @return a translator. + */ + public static Translator getTranslator() { + Translator t = baseTranslator; + t = new ClassBasedTranslator(t, 1); + t = new ExceptionSuppressingTranslator(t); + return t; + } + + /** + * Set the translator used in obtaining translated strings. + * @param translator the translator to set. + */ + public static void setBaseTranslator(Translator translator) { + Application.baseTranslator = translator; + } + + + /** + * @return the preferences + */ + public static Preferences getPreferences() { + return preferences; + } + + /** + * @param preferences the preferences to set + */ + public static void setPreferences(Preferences preferences) { + Application.preferences = preferences; + } + + /** + * @return the exceptionHandler + */ + public static ExceptionHandler getExceptionHandler() { + return exceptionHandler; + } + + /** + * @param exceptionHandler the exceptionHandler to set + */ + public static void setExceptionHandler(ExceptionHandler exceptionHandler) { + Application.exceptionHandler = exceptionHandler; + } + + /** + * Return the database of all thrust curves loaded into the system. + */ + public static MotorDatabase getMotorSetDatabase() { + return motorSetDatabase; + } + + /** + * Set the database of thrust curves loaded into the system. + */ + public static void setMotorSetDatabase(MotorDatabase motorSetDatabase) { + Application.motorSetDatabase = motorSetDatabase; + } + + +} diff --git a/core/src/net/sf/openrocket/startup/ExceptionHandler.java b/core/src/net/sf/openrocket/startup/ExceptionHandler.java new file mode 100644 index 00000000..fa115322 --- /dev/null +++ b/core/src/net/sf/openrocket/startup/ExceptionHandler.java @@ -0,0 +1,12 @@ +package net.sf.openrocket.startup; + +public interface ExceptionHandler { + + public void handleErrorCondition(String message); + public void handleErrorCondition(String message, Throwable exception); + public void handleErrorCondition(final Throwable exception); + + + public void uncaughtException(final Thread thread, final Throwable throwable); + +} diff --git a/core/src/net/sf/openrocket/startup/Preferences.java b/core/src/net/sf/openrocket/startup/Preferences.java new file mode 100644 index 00000000..0a5e62b9 --- /dev/null +++ b/core/src/net/sf/openrocket/startup/Preferences.java @@ -0,0 +1,407 @@ +package net.sf.openrocket.startup; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import net.sf.openrocket.database.Databases; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.material.Material; +import net.sf.openrocket.rocketcomponent.BodyComponent; +import net.sf.openrocket.rocketcomponent.FinSet; +import net.sf.openrocket.rocketcomponent.InternalComponent; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassObject; +import net.sf.openrocket.rocketcomponent.RecoveryDevice; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.BuildProperties; +import net.sf.openrocket.util.Color; +import net.sf.openrocket.util.LineStyle; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.UniqueID; + +public abstract class Preferences { + + /* + * Well known string keys to preferences. + * There are other strings out there in the source as well. + */ + public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition"; + public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves"; + public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation"; + // Preferences related to data export + public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator"; + public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment"; + public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment"; + public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments"; + public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter"; + public static final String USER_LOCAL = "locale"; + + public static final String PLOT_SHOW_POINTS = "ShowPlotPoints"; + + private static final String CHECK_UPDATES = "CheckUpdates"; + public static final String LAST_UPDATE = "LastUpdateVersion"; + + public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch"; + public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar"; + + // Node names + public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors"; + + /* + * ****************************************************************************************** + * + * Abstract methods which must be implemented by any derived class. + */ + public abstract boolean getBoolean( String key, boolean defaultValue ); + public abstract void putBoolean( String key, boolean value ); + + public abstract int getInt( String key, int defaultValue); + public abstract void putInt( String key, int value ); + + public abstract double getDouble( String key, double defaultValue ); + public abstract void putDouble( String key, double value ); + + public abstract String getString( String key, String defaultValue ); + public abstract void putString( String key, String value ); + + /** + * Directory represents a way to collect multiple keys together. Implementors may + * choose to concatenate the directory with the key using some special character. + * @param directory + * @param key + * @param defaultValue + * @return + */ + public abstract String getString( String directory, String key, String defaultValue); + + public abstract void putString( String directory, String key, String value ); + + /* + * ****************************************************************************************** + */ + public final boolean getCheckUpdates() { + return this.getBoolean(CHECK_UPDATES, BuildProperties.getDefaultCheckUpdates()); + } + + public final void setCheckUpdates(boolean check) { + this.putBoolean(CHECK_UPDATES, check); + } + + public final double getDefaultMach() { + // TODO: HIGH: implement custom default mach number + return 0.3; + } + + /** + * Return the OpenRocket unique ID. + * + * @return a random ID string that stays constant between OpenRocket executions + */ + public final String getUniqueID() { + String id = this.getString("id", null); + if (id == null) { + id = UniqueID.uuid(); + this.putString("id", id); + } + return id; + } + + /** + * Returns a limited-range integer value from the preferences. If the value + * in the preferences is negative or greater than max, then the default value + * is returned. + * + * @param key The preference to retrieve. + * @param max Maximum allowed value for the choice. + * @param def Default value. + * @return The preference value. + */ + public final int getChoice(String key, int max, int def) { + int v = this.getInt(key, def); + if ((v < 0) || (v > max)) + return def; + return v; + } + + /** + * Helper method that puts an integer choice value into the preferences. + * + * @param key the preference key. + * @param value the value to store. + */ + public final void putChoice(String key, int value) { + this.putInt(key, value); + } + + /** + * Retrieve an enum value from the user preferences. + * + * @param <T> the enum type + * @param key the key + * @param def the default value, cannot be null + * @return the value in the preferences, or the default value + */ + public final <T extends Enum<T>> T getEnum(String key, T def) { + if (def == null) { + throw new BugException("Default value cannot be null"); + } + + String value = getString(key, null); + if (value == null) { + return def; + } + + try { + return Enum.valueOf(def.getDeclaringClass(), value); + } catch (IllegalArgumentException e) { + return def; + } + } + + /** + * Store an enum value to the user preferences. + * + * @param key the key + * @param value the value to store, or null to remove the value + */ + public final void putEnum(String key, Enum<?> value) { + if (value == null) { + putString(key, null); + } else { + putString(key, value.name()); + } + } + + public Color getDefaultColor(Class<? extends RocketComponent> c) { + String color = get("componentColors", c, DEFAULT_COLORS); + if (color == null) + return Color.BLACK; + + Color clr = parseColor(color); + if (clr != null) { + return clr; + } else { + return Color.BLACK; + } + } + + public final void setDefaultColor(Class<? extends RocketComponent> c, Color color) { + if (color == null) + return; + putString("componentColors", c.getSimpleName(), stringifyColor(color)); + } + + + /** + * Retrieve a Line style for the given component. + * @param c + * @return + */ + public final LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) { + String value = get("componentStyle", c, DEFAULT_LINE_STYLES); + try { + return LineStyle.valueOf(value); + } catch (Exception e) { + return LineStyle.SOLID; + } + } + + /** + * Set a default line style for the given component. + * @param c + * @param style + */ + public final void setDefaultLineStyle(Class<? extends RocketComponent> c, + LineStyle style) { + if (style == null) + return; + putString("componentStyle", c.getSimpleName(), style.name()); + } + + /** + * Get the default material type for the given component. + * @param componentClass + * @param type the Material.Type to return. + * @return + */ + public Material getDefaultComponentMaterial( + Class<? extends RocketComponent> componentClass, + Material.Type type) { + + String material = get("componentMaterials", componentClass, null); + if (material != null) { + try { + Material m = Material.fromStorableString(material, false); + if (m.getType() == type) + return m; + } catch (IllegalArgumentException ignore) { + } + } + + switch (type) { + case LINE: + return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL; + case SURFACE: + return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL; + case BULK: + return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL; + } + throw new IllegalArgumentException("Unknown material type: " + type); + } + + /** + * Set the default material for a component type. + * @param componentClass + * @param material + */ + public void setDefaultComponentMaterial( + Class<? extends RocketComponent> componentClass, Material material) { + + putString("componentMaterials", componentClass.getSimpleName(), + material == null ? null : material.toStorableString()); + } + + /** + * get a net.sf.openrocket.util.Color object for the given key. + * @param key + * @param defaultValue + * @return + */ + public final Color getColor( String key, Color defaultValue ) { + Color c = parseColor( getString(key,null) ); + if ( c == null ) { + return defaultValue; + } + return c; + } + + /** + * set a net.sf.openrocket.util.Color preference value for the given key. + * @param key + * @param value + */ + public final void putColor( String key, Color value ) { + putString( key, stringifyColor(value) ); + } + + /** + * Helper function to convert a string representation into a net.sf.openrocket.util.Color object. + * @param color + * @return + */ + protected static Color parseColor(String color) { + if (color == null) { + return null; + } + + String[] rgb = color.split(","); + if (rgb.length == 3) { + try { + int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255); + int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255); + int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255); + return new Color(red, green, blue); + } catch (NumberFormatException ignore) { + } + } + return null; + } + + /** + * Helper function to convert a net.sf.openrocket.util.Color object into a + * String before storing in a preference. + * @param color + * @return + */ + protected static String stringifyColor(Color color) { + String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue(); + return string; + } + + /** + * Special helper function which allows for a map of default values. + * + * First getString(directory,componentClass.getSimpleName(), null) is invoked, + * if the returned value is null, the defaultMap is consulted for a value. + * + * @param directory + * @param componentClass + * @param defaultMap + * @return + */ + protected String get(String directory, + Class<? extends RocketComponent> componentClass, + Map<Class<?>, String> defaultMap) { + + // Search preferences + Class<?> c = componentClass; + while (c != null && RocketComponent.class.isAssignableFrom(c)) { + String value = this.getString(directory, c.getSimpleName(), null); + if (value != null) + return value; + c = c.getSuperclass(); + } + + if (defaultMap == null) + return null; + + // Search defaults + c = componentClass; + while (RocketComponent.class.isAssignableFrom(c)) { + String value = defaultMap.get(c); + if (value != null) + return value; + c = c.getSuperclass(); + } + + return null; + } + + public abstract void addUserMaterial(Material m); + public abstract Set<Material> getUserMaterials(); + public abstract void removeUserMaterial(Material m); + + /* + * Map of default line styles + */ + private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES = + new HashMap<Class<?>, String>(); + static { + DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name()); + DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name()); + } + + /* + * Within a holder class so they will load only when needed. + */ + private static class DefaultMaterialHolder { + private static final Translator trans = Application.getTranslator(); + + //// Elastic cord (round 2mm, 1/16 in) + private static final Material DEFAULT_LINE_MATERIAL = + Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"), + 0.0018, false); + //// Ripstop nylon + private static final Material DEFAULT_SURFACE_MATERIAL = + Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false); + //// Cardboard + private static final Material DEFAULT_BULK_MATERIAL = + Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false); + } + + private static final HashMap<Class<?>, String> DEFAULT_COLORS = + new HashMap<Class<?>, String>(); + static { + DEFAULT_COLORS.put(BodyComponent.class, "0,0,240"); + DEFAULT_COLORS.put(FinSet.class, "0,0,200"); + DEFAULT_COLORS.put(LaunchLug.class, "0,0,180"); + DEFAULT_COLORS.put(InternalComponent.class, "170,0,100"); + DEFAULT_COLORS.put(MassObject.class, "0,0,0"); + DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0"); + } + + + +} diff --git a/core/src/net/sf/openrocket/startup/Startup.java b/core/src/net/sf/openrocket/startup/Startup.java new file mode 100644 index 00000000..d288ea76 --- /dev/null +++ b/core/src/net/sf/openrocket/startup/Startup.java @@ -0,0 +1,203 @@ +package net.sf.openrocket.startup; + +import java.io.PrintStream; +import java.util.Locale; +import java.util.prefs.Preferences; + +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.DebugTranslator; +import net.sf.openrocket.l10n.L10N; +import net.sf.openrocket.l10n.ResourceBundleTranslator; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.logging.DelegatorLogger; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.LogLevel; +import net.sf.openrocket.logging.LogLevelBufferLogger; +import net.sf.openrocket.logging.PrintStreamLogger; + + +/** + * The first class in the OpenRocket startup sequence. This class is responsible + * for setting up the Application class with the statically used subsystems + * (logging and translation) and then delegating to Startup2 class. + * <p> + * This class must be very cautious about what classes it calls. This is because + * the loggers/translators for classes are initialized as static final members during + * class initialization. For example, this class MUST NOT use the Prefs class, because + * using it will cause LineStyle to be initialized, which then receives an invalid + * (not-yet-initialized) translator. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Startup { + + static LogHelper log; + + private static final String LOG_STDERR_PROPERTY = "openrocket.log.stderr"; + private static final String LOG_STDOUT_PROPERTY = "openrocket.log.stdout"; + + private static final int LOG_BUFFER_LENGTH = 50; + + + /** + * OpenRocket startup main method. + */ + public static void main(final String[] args) throws Exception { + + // Check for "openrocket.debug" property before anything else + checkDebugStatus(); + + // Initialize logging first so we can use it + initializeLogging(); + + Application.setPreferences( new SwingPreferences() ); + + // Setup the translations + initializeL10n(); + + // Continue startup in Startup2 class (where Application is already set up) + Startup2.runMain(args); + + } + + + + /** + * Set proper system properties if openrocket.debug is defined. + */ + private static void checkDebugStatus() { + if (System.getProperty("openrocket.debug") != null) { + setPropertyIfNotSet("openrocket.log.stdout", "VBOSE"); + setPropertyIfNotSet("openrocket.log.tracelevel", "VBOSE"); + setPropertyIfNotSet("openrocket.debug.menu", "true"); + setPropertyIfNotSet("openrocket.debug.mutexlocation", "true"); + setPropertyIfNotSet("openrocket.debug.motordigest", "true"); + } + } + + private static void setPropertyIfNotSet(String key, String value) { + if (System.getProperty(key) == null) { + System.setProperty(key, value); + } + } + + + + /** + * Initializes the loggins system. + */ + private static void initializeLogging() { + DelegatorLogger delegator = new DelegatorLogger(); + + // Log buffer + LogLevelBufferLogger buffer = new LogLevelBufferLogger(LOG_BUFFER_LENGTH); + delegator.addLogger(buffer); + + // Check whether to log to stdout/stderr + PrintStreamLogger printer = new PrintStreamLogger(); + boolean logout = setLogOutput(printer, System.out, System.getProperty(LOG_STDOUT_PROPERTY), null); + boolean logerr = setLogOutput(printer, System.err, System.getProperty(LOG_STDERR_PROPERTY), LogLevel.ERROR); + if (logout || logerr) { + delegator.addLogger(printer); + } + + // Set the loggers + Application.setLogger(delegator); + Application.setLogBuffer(buffer); + + // Initialize the log for this class + log = Application.getLogger(); + log.info("Logging subsystem initialized"); + String str = "Console logging output:"; + for (LogLevel l : LogLevel.values()) { + PrintStream ps = printer.getOutput(l); + str += " " + l.name() + ":"; + if (ps == System.err) { + str += "stderr"; + } else if (ps == System.out) { + str += "stdout"; + } else { + str += "none"; + } + } + str += " (" + LOG_STDOUT_PROPERTY + "=" + System.getProperty(LOG_STDOUT_PROPERTY) + + " " + LOG_STDERR_PROPERTY + "=" + System.getProperty(LOG_STDERR_PROPERTY) + ")"; + log.info(str); + } + + private static boolean setLogOutput(PrintStreamLogger logger, PrintStream stream, String level, LogLevel defaultLevel) { + LogLevel minLevel = LogLevel.fromString(level, defaultLevel); + if (minLevel == null) { + return false; + } + + for (LogLevel l : LogLevel.values()) { + if (l.atLeast(minLevel)) { + logger.setOutput(l, stream); + } + } + return true; + } + + + + + /** + * Initializes the localization system. + */ + private static void initializeL10n() { + + // Check for locale propery + String langcode = System.getProperty("openrocket.locale"); + + if (langcode != null) { + + Locale l = L10N.toLocale(langcode); + log.info("Setting custom locale " + l); + Locale.setDefault(l); + + } else { + + // Check user-configured locale + Locale l = getUserLocale(); + if (l != null) { + log.info("Setting user-selected locale " + l); + Locale.setDefault(l); + } else { + log.info("Using default locale " + Locale.getDefault()); + } + + } + + // Setup the translator + Translator t; + t = new ResourceBundleTranslator("l10n.messages"); + if (Locale.getDefault().getLanguage().equals("xx")) { + t = new DebugTranslator(t); + } + + log.info("Set up translation for locale " + Locale.getDefault() + + ", debug.currentFile=" + t.get("debug.currentFile")); + + Application.setBaseTranslator(t); + } + + + + + private static Locale getUserLocale() { + /* + * This method MUST NOT use the Prefs class, since is causes a multitude + * of classes to be initialized. Therefore this duplicates the functionality + * of the Prefs class locally. + */ + + if (System.getProperty("openrocket.debug.prefs") != null) { + return null; + } + + return L10N.toLocale(Preferences.userRoot().node("OpenRocket").get("locale", null)); + } + + +} diff --git a/core/src/net/sf/openrocket/startup/Startup2.java b/core/src/net/sf/openrocket/startup/Startup2.java new file mode 100644 index 00000000..11ceb190 --- /dev/null +++ b/core/src/net/sf/openrocket/startup/Startup2.java @@ -0,0 +1,294 @@ +package net.sf.openrocket.startup; + +import java.awt.GraphicsEnvironment; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.swing.SwingUtilities; +import javax.swing.Timer; +import javax.swing.ToolTipManager; + +import net.sf.openrocket.communication.UpdateInfo; +import net.sf.openrocket.communication.UpdateInfoRetriever; +import net.sf.openrocket.database.Databases; +import net.sf.openrocket.database.ThrustCurveMotorSet; +import net.sf.openrocket.database.ThrustCurveMotorSetDatabase; +import net.sf.openrocket.file.iterator.DirectoryIterator; +import net.sf.openrocket.file.iterator.FileIterator; +import net.sf.openrocket.file.motor.MotorLoaderHelper; +import net.sf.openrocket.gui.dialogs.UpdateInfoDialog; +import net.sf.openrocket.gui.main.BasicFrame; +import net.sf.openrocket.gui.main.Splash; +import net.sf.openrocket.gui.main.SwingExceptionHandler; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.gui.util.SimpleFileFilter; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.util.BuildProperties; + +/** + * The second class in the OpenRocket startup sequence. This class can assume the + * Application class to be properly set up, and can use any classes safely. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Startup2 { + private static final LogHelper log = Application.getLogger(); + + + private static final String THRUSTCURVE_DIRECTORY = "datafiles/thrustcurves/"; + + /** Block motor loading for this many milliseconds */ + private static AtomicInteger blockLoading = new AtomicInteger(Integer.MAX_VALUE); + + + + /** + * Run when starting up OpenRocket after Application has been set up. + * + * @param args command line arguments + */ + static void runMain(final String[] args) throws Exception { + + log.info("Starting up OpenRocket version " + BuildProperties.getVersion()); + + // Check that we're not running headless + log.info("Checking for graphics head"); + checkHead(); + + // Check that we're running a good version of a JRE + log.info("Checking JRE compatibility"); + VersionHelper.checkVersion(); + VersionHelper.checkOpenJDK(); + + // Run the actual startup method in the EDT since it can use progress dialogs etc. + log.info("Moving startup to EDT"); + SwingUtilities.invokeAndWait(new Runnable() { + @Override + public void run() { + runInEDT(args); + } + }); + + log.info("Startup complete"); + } + + + /** + * Run in the EDT when starting up OpenRocket. + * + * @param args command line arguments + */ + private static void runInEDT(String[] args) { + + // Initialize the splash screen with version info + log.info("Initializing the splash screen"); + Splash.init(); + + // Setup the uncaught exception handler + log.info("Registering exception handler"); + SwingExceptionHandler exceptionHandler = new SwingExceptionHandler(); + Application.setExceptionHandler(exceptionHandler); + exceptionHandler.registerExceptionHandler(); + + // Start update info fetching + final UpdateInfoRetriever updateInfo; + if ( Application.getPreferences().getCheckUpdates()) { + log.info("Starting update check"); + updateInfo = new UpdateInfoRetriever(); + updateInfo.start(); + } else { + log.info("Update check disabled"); + updateInfo = null; + } + + // Set the best available look-and-feel + log.info("Setting best LAF"); + GUIUtil.setBestLAF(); + + // Set tooltip delay time. Tooltips are used in MotorChooserDialog extensively. + ToolTipManager.sharedInstance().setDismissDelay(30000); + + // Load defaults + ((SwingPreferences) Application.getPreferences()).loadDefaultUnits(); + + // Load motors etc. + log.info("Loading databases"); + loadMotor(); + Databases.fakeMethod(); + + // Starting action (load files or open new document) + log.info("Opening main application window"); + if (!handleCommandLine(args)) { + BasicFrame.newAction(); + } + + // Check whether update info has been fetched or whether it needs more time + log.info("Checking update status"); + checkUpdateStatus(updateInfo); + + // Block motor loading for 1.5 seconds to allow window painting to be faster + blockLoading.set(1500); + } + + + /** + * Check that the JRE is not running headless. + */ + private static void checkHead() { + + if (GraphicsEnvironment.isHeadless()) { + log.error("Application is headless."); + System.err.println(); + System.err.println("OpenRocket cannot currently be run without the graphical " + + "user interface."); + System.err.println(); + System.exit(1); + } + + } + + + private static void loadMotor() { + + log.info("Starting motor loading from " + THRUSTCURVE_DIRECTORY + " in background thread."); + ThrustCurveMotorSetDatabase db = new ThrustCurveMotorSetDatabase(true) { + + @Override + protected void loadMotors() { + + // Block loading until timeout occurs or database is taken into use + log.info("Blocking motor loading while starting up"); + while (!inUse && blockLoading.addAndGet(-100) > 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + log.info("Blocking ended, inUse=" + inUse + " blockLoading=" + blockLoading.get()); + + // Start loading + log.info("Loading motors from " + THRUSTCURVE_DIRECTORY); + long t0 = System.currentTimeMillis(); + int fileCount; + int thrustCurveCount; + + // Load the packaged thrust curves + List<Motor> list; + FileIterator iterator = DirectoryIterator.findDirectory(THRUSTCURVE_DIRECTORY, + new SimpleFileFilter("", false, "eng", "rse")); + if (iterator == null) { + throw new IllegalStateException("Thrust curve directory " + THRUSTCURVE_DIRECTORY + + "not found, distribution built wrong"); + } + list = MotorLoaderHelper.load(iterator); + for (Motor m : list) { + this.addMotor((ThrustCurveMotor) m); + } + fileCount = iterator.getFileCount(); + + thrustCurveCount = list.size(); + + // Load the user-defined thrust curves + for (File file : ((SwingPreferences) Application.getPreferences()).getUserThrustCurveFiles()) { + log.info("Loading motors from " + file); + list = MotorLoaderHelper.load(file); + for (Motor m : list) { + this.addMotor((ThrustCurveMotor) m); + } + fileCount++; + thrustCurveCount += list.size(); + } + + long t1 = System.currentTimeMillis(); + + // Count statistics + int distinctMotorCount = 0; + int distinctThrustCurveCount = 0; + distinctMotorCount = motorSets.size(); + for (ThrustCurveMotorSet set : motorSets) { + distinctThrustCurveCount += set.getMotorCount(); + } + log.info("Motor loading done, took " + (t1 - t0) + " ms to load " + + fileCount + " files/directories containing " + + thrustCurveCount + " thrust curves which contained " + + distinctMotorCount + " distinct motors with " + + distinctThrustCurveCount + " distinct thrust curves."); + } + + }; + db.startLoading(); + Application.setMotorSetDatabase(db); + } + + private static void checkUpdateStatus(final UpdateInfoRetriever updateInfo) { + if (updateInfo == null) + return; + + int delay = 1000; + if (!updateInfo.isRunning()) + delay = 100; + + final Timer timer = new Timer(delay, null); + + ActionListener listener = new ActionListener() { + private int count = 5; + + @Override + public void actionPerformed(ActionEvent e) { + if (!updateInfo.isRunning()) { + timer.stop(); + + String current = BuildProperties.getVersion(); + String last = Application.getPreferences().getString(Preferences.LAST_UPDATE, ""); + + UpdateInfo info = updateInfo.getUpdateInfo(); + if (info != null && info.getLatestVersion() != null && + !current.equals(info.getLatestVersion()) && + !last.equals(info.getLatestVersion())) { + + UpdateInfoDialog infoDialog = new UpdateInfoDialog(info); + infoDialog.setVisible(true); + if (infoDialog.isReminderSelected()) { + Application.getPreferences().putString(Preferences.LAST_UPDATE, ""); + } else { + Application.getPreferences().putString(Preferences.LAST_UPDATE, info.getLatestVersion()); + } + } + } + count--; + if (count <= 0) + timer.stop(); + } + }; + timer.addActionListener(listener); + timer.start(); + } + + /** + * Handles arguments passed from the command line. This may be used either + * when starting the first instance of OpenRocket or later when OpenRocket is + * executed again while running. + * + * @param args the command-line arguments. + * @return whether a new frame was opened or similar user desired action was + * performed as a result. + */ + private static boolean handleCommandLine(String[] args) { + + // Check command-line for files + boolean opened = false; + for (String file : args) { + if (BasicFrame.open(new File(file), null)) { + opened = true; + } + } + return opened; + } + +} diff --git a/core/src/net/sf/openrocket/startup/VersionHelper.java b/core/src/net/sf/openrocket/startup/VersionHelper.java new file mode 100644 index 00000000..097f1dc4 --- /dev/null +++ b/core/src/net/sf/openrocket/startup/VersionHelper.java @@ -0,0 +1,128 @@ +package net.sf.openrocket.startup; + +import java.awt.GraphicsEnvironment; + +import javax.swing.JOptionPane; + +import net.sf.openrocket.logging.LogHelper; + +public class VersionHelper { + + private static final LogHelper log = Application.getLogger(); + + private static final int REQUIRED_MAJOR_VERSION = 1; + private static final int REQUIRED_MINOR_VERSION = 6; + + // OpenJDK 1.6.0_0-b16 is known to work, 1.6.0_0-b12 does not + private static final String BAD_OPENJDK_VERSION = "^1.6.0_0-b([0-9]|1[1-5])$"; + + + /** + * Check that the JRE version is high enough. + */ + static void checkVersion() { + + String[] version = System.getProperty("java.specification.version", "").split("\\."); + + String jreName = System.getProperty("java.vm.name", "(unknown)"); + String jreVersion = System.getProperty("java.runtime.version", "(unknown)"); + String jreVendor = System.getProperty("java.vendor", "(unknown)"); + + log.info("Running JRE " + jreName + " version " + jreVersion + " by " + jreVendor); + + int major, minor; + + try { + major = Integer.parseInt(version[0]); + minor = Integer.parseInt(version[1]); + + if (major < REQUIRED_MAJOR_VERSION || + (major == REQUIRED_MAJOR_VERSION && minor < REQUIRED_MINOR_VERSION)) { + error(new String[] { "Java SE version 6 is required to run OpenRocket.", + "You are currently running " + jreName + " version " + + jreVersion + " by " + jreVendor }); + } + + } catch (RuntimeException e) { + + confirm(new String[] { "The Java version in use could not be detected.", + "OpenRocket requires at least Java SE 6.", + "Continue anyway?" }); + + } + + } + + /** + * Check whether OpenJDK is being used, and if it is warn the user about + * problems and confirm whether to continue. + */ + static void checkOpenJDK() { + + if (System.getProperty("java.runtime.name", "").toLowerCase().indexOf("icedtea") >= 0 || + System.getProperty("java.vm.name", "").toLowerCase().indexOf("openjdk") >= 0) { + + String jreName = System.getProperty("java.vm.name", "(unknown)"); + String jreVersion = System.getProperty("java.runtime.version", "(unknown)"); + String jreVendor = System.getProperty("java.vendor", "(unknown)"); + + if (jreVersion.matches(BAD_OPENJDK_VERSION)) { + + confirm(new String[] { "Old versions of OpenJDK are known to have problems " + + "running OpenRocket.", + " ", + "You are currently running " + jreName + " version " + + jreVersion + " by " + jreVendor, + "Do you want to continue?" }); + } + } + } + + + + /////////// Helper methods ////////// + + /** + * Presents an error message to the user and exits the application. + * + * @param message an array of messages to present. + */ + private static void error(String[] message) { + + System.err.println(); + System.err.println("Error starting OpenRocket:"); + System.err.println(); + for (int i = 0; i < message.length; i++) { + System.err.println(message[i]); + } + System.err.println(); + + + if (!GraphicsEnvironment.isHeadless()) { + + JOptionPane.showMessageDialog(null, message, "Error starting OpenRocket", + JOptionPane.ERROR_MESSAGE); + + } + + System.exit(1); + } + + + /** + * Presents the user with a message dialog and asks whether to continue. + * If the user does not select "Yes" the the application exits. + * + * @param message the message Strings to show. + */ + private static void confirm(String[] message) { + + if (!GraphicsEnvironment.isHeadless()) { + + if (JOptionPane.showConfirmDialog(null, message, "Error starting OpenRocket", + JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { + System.exit(1); + } + } + } +} diff --git a/core/src/net/sf/openrocket/unit/CaliberUnit.java b/core/src/net/sf/openrocket/unit/CaliberUnit.java new file mode 100644 index 00000000..cc93cb51 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/CaliberUnit.java @@ -0,0 +1,133 @@ +package net.sf.openrocket.unit; + +import java.util.Iterator; + +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.SymmetricComponent; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; + + +public class CaliberUnit extends GeneralUnit { + + public static final double DEFAULT_CALIBER = 0.01; + + private final Configuration configuration; + private final Rocket rocket; + + private int rocketModId = -1; + private int configurationModId = -1; + + private double caliber = -1; + + + + + public CaliberUnit(Configuration configuration) { + super(1.0, "cal"); + this.configuration = configuration; + + if (configuration == null) { + this.rocket = null; + } else { + this.rocket = configuration.getRocket(); + } + } + + public CaliberUnit(Rocket rocket) { + super(1.0, "cal"); + this.configuration = null; + this.rocket = rocket; + } + + public CaliberUnit(double reference) { + super(1.0, "cal"); + this.configuration = null; + this.rocket = null; + this.caliber = reference; + + if (reference <= 0) { + throw new IllegalArgumentException("Illegal reference = " + reference); + } + } + + + @Override + public double fromUnit(double value) { + checkCaliber(); + + return value * caliber; + } + + @Override + public double toUnit(double value) { + checkCaliber(); + + return value / caliber; + } + + + + private void checkCaliber() { + if (configuration != null && configuration.getModID() != configurationModId) { + caliber = -1; + configurationModId = configuration.getModID(); + } + if (rocket != null && rocket.getModID() != rocketModId) { + caliber = -1; + rocketModId = rocket.getModID(); + } + if (caliber < 0) { + if (configuration != null) { + caliber = calculateCaliber(configuration); + } else if (rocket != null) { + caliber = calculateCaliber(rocket); + } else { + throw new BugException("Both rocket and configuration are null"); + } + } + } + + + /** + * Calculate the caliber of a rocket configuration. + * + * @param config the rocket configuration + * @return the caliber of the rocket, or the default caliber. + */ + public static double calculateCaliber(Configuration config) { + return calculateCaliber(config.iterator()); + } + + /** + * Calculate the caliber of a rocket. + * + * @param rocket the rocket + * @return the caliber of the rocket, or the default caliber. + */ + public static double calculateCaliber(Rocket rocket) { + return calculateCaliber(rocket.iterator()); + } + + + + private static double calculateCaliber(Iterator<RocketComponent> iterator) { + double cal = 0; + + while (iterator.hasNext()) { + RocketComponent c = iterator.next(); + if (c instanceof SymmetricComponent) { + double r1 = ((SymmetricComponent) c).getForeRadius() * 2; + double r2 = ((SymmetricComponent) c).getAftRadius() * 2; + cal = MathUtil.max(cal, r1, r2); + } + } + + if (cal < 0.0001) + cal = DEFAULT_CALIBER; + + return cal; + } +} diff --git a/core/src/net/sf/openrocket/unit/DegreeUnit.java b/core/src/net/sf/openrocket/unit/DegreeUnit.java new file mode 100644 index 00000000..240e0b84 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/DegreeUnit.java @@ -0,0 +1,29 @@ +package net.sf.openrocket.unit; + +import java.text.DecimalFormat; + +import net.sf.openrocket.util.Chars; + +public class DegreeUnit extends GeneralUnit { + + public DegreeUnit() { + super(Math.PI/180.0, ""+Chars.DEGREE); + } + + @Override + public boolean hasSpace() { + return false; + } + + @Override + public double round(double v) { + return Math.rint(v); + } + + private final DecimalFormat decFormat = new DecimalFormat("0.#"); + @Override + public String toString(double value) { + double val = toUnit(value); + return decFormat.format(val); + } +} diff --git a/core/src/net/sf/openrocket/unit/FixedPrecisionUnit.java b/core/src/net/sf/openrocket/unit/FixedPrecisionUnit.java new file mode 100644 index 00000000..c175d4ef --- /dev/null +++ b/core/src/net/sf/openrocket/unit/FixedPrecisionUnit.java @@ -0,0 +1,137 @@ +package net.sf.openrocket.unit; + +import java.util.ArrayList; + +public class FixedPrecisionUnit extends Unit { + + private final double precision; + private final String formatString; + + public FixedPrecisionUnit(String unit, double precision) { + this(unit, precision, 1.0); + } + + public FixedPrecisionUnit(String unit, double precision, double multiplier) { + super(multiplier, unit); + + this.precision = precision; + + int decimals = 0; + double p = precision; + while ((p - Math.floor(p)) > 0.0000001) { + p *= 10; + decimals++; + } + formatString = "%." + decimals + "f"; + } + + + @Override + public double getNextValue(double value) { + return round(value + precision); + } + + @Override + public double getPreviousValue(double value) { + return round(value - precision); + } + + + @Override + public double round(double value) { + return Math.rint(value/precision)*precision; + } + + + + + @Override + public String toString(double value) { + return String.format(formatString, this.toUnit(value)); + } + + + + // TODO: LOW: This is copied from GeneralUnit, perhaps combine + @Override + public Tick[] getTicks(double start, double end, double minor, double major) { + // Convert values + start = toUnit(start); + end = toUnit(end); + minor = toUnit(minor); + major = toUnit(major); + + if (minor <= 0 || major <= 0 || major < minor) { + throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major); + } + + ArrayList<Tick> ticks = new ArrayList<Tick>(); + + int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable + double minstep; + + // Find the smallest possible step size + double one=1; + while (one > minor) + one /= 10; + while (one < minor) + one *= 10; + // one is the smallest round-ten that is larger than minor + if (one/2 >= minor) { + // smallest step is round-five + minstep = one/2; + mod2 = 2; // Changed later if clashes with major ticks + } else { + minstep = one; + mod2 = 10; // Changed later if clashes with major ticks + } + + // Find step size for major ticks + one = 1; + while (one > major) + one /= 10; + while (one < major) + one *= 10; + if (one/2 >= major) { + // major step is round-five, major-notable is next round-ten + double majorstep = one/2; + mod3 = (int)Math.round(majorstep/minstep); + mod4 = mod3*2; + } else { + // major step is round-ten, major-notable is next round-ten + mod3 = (int)Math.round(one/minstep); + mod4 = mod3*10; + } + // Check for clashes between minor-notable and major-nonnotable + if (mod3 == mod2) { + if (mod2==2) + mod2 = 1; // Every minor tick is notable + else + mod2 = 5; // Every fifth minor tick is notable + } + + + // Calculate starting position + int pos = (int)Math.ceil(start/minstep); +// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4); + while (pos*minstep <= end) { + double unitValue = pos*minstep; + double value = fromUnit(unitValue); + + if (pos%mod4 == 0) + ticks.add(new Tick(value,unitValue,true,true)); + else if (pos%mod3 == 0) + ticks.add(new Tick(value,unitValue,true,false)); + else if (pos%mod2 == 0) + ticks.add(new Tick(value,unitValue,false,true)); + else + ticks.add(new Tick(value,unitValue,false,false)); + + pos++; + } + + return ticks.toArray(new Tick[0]); + } + + +} diff --git a/core/src/net/sf/openrocket/unit/FrequencyUnit.java b/core/src/net/sf/openrocket/unit/FrequencyUnit.java new file mode 100644 index 00000000..7b5d15cf --- /dev/null +++ b/core/src/net/sf/openrocket/unit/FrequencyUnit.java @@ -0,0 +1,25 @@ +package net.sf.openrocket.unit; + +public class FrequencyUnit extends GeneralUnit { + + + public FrequencyUnit(double multiplier, String unit) { + super(multiplier, unit); + } + + + + @Override + public double toUnit(double value) { + double hz = 1/value; + return hz / multiplier; + } + + + @Override + public double fromUnit(double value) { + double hz = value * multiplier; + return 1/hz; + } + +} diff --git a/core/src/net/sf/openrocket/unit/GeneralUnit.java b/core/src/net/sf/openrocket/unit/GeneralUnit.java new file mode 100644 index 00000000..d604fe6e --- /dev/null +++ b/core/src/net/sf/openrocket/unit/GeneralUnit.java @@ -0,0 +1,228 @@ +package net.sf.openrocket.unit; + +import java.util.ArrayList; + +public class GeneralUnit extends Unit { + + private final int significantNumbers; + private final int decimalRounding; + + // Values smaller that this are rounded using decimal rounding + // [pre-calculated as 10^(significantNumbers-1)] + private final double decimalLimit; + + // Pre-calculated as 10^significantNumbers + private final double significantNumbersLimit; + + + public GeneralUnit(double multiplier, String unit) { + this(multiplier, unit, 2, 10); + } + + public GeneralUnit(double multiplier, String unit, int significantNumbers) { + this(multiplier, unit, significantNumbers, 10); + } + + public GeneralUnit(double multiplier, String unit, int significantNumbers, int decimalRounding) { + super(multiplier, unit); + assert(significantNumbers>0); + assert(decimalRounding>0); + + this.significantNumbers = significantNumbers; + this.decimalRounding = decimalRounding; + + double d=1; + double e=10; + for (int i=1; i<significantNumbers; i++) { + d *= 10.0; + e *= 10.0; + } + decimalLimit = d; + significantNumbersLimit = e; + } + + @Override + public double round(double value) { + if (value < decimalLimit) { + // Round to closest 1/decimalRounding + return Math.rint(value*decimalRounding)/decimalRounding; + } else { + // Round to given amount of significant numbers + double m = 1; + while (value >= significantNumbersLimit) { + m *= 10.0; + value /= 10.0; + } + return Math.rint(value)*m; + } + } + + + + + // TODO: LOW: untested + // start, end and scale in this units +// @Override + public ArrayList<Tick> getTicks(double start, double end, double scale) { + ArrayList<Tick> ticks = new ArrayList<Tick>(); + double delta; + int normal, major; + + // TODO: LOW: more fine-grained (e.g. 0||||5||||10||||15||||20) + if (scale <= 1.0/decimalRounding) { + delta = 1.0/decimalRounding; + normal = 1; + major = decimalRounding; + } else if (scale <= 1.0) { + delta = 1.0/decimalRounding; + normal = decimalRounding; + major = decimalRounding*10; + } else { + double r = scale; + delta = 1; + while (r > 10) { + r /= 10; + delta *= 10; + } + normal = 10; + major = 100; // TODO: LOW: More fine-grained with 5 + } + + double v = Math.ceil(start/delta)*delta; + int n = (int)Math.round(v/delta); + +// while (v <= end) { +// if (n%major == 0) +// ticks.add(new Tick(v,Tick.MAJOR)); +// else if (n%normal == 0) +// ticks.add(new Tick(v,Tick.NORMAL)); +// else +// ticks.add(new Tick(v,Tick.MINOR)); +// v += delta; +// n++; +// } + + return ticks; + } + + @Override + public Tick[] getTicks(double start, double end, double minor, double major) { + // Convert values + start = toUnit(start); + end = toUnit(end); + minor = toUnit(minor); + major = toUnit(major); + + if (minor <= 0 || major <= 0 || major < minor) { + throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major); + } + + ArrayList<Tick> ticks = new ArrayList<Tick>(); + + int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable + double minstep; + + // Find the smallest possible step size + double one=1; + while (one > minor) + one /= 10; + while (one < minor) + one *= 10; + // one is the smallest round-ten that is larger than minor + if (one/2 >= minor) { + // smallest step is round-five + minstep = one/2; + mod2 = 2; // Changed later if clashes with major ticks + } else { + minstep = one; + mod2 = 10; // Changed later if clashes with major ticks + } + + // Find step size for major ticks + one = 1; + while (one > major) + one /= 10; + while (one < major) + one *= 10; + if (one/2 >= major) { + // major step is round-five, major-notable is next round-ten + double majorstep = one/2; + mod3 = (int)Math.round(majorstep/minstep); + mod4 = mod3*2; + } else { + // major step is round-ten, major-notable is next round-ten + mod3 = (int)Math.round(one/minstep); + mod4 = mod3*10; + } + // Check for clashes between minor-notable and major-nonnotable + if (mod3 == mod2) { + if (mod2==2) + mod2 = 1; // Every minor tick is notable + else + mod2 = 5; // Every fifth minor tick is notable + } + + + // Calculate starting position + int pos = (int)Math.ceil(start/minstep); +// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4); + while (pos*minstep <= end) { + double unitValue = pos*minstep; + double value = fromUnit(unitValue); + + if (pos%mod4 == 0) + ticks.add(new Tick(value,unitValue,true,true)); + else if (pos%mod3 == 0) + ticks.add(new Tick(value,unitValue,true,false)); + else if (pos%mod2 == 0) + ticks.add(new Tick(value,unitValue,false,true)); + else + ticks.add(new Tick(value,unitValue,false,false)); + + pos++; + } + + return ticks.toArray(new Tick[0]); + } + + + @Override + public double getNextValue(double value) { + // TODO: HIGH: Auto-generated method stub + return value+1; + } + + @Override + public double getPreviousValue(double value) { + // TODO: HIGH: Auto-generated method stub + return value-1; + } + + + ///// TESTING: + + private static void printTicks(double start, double end, double minor, double major) { + Tick[] ticks = Unit.NOUNIT2.getTicks(start, end, minor, major); + String str = "Ticks for ("+start+","+end+","+minor+","+major+"):"; + for (int i=0; i<ticks.length; i++) { + str += " "+ticks[i].value; + if (ticks[i].major) { + if (ticks[i].notable) + str += "*"; + else + str += "o"; + } else { + if (ticks[i].notable) + str += "_"; + else + str += " "; + } + } + System.out.println(str); + } + public static void main(String[] arg) { + printTicks(0,100,1,10); + printTicks(4.7,11.0,0.15,0.7); + } + +} diff --git a/core/src/net/sf/openrocket/unit/RadianUnit.java b/core/src/net/sf/openrocket/unit/RadianUnit.java new file mode 100644 index 00000000..18f1aff6 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/RadianUnit.java @@ -0,0 +1,22 @@ +package net.sf.openrocket.unit; + +import java.text.DecimalFormat; + +public class RadianUnit extends GeneralUnit { + + public RadianUnit() { + super(1,"rad"); + } + + @Override + public double round(double v) { + return Math.rint(v*10.0)/10.0; + } + + private final DecimalFormat decFormat = new DecimalFormat("0.0"); + @Override + public String toString(double value) { + double val = toUnit(value); + return decFormat.format(val); + } +} diff --git a/core/src/net/sf/openrocket/unit/TemperatureUnit.java b/core/src/net/sf/openrocket/unit/TemperatureUnit.java new file mode 100644 index 00000000..23645ceb --- /dev/null +++ b/core/src/net/sf/openrocket/unit/TemperatureUnit.java @@ -0,0 +1,27 @@ +package net.sf.openrocket.unit; + +public class TemperatureUnit extends FixedPrecisionUnit { + + protected final double addition; + + public TemperatureUnit(double multiplier, double addition, String unit) { + super(unit, 1, multiplier); + + this.addition = addition; + } + + @Override + public boolean hasSpace() { + return false; + } + + @Override + public double toUnit(double value) { + return value/multiplier - addition; + } + + @Override + public double fromUnit(double value) { + return (value + addition)*multiplier; + } +} diff --git a/core/src/net/sf/openrocket/unit/Tick.java b/core/src/net/sf/openrocket/unit/Tick.java new file mode 100644 index 00000000..4448aac1 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/Tick.java @@ -0,0 +1,28 @@ +package net.sf.openrocket.unit; + +public final class Tick { + public final double value; + public final double unitValue; + public final boolean major; + public final boolean notable; + + public Tick(double value, double unitValue, boolean major, boolean notable) { + this.value = value; + this.unitValue = unitValue; + this.major = major; + this.notable = notable; + } + + @Override + public String toString() { + String s = "Tick[value="+value; + if (major) + s += ",major"; + else + s += ",minor"; + if (notable) + s += ",notable"; + s+= "]"; + return s; + } +} diff --git a/core/src/net/sf/openrocket/unit/Unit.java b/core/src/net/sf/openrocket/unit/Unit.java new file mode 100644 index 00000000..4703b31b --- /dev/null +++ b/core/src/net/sf/openrocket/unit/Unit.java @@ -0,0 +1,237 @@ +package net.sf.openrocket.unit; + +import java.text.DecimalFormat; + +import net.sf.openrocket.util.Chars; + +public abstract class Unit { + + /** No unit with 2 digit precision */ + public static final Unit NOUNIT2 = new GeneralUnit(1, "" + Chars.ZWSP, 2); + + protected final double multiplier; // meters = units * multiplier + protected final String unit; + + /** + * Creates a new Unit with a given multiplier and unit name. + * + * Multiplier e.g. 1 in = 0.0254 meter + * + * @param multiplier The multiplier to use on the value, 1 this unit == multiplier SI units + * @param unit The unit's short form. + */ + public Unit(double multiplier, String unit) { + if (multiplier == 0) + throw new IllegalArgumentException("Unit has multiplier=0"); + this.multiplier = multiplier; + this.unit = unit; + } + + /** + * Converts from SI units to this unit. The default implementation simply divides by the + * multiplier. + * + * @param value Value in SI unit + * @return Value in these units + */ + public double toUnit(double value) { + return value / multiplier; + } + + /** + * Convert from this type of units to SI units. The default implementation simply + * multiplies by the multiplier. + * + * @param value Value in these units + * @return Value in SI units + */ + public double fromUnit(double value) { + return value * multiplier; + } + + + /** + * Return the unit name. + * + * @return the unit. + */ + public String getUnit() { + return unit; + } + + /** + * Whether the value and unit should be separated by a whitespace. This method + * returns true as most units have a space between the value and unit, but may be + * overridden. + * + * @return true if the value and unit should be separated + */ + public boolean hasSpace() { + return true; + } + + + // Testcases for toString(double) + public static void main(String arg[]) { + System.out.println(NOUNIT2.toString(0.0049)); + System.out.println(NOUNIT2.toString(0.0050)); + System.out.println(NOUNIT2.toString(0.0051)); + System.out.println(NOUNIT2.toString(0.00123)); + System.out.println(NOUNIT2.toString(0.0123)); + System.out.println(NOUNIT2.toString(0.1234)); + System.out.println(NOUNIT2.toString(1.2345)); + System.out.println(NOUNIT2.toString(12.345)); + System.out.println(NOUNIT2.toString(123.456)); + System.out.println(NOUNIT2.toString(1234.5678)); + System.out.println(NOUNIT2.toString(12345.6789)); + System.out.println(NOUNIT2.toString(123456.789)); + System.out.println(NOUNIT2.toString(1234567.89)); + System.out.println(NOUNIT2.toString(12345678.9)); + + System.out.println(NOUNIT2.toString(-0.0049)); + System.out.println(NOUNIT2.toString(-0.0050)); + System.out.println(NOUNIT2.toString(-0.0051)); + System.out.println(NOUNIT2.toString(-0.00123)); + System.out.println(NOUNIT2.toString(-0.0123)); + System.out.println(NOUNIT2.toString(-0.1234)); + System.out.println(NOUNIT2.toString(-1.2345)); + System.out.println(NOUNIT2.toString(-12.345)); + System.out.println(NOUNIT2.toString(-123.456)); + System.out.println(NOUNIT2.toString(-1234.5678)); + System.out.println(NOUNIT2.toString(-12345.6789)); + System.out.println(NOUNIT2.toString(-123456.789)); + System.out.println(NOUNIT2.toString(-1234567.89)); + System.out.println(NOUNIT2.toString(-12345678.9)); + + } + + + @Override + public String toString() { + return unit; + } + + // TODO: Should this use grouping separator ("#,##0.##")? + + private static final DecimalFormat intFormat = new DecimalFormat("#"); + private static final DecimalFormat decFormat = new DecimalFormat("0.##"); + private static final DecimalFormat expFormat = new DecimalFormat("0.00E0"); + + /** + * Format the given value (in SI units) to a string representation of the value in this + * units. An suitable amount of decimals for the unit are used in the representation. + * The unit is not appended to the numerical value. + * + * @param value Value in SI units. + * @return A string representation of the number in these units. + */ + public String toString(double value) { + double val = toUnit(value); + + if (Math.abs(val) > 1E6) { + return expFormat.format(val); + } + if (Math.abs(val) >= 100) { + return intFormat.format(val); + } + if (Math.abs(val) <= 0.005) { + return "0"; + } + + double sign = Math.signum(val); + val = Math.abs(val); + double mul = 1.0; + while (val < 100) { + mul *= 10; + val *= 10; + } + val = Math.rint(val) / mul * sign; + + return decFormat.format(val); + } + + + /** + * Return a string with the specified value and unit. The value is converted into + * this unit. If <code>value</code> is NaN, returns "N/A" (not applicable). + * + * @param value the value to print in SI units. + * @return the value and unit, or "N/A". + */ + public String toStringUnit(double value) { + if (Double.isNaN(value)) + return "N/A"; + + String s = toString(value); + if (hasSpace()) + s += " "; + s += unit; + return s; + } + + + + /** + * Creates a new Value object with the specified value and this unit. + * + * @param value the value to set. + * @return a new Value object. + */ + public Value toValue(double value) { + return new Value(value, this); + } + + + + /** + * Round the value (in the current units) to a precision suitable for rough valuing + * (approximately 2 significant numbers). + * + * @param value Value in current units + * @return Rounded value. + */ + public abstract double round(double value); + + /** + * Return the next rounded value after the given value. + * @param value Value in these units. + * @return The next suitable rounded value. + */ + public abstract double getNextValue(double value); + + /** + * Return the previous rounded value before the given value. + * @param value Value in these units. + * @return The previous suitable rounded value. + */ + public abstract double getPreviousValue(double value); + + //public abstract ArrayList<Tick> getTicks(double start, double end, double scale); + + /** + * Return ticks in the range start - end (in current units). minor is the minimum + * distance between minor, non-notable ticks and major the minimum distance between + * major non-notable ticks. The values are in current units, i.e. no conversion is + * performed. + */ + public abstract Tick[] getTicks(double start, double end, double minor, double major); + + /** + * Compares whether the two units are equal. Equality requires the unit classes, + * multiplier values and units to be equal. + */ + @Override + public boolean equals(Object other) { + if (other == null) + return false; + if (this.getClass() != other.getClass()) + return false; + return ((this.multiplier == ((Unit) other).multiplier) && this.unit.equals(((Unit) other).unit)); + } + + @Override + public int hashCode() { + return this.getClass().hashCode() + this.unit.hashCode(); + } + +} diff --git a/core/src/net/sf/openrocket/unit/UnitGroup.java b/core/src/net/sf/openrocket/unit/UnitGroup.java new file mode 100644 index 00000000..f72f22a9 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/UnitGroup.java @@ -0,0 +1,569 @@ +package net.sf.openrocket.unit; + +import static net.sf.openrocket.util.Chars.*; +import static net.sf.openrocket.util.MathUtil.pow2; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.sf.openrocket.rocketcomponent.Configuration; +import net.sf.openrocket.rocketcomponent.Rocket; + + +/** + * A group of units (eg. length, mass etc.). Contains a list of different units of a same + * quantity. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + +public class UnitGroup { + + public static final UnitGroup UNITS_NONE; + + public static final UnitGroup UNITS_MOTOR_DIMENSIONS; + public static final UnitGroup UNITS_LENGTH; + public static final UnitGroup UNITS_DISTANCE; + + public static final UnitGroup UNITS_AREA; + public static final UnitGroup UNITS_STABILITY; + /** + * This unit group contains only the caliber unit that never scales the originating "SI" value. + * It can be used in cases where the originating value is already in calibers to obtains the correct unit. + */ + public static final UnitGroup UNITS_STABILITY_CALIBERS; + public static final UnitGroup UNITS_VELOCITY; + public static final UnitGroup UNITS_ACCELERATION; + public static final UnitGroup UNITS_MASS; + public static final UnitGroup UNITS_INERTIA; + public static final UnitGroup UNITS_ANGLE; + public static final UnitGroup UNITS_DENSITY_BULK; + public static final UnitGroup UNITS_DENSITY_SURFACE; + public static final UnitGroup UNITS_DENSITY_LINE; + public static final UnitGroup UNITS_FORCE; + public static final UnitGroup UNITS_IMPULSE; + + /** Time in the order of less than a second (time step etc). */ + public static final UnitGroup UNITS_TIME_STEP; + + /** Time in the order of seconds (motor delay etc). */ + public static final UnitGroup UNITS_SHORT_TIME; + + /** Time in the order of the flight time of a rocket. */ + public static final UnitGroup UNITS_FLIGHT_TIME; + public static final UnitGroup UNITS_ROLL; + public static final UnitGroup UNITS_TEMPERATURE; + public static final UnitGroup UNITS_PRESSURE; + public static final UnitGroup UNITS_RELATIVE; + public static final UnitGroup UNITS_ROUGHNESS; + + public static final UnitGroup UNITS_COEFFICIENT; + + // public static final UnitGroup UNITS_FREQUENCY; + + + public static final Map<String, UnitGroup> UNITS; + + + /* + * Note: Units may not use HTML tags. + * + * The scaling value "X" is obtained by "one of this unit is X of SI units" + * Type into Google for example: "1 in^2 in m^2" + */ + static { + UNITS_NONE = new UnitGroup(); + UNITS_NONE.addUnit(Unit.NOUNIT2); + + UNITS_LENGTH = new UnitGroup(); + UNITS_LENGTH.addUnit(new GeneralUnit(0.001, "mm")); + UNITS_LENGTH.addUnit(new GeneralUnit(0.01, "cm")); + UNITS_LENGTH.addUnit(new GeneralUnit(1, "m")); + UNITS_LENGTH.addUnit(new GeneralUnit(0.0254, "in")); + UNITS_LENGTH.addUnit(new GeneralUnit(0.3048, "ft")); + UNITS_LENGTH.setDefaultUnit(1); + + UNITS_MOTOR_DIMENSIONS = new UnitGroup(); + UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.001, "mm")); + UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.01, "cm")); + UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.0254, "in")); + UNITS_MOTOR_DIMENSIONS.setDefaultUnit(0); + + UNITS_DISTANCE = new UnitGroup(); + UNITS_DISTANCE.addUnit(new GeneralUnit(1, "m")); + UNITS_DISTANCE.addUnit(new GeneralUnit(1000, "km")); + UNITS_DISTANCE.addUnit(new GeneralUnit(0.3048, "ft")); + UNITS_DISTANCE.addUnit(new GeneralUnit(0.9144, "yd")); + UNITS_DISTANCE.addUnit(new GeneralUnit(1609.344, "mi")); + UNITS_DISTANCE.addUnit(new GeneralUnit(1852, "nmi")); + + UNITS_AREA = new UnitGroup(); + UNITS_AREA.addUnit(new GeneralUnit(pow2(0.001), "mm" + SQUARED)); + UNITS_AREA.addUnit(new GeneralUnit(pow2(0.01), "cm" + SQUARED)); + UNITS_AREA.addUnit(new GeneralUnit(1, "m" + SQUARED)); + UNITS_AREA.addUnit(new GeneralUnit(pow2(0.0254), "in" + SQUARED)); + UNITS_AREA.addUnit(new GeneralUnit(pow2(0.3048), "ft" + SQUARED)); + UNITS_AREA.setDefaultUnit(1); + + + UNITS_STABILITY = new UnitGroup(); + UNITS_STABILITY.addUnit(new GeneralUnit(0.001, "mm")); + UNITS_STABILITY.addUnit(new GeneralUnit(0.01, "cm")); + UNITS_STABILITY.addUnit(new GeneralUnit(0.0254, "in")); + UNITS_STABILITY.addUnit(new CaliberUnit((Rocket) null)); + UNITS_STABILITY.setDefaultUnit(3); + + UNITS_STABILITY_CALIBERS = new UnitGroup(); + UNITS_STABILITY_CALIBERS.addUnit(new GeneralUnit(1, "cal")); + + + UNITS_VELOCITY = new UnitGroup(); + UNITS_VELOCITY.addUnit(new GeneralUnit(1, "m/s")); + UNITS_VELOCITY.addUnit(new GeneralUnit(1 / 3.6, "km/h")); + UNITS_VELOCITY.addUnit(new GeneralUnit(0.3048, "ft/s")); + UNITS_VELOCITY.addUnit(new GeneralUnit(0.44704, "mph")); + + UNITS_ACCELERATION = new UnitGroup(); + UNITS_ACCELERATION.addUnit(new GeneralUnit(1, "m/s" + SQUARED)); + UNITS_ACCELERATION.addUnit(new GeneralUnit(0.3048, "ft/s" + SQUARED)); + UNITS_ACCELERATION.addUnit(new GeneralUnit(9.80665, "G")); + + UNITS_MASS = new UnitGroup(); + UNITS_MASS.addUnit(new GeneralUnit(0.001, "g")); + UNITS_MASS.addUnit(new GeneralUnit(1, "kg")); + UNITS_MASS.addUnit(new GeneralUnit(0.0283495231, "oz")); + UNITS_MASS.addUnit(new GeneralUnit(0.45359237, "lb")); + + UNITS_INERTIA = new UnitGroup(); + UNITS_INERTIA.addUnit(new GeneralUnit(0.0001, "kg" + DOT + "cm" + SQUARED)); + UNITS_INERTIA.addUnit(new GeneralUnit(1, "kg" + DOT + "m" + SQUARED)); + UNITS_INERTIA.addUnit(new GeneralUnit(1.82899783e-5, "oz" + DOT + "in" + SQUARED)); + UNITS_INERTIA.addUnit(new GeneralUnit(0.000292639653, "lb" + DOT + "in" + SQUARED)); + UNITS_INERTIA.addUnit(new GeneralUnit(0.0421401101, "lb" + DOT + "ft" + SQUARED)); + UNITS_INERTIA.addUnit(new GeneralUnit(1.35581795, "lbf" + DOT + "ft" + DOT + "s" + SQUARED)); + UNITS_INERTIA.setDefaultUnit(1); + + UNITS_ANGLE = new UnitGroup(); + UNITS_ANGLE.addUnit(new DegreeUnit()); + UNITS_ANGLE.addUnit(new FixedPrecisionUnit("rad", 0.01)); + UNITS_ANGLE.addUnit(new GeneralUnit(1.0 / 3437.74677078, "arcmin")); + + UNITS_DENSITY_BULK = new UnitGroup(); + UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1000, "g/cm" + CUBED)); + UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1, "kg/m" + CUBED)); + UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1729.99404, "oz/in" + CUBED)); + UNITS_DENSITY_BULK.addUnit(new GeneralUnit(16.0184634, "lb/ft" + CUBED)); + + UNITS_DENSITY_SURFACE = new UnitGroup(); + UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(10, "g/cm" + SQUARED)); + UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.001, "g/m" + SQUARED)); + UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(1, "kg/m" + SQUARED)); + UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(43.9418487, "oz/in" + SQUARED)); + UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.305151727, "oz/ft" + SQUARED)); + UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(4.88242764, "lb/ft" + SQUARED)); + UNITS_DENSITY_SURFACE.setDefaultUnit(1); + + UNITS_DENSITY_LINE = new UnitGroup(); + UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.001, "g/m")); + UNITS_DENSITY_LINE.addUnit(new GeneralUnit(1, "kg/m")); + UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.0930102465, "oz/ft")); + + UNITS_FORCE = new UnitGroup(); + UNITS_FORCE.addUnit(new GeneralUnit(1, "N")); + UNITS_FORCE.addUnit(new GeneralUnit(4.44822162, "lbf")); + UNITS_FORCE.addUnit(new GeneralUnit(9.80665, "kgf")); + + UNITS_IMPULSE = new UnitGroup(); + UNITS_IMPULSE.addUnit(new GeneralUnit(1, "Ns")); + UNITS_IMPULSE.addUnit(new GeneralUnit(4.44822162, "lbf" + DOT + "s")); + + UNITS_TIME_STEP = new UnitGroup(); + UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("ms", 1, 0.001)); + UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("s", 0.01)); + UNITS_TIME_STEP.setDefaultUnit(1); + + UNITS_SHORT_TIME = new UnitGroup(); + UNITS_SHORT_TIME.addUnit(new GeneralUnit(1, "s")); + + UNITS_FLIGHT_TIME = new UnitGroup(); + UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(1, "s")); + UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(60, "min")); + + UNITS_ROLL = new UnitGroup(); + UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s")); + UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI, "r/s")); + UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI / 60, "rpm")); + UNITS_ROLL.setDefaultUnit(1); + + UNITS_TEMPERATURE = new UnitGroup(); + UNITS_TEMPERATURE.addUnit(new FixedPrecisionUnit("K", 1)); + UNITS_TEMPERATURE.addUnit(new TemperatureUnit(1, 273.15, DEGREE + "C")); + UNITS_TEMPERATURE.addUnit(new TemperatureUnit(5.0 / 9.0, 459.67, DEGREE + "F")); + UNITS_TEMPERATURE.setDefaultUnit(1); + + UNITS_PRESSURE = new UnitGroup(); + UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("mbar", 1, 1.0e2)); + UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("bar", 0.001, 1.0e5)); + UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("atm", 0.001, 1.01325e5)); + UNITS_PRESSURE.addUnit(new GeneralUnit(101325.0 / 760.0, "mmHg")); + UNITS_PRESSURE.addUnit(new GeneralUnit(3386.389, "inHg")); + UNITS_PRESSURE.addUnit(new GeneralUnit(6894.75729, "psi")); + UNITS_PRESSURE.addUnit(new GeneralUnit(1, "Pa")); + + UNITS_RELATIVE = new UnitGroup(); + UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01, 1.0)); + UNITS_RELATIVE.addUnit(new GeneralUnit(0.01, "%")); + UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + PERMILLE, 1, 0.001)); + // UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01, 1.0)); + // UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("%", 1, 0.01)); + // UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + PERMILLE, 1, 0.001)); + UNITS_RELATIVE.setDefaultUnit(1); + + + UNITS_ROUGHNESS = new UnitGroup(); + UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.000001, MICRO + "m")); + UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.0000254, "mil")); + + + UNITS_COEFFICIENT = new UnitGroup(); + UNITS_COEFFICIENT.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01)); // zero-width space + + + // This is not used by OpenRocket, and not extensively tested: + // UNITS_FREQUENCY = new UnitGroup(); + // UNITS_FREQUENCY.addUnit(new GeneralUnit(1, "s")); + // UNITS_FREQUENCY.addUnit(new GeneralUnit(0.001, "ms")); + // UNITS_FREQUENCY.addUnit(new GeneralUnit(0.000001, MICRO + "s")); + // UNITS_FREQUENCY.addUnit(new FrequencyUnit(1, "Hz")); + // UNITS_FREQUENCY.addUnit(new FrequencyUnit(1000, "kHz")); + // UNITS_FREQUENCY.setDefaultUnit(3); + + + HashMap<String, UnitGroup> map = new HashMap<String, UnitGroup>(); + map.put("NONE", UNITS_NONE); + map.put("LENGTH", UNITS_LENGTH); + map.put("MOTOR_DIMENSIONS", UNITS_MOTOR_DIMENSIONS); + map.put("DISTANCE", UNITS_DISTANCE); + map.put("VELOCITY", UNITS_VELOCITY); + map.put("ACCELERATION", UNITS_ACCELERATION); + map.put("AREA", UNITS_AREA); + map.put("STABILITY", UNITS_STABILITY); + map.put("MASS", UNITS_MASS); + map.put("INERTIA", UNITS_INERTIA); + map.put("ANGLE", UNITS_ANGLE); + map.put("DENSITY_BULK", UNITS_DENSITY_BULK); + map.put("DENSITY_SURFACE", UNITS_DENSITY_SURFACE); + map.put("DENSITY_LINE", UNITS_DENSITY_LINE); + map.put("FORCE", UNITS_FORCE); + map.put("IMPULSE", UNITS_IMPULSE); + map.put("TIME_STEP", UNITS_TIME_STEP); + map.put("SHORT_TIME", UNITS_SHORT_TIME); + map.put("FLIGHT_TIME", UNITS_FLIGHT_TIME); + map.put("ROLL", UNITS_ROLL); + map.put("TEMPERATURE", UNITS_TEMPERATURE); + map.put("PRESSURE", UNITS_PRESSURE); + map.put("RELATIVE", UNITS_RELATIVE); + map.put("ROUGHNESS", UNITS_ROUGHNESS); + map.put("COEFFICIENT", UNITS_COEFFICIENT); + + UNITS = Collections.unmodifiableMap(map); + } + + public static void setDefaultMetricUnits() { + UNITS_LENGTH.setDefaultUnit("cm"); + UNITS_MOTOR_DIMENSIONS.setDefaultUnit("mm"); + UNITS_DISTANCE.setDefaultUnit("m"); + UNITS_AREA.setDefaultUnit("cm" + SQUARED); + UNITS_STABILITY.setDefaultUnit("cal"); + UNITS_VELOCITY.setDefaultUnit("m/s"); + UNITS_ACCELERATION.setDefaultUnit("m/s" + SQUARED); + UNITS_MASS.setDefaultUnit("g"); + UNITS_INERTIA.setDefaultUnit("kg" + DOT + "m" + SQUARED); + UNITS_ANGLE.setDefaultUnit("" + DEGREE); + UNITS_DENSITY_BULK.setDefaultUnit("g/cm" + CUBED); + UNITS_DENSITY_SURFACE.setDefaultUnit("g/m" + SQUARED); + UNITS_DENSITY_LINE.setDefaultUnit("g/m"); + UNITS_FORCE.setDefaultUnit("N"); + UNITS_IMPULSE.setDefaultUnit("Ns"); + UNITS_TIME_STEP.setDefaultUnit("s"); + UNITS_FLIGHT_TIME.setDefaultUnit("s"); + UNITS_ROLL.setDefaultUnit("r/s"); + UNITS_TEMPERATURE.setDefaultUnit(DEGREE + "C"); + UNITS_PRESSURE.setDefaultUnit("mbar"); + UNITS_RELATIVE.setDefaultUnit("%"); + UNITS_ROUGHNESS.setDefaultUnit(MICRO + "m"); + } + + public static void setDefaultImperialUnits() { + UNITS_LENGTH.setDefaultUnit("in"); + UNITS_MOTOR_DIMENSIONS.setDefaultUnit("in"); + UNITS_DISTANCE.setDefaultUnit("ft"); + UNITS_AREA.setDefaultUnit("in" + SQUARED); + UNITS_STABILITY.setDefaultUnit("cal"); + UNITS_VELOCITY.setDefaultUnit("ft/s"); + UNITS_ACCELERATION.setDefaultUnit("ft/s" + SQUARED); + UNITS_MASS.setDefaultUnit("oz"); + UNITS_INERTIA.setDefaultUnit("lb" + DOT + "ft" + SQUARED); + UNITS_ANGLE.setDefaultUnit("" + DEGREE); + UNITS_DENSITY_BULK.setDefaultUnit("oz/in" + CUBED); + UNITS_DENSITY_SURFACE.setDefaultUnit("oz/ft" + SQUARED); + UNITS_DENSITY_LINE.setDefaultUnit("oz/ft"); + UNITS_FORCE.setDefaultUnit("N"); + UNITS_IMPULSE.setDefaultUnit("Ns"); + UNITS_TIME_STEP.setDefaultUnit("s"); + UNITS_FLIGHT_TIME.setDefaultUnit("s"); + UNITS_ROLL.setDefaultUnit("r/s"); + UNITS_TEMPERATURE.setDefaultUnit(DEGREE + "F"); + UNITS_PRESSURE.setDefaultUnit("mbar"); + UNITS_RELATIVE.setDefaultUnit("%"); + UNITS_ROUGHNESS.setDefaultUnit("mil"); + } + + + /** + * Return a UnitGroup for stability units based on the rocket. + * + * @param rocket the rocket from which to calculate the caliber + * @return the unit group + */ + public static UnitGroup stabilityUnits(Rocket rocket) { + return new StabilityUnitGroup(rocket); + } + + + /** + * Return a UnitGroup for stability units based on the rocket configuration. + * + * @param config the rocket configuration from which to calculate the caliber + * @return the unit group + */ + public static UnitGroup stabilityUnits(Configuration config) { + return new StabilityUnitGroup(config); + } + + + /** + * Return a UnitGroup for stability units based on a constant caliber. + * + * @param reference the constant reference length + * @return the unit group + */ + public static UnitGroup stabilityUnits(double reference) { + return new StabilityUnitGroup(reference); + } + + + ////////////////////////////////////////////////////// + + + protected ArrayList<Unit> units = new ArrayList<Unit>(); + protected int defaultUnit = 0; + + public int getUnitCount() { + return units.size(); + } + + public Unit getDefaultUnit() { + return units.get(defaultUnit); + } + + public int getDefaultUnitIndex() { + return defaultUnit; + } + + public void setDefaultUnit(int n) { + if (n < 0 || n >= units.size()) { + throw new IllegalArgumentException("index out of range: " + n); + } + defaultUnit = n; + } + + + + /** + * Find a unit by approximate unit name. Only letters and (ordinary) numbers are + * considered in the matching. This method is mainly means for testing, allowing + * a simple means to obtain a particular unit. + * + * @param str the unit name. + * @return the corresponding unit, or <code>null</code> if not found. + */ + public Unit findApproximate(String str) { + str = str.replaceAll("\\W", "").trim(); + for (Unit u : units) { + String name = u.getUnit().replaceAll("\\W", "").trim(); + if (str.equalsIgnoreCase(name)) + return u; + } + return null; + } + + /** + * Set the default unit based on the unit name. Throws an exception if a + * unit with the provided name is not available. + * + * @param name the unit name. + * @throws IllegalArgumentException if the corresponding unit is not found in the group. + */ + public void setDefaultUnit(String name) throws IllegalArgumentException { + for (int i = 0; i < units.size(); i++) { + if (units.get(i).getUnit().equals(name)) { + setDefaultUnit(i); + return; + } + } + throw new IllegalArgumentException("name=" + name); + } + + + public Unit getUnit(int n) { + return units.get(n); + } + + public int getUnitIndex(Unit u) { + return units.indexOf(u); + } + + public void addUnit(Unit u) { + units.add(u); + } + + public boolean contains(Unit u) { + return units.contains(u); + } + + public Unit[] getUnits() { + return units.toArray(new Unit[0]); + } + + + /** + * Return the value formatted by the default unit of this group. + * It is the same as calling <code>getDefaultUnit().toString(value)</code>. + * + * @param value the SI value to format. + * @return the formatted string. + * @see Unit#toString(double) + */ + public String toString(double value) { + return this.getDefaultUnit().toString(value); + } + + + /** + * Return the value formatted by the default unit of this group including the unit. + * It is the same as calling <code>getDefaultUnit().toStringUnit(value)</code>. + * + * @param value the SI value to format. + * @return the formatted string. + * @see Unit#toStringUnit(double) + */ + public String toStringUnit(double value) { + return this.getDefaultUnit().toStringUnit(value); + } + + + + + + /** + * Creates a new Value object with the specified value and the default unit of this group. + * + * @param value the value to set. + * @return a new Value object. + */ + public Value toValue(double value) { + return this.getDefaultUnit().toValue(value); + } + + + + + private static final Pattern STRING_PATTERN = Pattern.compile("^\\s*([0-9.,-]+)(.*?)$"); + + /** + * Converts a string into an SI value. If the string has one of the units in this + * group appended to it, that unit will be used in conversion. Otherwise the default + * unit will be used. If an unknown unit is specified or the value does not parse + * with <code>Double.parseDouble</code> then a <code>NumberFormatException</code> + * is thrown. + * <p> + * This method is applicable only for simple units without e.g. powers. + * + * @param str the string to parse. + * @return the SI value. + * @throws NumberFormatException if the string cannot be parsed. + */ + public double fromString(String str) { + Matcher matcher = STRING_PATTERN.matcher(str); + + if (!matcher.matches()) { + throw new NumberFormatException("string did not match required pattern"); + } + + double value = Double.parseDouble(matcher.group(1)); + String unit = matcher.group(2).trim(); + + if (unit.equals("")) { + value = this.getDefaultUnit().fromUnit(value); + } else { + int i; + for (i = 0; i < units.size(); i++) { + Unit u = units.get(i); + if (unit.equalsIgnoreCase(u.getUnit())) { + value = u.fromUnit(value); + break; + } + } + if (i >= units.size()) { + throw new NumberFormatException("unknown unit " + unit); + } + } + + return value; + } + + + /////////////////////////// + + + /** + * A private class that switches the CaliberUnit to a rocket-specific CaliberUnit. + * All other methods are passed through to UNITS_STABILITY. + */ + private static class StabilityUnitGroup extends UnitGroup { + + public StabilityUnitGroup(double ref) { + this(new CaliberUnit(ref)); + } + + public StabilityUnitGroup(Rocket rocket) { + this(new CaliberUnit(rocket)); + } + + public StabilityUnitGroup(Configuration config) { + this(new CaliberUnit(config)); + } + + private StabilityUnitGroup(CaliberUnit caliberUnit) { + this.units.addAll(UnitGroup.UNITS_STABILITY.units); + this.defaultUnit = UnitGroup.UNITS_STABILITY.defaultUnit; + for (int i = 0; i < units.size(); i++) { + if (units.get(i) instanceof CaliberUnit) { + units.set(i, caliberUnit); + } + } + } + + + @Override + public void setDefaultUnit(int n) { + super.setDefaultUnit(n); + UNITS_STABILITY.setDefaultUnit(n); + } + } +} diff --git a/core/src/net/sf/openrocket/unit/Value.java b/core/src/net/sf/openrocket/unit/Value.java new file mode 100644 index 00000000..65382523 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/Value.java @@ -0,0 +1,153 @@ +package net.sf.openrocket.unit; + +import net.sf.openrocket.util.MathUtil; + +/** + * An immutable class representing an SI value and a unit. The toString() method yields the + * current value in the current units. This class may be used to encapsulate + * a sortable value for example for tables. The sorting is performed by the + * value in the current units, ignoring the unit. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Value implements Comparable<Value> { + + private final double value; + private final Unit unit; + + + /** + * Create a new Value object. + * + * @param value the value to set. + * @param unit the unit to set (<code>null</code> not allowed) + */ + public Value(double value, Unit unit) { + if (unit == null) { + throw new IllegalArgumentException("unit is null"); + } + this.value = value; + this.unit = unit; + } + + + /** + * Creates a new Value object using unit group. Currently it simply uses the default + * unit of the group, but may later change. + * + * @param value the value to set. + * @param group the group the value belongs to. + */ + public Value(double value, UnitGroup group) { + this(value, group.getDefaultUnit()); + } + + + /** + * Get the value of this object (in SI units). + * + * @return the value + */ + public double getValue() { + return value; + } + + + + /** + * Get the value of this object in the current units. + * + * @return the value in the current units. + */ + public double getUnitValue() { + return unit.toUnit(value); + } + + + /** + * Get the unit of this object. + * + * @return the unit + */ + public Unit getUnit() { + return unit; + } + + + /** + * Return a string formatted using the {@link Unit#toStringUnit(double)} method + * of the current unit. If the unit is <code>null</code> then the UNITS_NONE + * group is used. + */ + @Override + public String toString() { + return unit.toStringUnit(value); + } + + + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + + Value other = (Value) obj; + if (this.unit != other.unit) { + return false; + } + + if (!MathUtil.equals(this.value, other.value)) { + return false; + } + + return true; + } + + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((unit == null) ? 0 : unit.hashCode()); + long temp; + temp = Double.doubleToLongBits(value); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + + /** + * Compare this value to another value. The comparison is performed primarily by + * the unit text, secondarily the value in the unit values. + */ + @Override + public int compareTo(Value o) { + int n = this.getUnit().getUnit().compareTo(o.getUnit().getUnit()); + if (n != 0) + return n; + + double us = this.getUnitValue(); + double them = o.getUnitValue(); + + if (Double.isNaN(us)) { + if (Double.isNaN(them)) + return 0; + else + return 1; + } + if (Double.isNaN(them)) + return -1; + + if (us < them) + return -1; + else if (us > them) + return 1; + else + return 0; + } + +} diff --git a/core/src/net/sf/openrocket/unit/ValueComparator.java b/core/src/net/sf/openrocket/unit/ValueComparator.java new file mode 100644 index 00000000..a948e9d2 --- /dev/null +++ b/core/src/net/sf/openrocket/unit/ValueComparator.java @@ -0,0 +1,14 @@ +package net.sf.openrocket.unit; + +import java.util.Comparator; + +public class ValueComparator implements Comparator<Value> { + + public static final ValueComparator INSTANCE = new ValueComparator(); + + @Override + public int compare(Value o1, Value o2) { + return o1.compareTo(o2); + } + +} diff --git a/core/src/net/sf/openrocket/util/AbstractChangeSource.java b/core/src/net/sf/openrocket/util/AbstractChangeSource.java new file mode 100644 index 00000000..57664aed --- /dev/null +++ b/core/src/net/sf/openrocket/util/AbstractChangeSource.java @@ -0,0 +1,48 @@ +package net.sf.openrocket.util; + +import java.util.EventListener; +import java.util.EventObject; +import java.util.List; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * Abstract implementation of a ChangeSource. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public abstract class AbstractChangeSource implements ChangeSource { + private static final LogHelper log = Application.getLogger(); + + private final List<EventListener> listeners = new ArrayList<EventListener>(); + + private final EventObject event = new EventObject(this); + + + @Override + public final void addChangeListener(EventListener listener) { + listeners.add(listener); + log.verbose(1, "Adding change listeners, listener count is now " + listeners.size()); + } + + @Override + public final void removeChangeListener(EventListener listener) { + listeners.remove(listener); + log.verbose(1, "Removing change listeners, listener count is now " + listeners.size()); + } + + + /** + * Fire a change event to all listeners. + */ + protected void fireChangeEvent() { + // Copy the list before iterating to prevent concurrent modification exceptions. + EventListener[] list = listeners.toArray(new EventListener[0]); + for (EventListener l : list) { + if ( l instanceof StateChangeListener ) { + ((StateChangeListener)l).stateChanged(event); + } + } + } +} diff --git a/core/src/net/sf/openrocket/util/ArrayList.java b/core/src/net/sf/openrocket/util/ArrayList.java new file mode 100644 index 00000000..a91deaa4 --- /dev/null +++ b/core/src/net/sf/openrocket/util/ArrayList.java @@ -0,0 +1,30 @@ +package net.sf.openrocket.util; + +import java.util.Collection; + +/** + * An implementation of an ArrayList with a type-safe {@link #clone()} method. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class ArrayList<E> extends java.util.ArrayList<E> { + + public ArrayList() { + super(); + } + + public ArrayList(Collection<? extends E> c) { + super(c); + } + + public ArrayList(int initialCapacity) { + super(initialCapacity); + } + + @SuppressWarnings("unchecked") + @Override + public ArrayList<E> clone() { + return (ArrayList<E>) super.clone(); + } + +} diff --git a/core/src/net/sf/openrocket/util/Base64.java b/core/src/net/sf/openrocket/util/Base64.java new file mode 100644 index 00000000..042d3671 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Base64.java @@ -0,0 +1,232 @@ +package net.sf.openrocket.util; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class Base64 { + + public static final int DEFAULT_CHARS_PER_LINE = 72; + + private static final char[] ALPHABET = new char[] { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' + }; + private static final char PAD = '='; + +// private static final byte[] REVERSE; +// static { +// REVERSE = new byte[128]; +// Arrays.fill(REVERSE, (byte)-1); +// for (int i=0; i<64; i++) { +// REVERSE[ALPHABET[i]] = (byte)i; +// } +// REVERSE['-'] = 62; +// REVERSE['_'] = 63; +// REVERSE[PAD] = 0; +// } + + private static final Map<Character,Integer> REVERSE = new HashMap<Character,Integer>(); + static { + for (int i=0; i<64; i++) { + REVERSE.put(ALPHABET[i], i); + } + REVERSE.put('-', 62); + REVERSE.put('_', 63); + REVERSE.put(PAD, 0); + } + + + public static String encode(byte[] data) { + return encode(data, DEFAULT_CHARS_PER_LINE); + } + + public static String encode(byte[] data, int maxColumn) { + StringBuilder builder = new StringBuilder(); + int column = 0; + + for (int position=0; position < data.length; position+=3) { + if (column+4 > maxColumn) { + builder.append('\n'); + column = 0; + } + builder.append(encodeGroup(data, position)); + column += 4; + } + builder.append('\n'); + return builder.toString(); + } + + + + + public static byte[] decode(String data) { + byte[] array = new byte[data.length()*3/4]; + char[] block = new char[4]; + int length = 0; + + for (int position=0; position < data.length(); ) { + int p; + for (p=0; p<4 && position < data.length(); position++) { + char c = data.charAt(position); + if (!Character.isWhitespace(c)) { + block[p] = c; + p++; + } + } + + if (p==0) + break; + if (p!=4) { + throw new IllegalArgumentException("Data ended when decoding Base64, p="+p); + } + + int l = decodeGroup(block, array, length); + length += l; + if (l < 3) + break; + } + return Arrays.copyOf(array, length); + } + + + //// Helper methods + + + /** + * Encode three bytes of data into four characters. + */ + private static char[] encodeGroup(byte[] data, int position) { + char[] c = new char[] { '=','=','=','=' }; + int b1=0, b2=0, b3=0; + int length = data.length - position; + + if (length == 0) + return c; + + if (length >= 1) { + b1 = ((int)data[position])&0xFF; + } + if (length >= 2) { + b2 = ((int)data[position+1])&0xFF; + } + if (length >= 3) { + b3 = ((int)data[position+2])&0xFF; + } + + c[0] = ALPHABET[b1>>2]; + c[1] = ALPHABET[(b1 & 3)<<4 | (b2>>4)]; + if (length == 1) + return c; + c[2] = ALPHABET[(b2 & 15)<<2 | (b3>>6)]; + if (length == 2) + return c; + c[3] = ALPHABET[b3 & 0x3f]; + return c; + } + + + /** + * Decode four chars from data into 0-3 bytes of data starting at position in array. + * @return the number of bytes decoded. + */ + private static int decodeGroup(char[] data, byte[] array, int position) { + int b1, b2, b3, b4; + + try { + b1 = REVERSE.get(data[0]); + b2 = REVERSE.get(data[1]); + b3 = REVERSE.get(data[2]); + b4 = REVERSE.get(data[3]); + } catch (NullPointerException e) { + // If auto-boxing fails + throw new IllegalArgumentException("Illegal characters in the sequence to be "+ + "decoded: "+Arrays.toString(data)); + } + + array[position] = (byte)((b1 << 2) | (b2 >> 4)); + array[position+1] = (byte)((b2 << 4) | (b3 >> 2)); + array[position+2] = (byte)((b3 << 6) | (b4)); + + // Check the amount of data decoded + if (data[0] == PAD) + return 0; + if (data[1] == PAD) { + throw new IllegalArgumentException("Illegal character padding in sequence to be "+ + "decoded: "+Arrays.toString(data)); + } + if (data[2] == PAD) + return 1; + if (data[3] == PAD) + return 2; + + return 3; + } + + + + public static void main(String[] arg) { + Random rnd = new Random(); + + for (int round=0; round < 1000; round++) { + int n = rnd.nextInt(1000); + n = 100000; + + byte[] array = new byte[n]; + rnd.nextBytes(array); + + String encoded = encode(array); + + System.out.println(encoded); + System.exit(0); +// for (int i=0; i<1000; i++) { +// int pos = rnd.nextInt(encoded.length()); +// String s1 = encoded.substring(0, pos); +// String s2 = encoded.substring(pos); +// switch (rnd.nextInt(15)) { +// case 0: +// encoded = s1 + " " + s2; +// break; +// case 1: +// encoded = s1 + "\u0009" + s2; +// break; +// case 2: +// encoded = s1 + "\n" + s2; +// break; +// case 3: +// encoded = s1 + "\u000B" + s2; +// break; +// case 4: +// encoded = s1 + "\r" + s2; +// break; +// case 5: +// encoded = s1 + "\u000C" + s2; +// break; +// case 6: +// encoded = s1 + "\u001C" + s2; +// break; +// } +// } + + byte[] decoded = null; + try { + decoded = decode(encoded); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + System.err.println("Bad data:\n"+encoded); + System.exit(1); + } + + if (!Arrays.equals(array, decoded)) { + System.err.println("Data differs! n="+n); + System.exit(1); + } + System.out.println("n="+n+" ok!"); + } + } + + +} diff --git a/core/src/net/sf/openrocket/util/BugException.java b/core/src/net/sf/openrocket/util/BugException.java new file mode 100644 index 00000000..8d113add --- /dev/null +++ b/core/src/net/sf/openrocket/util/BugException.java @@ -0,0 +1,22 @@ +package net.sf.openrocket.util; + +/** + * Thrown when a bug is noticed. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class BugException extends FatalException { + + public BugException(String message) { + super("BUG: " + message); + } + + public BugException(Throwable cause) { + super("BUG: " + cause.getMessage(), cause); + } + + public BugException(String message, Throwable cause) { + super("BUG: " + message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/util/BuildProperties.java b/core/src/net/sf/openrocket/util/BuildProperties.java new file mode 100644 index 00000000..46bc3c20 --- /dev/null +++ b/core/src/net/sf/openrocket/util/BuildProperties.java @@ -0,0 +1,75 @@ +package net.sf.openrocket.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.MissingResourceException; +import java.util.Properties; + +public class BuildProperties { + + private static final Properties PROPERTIES; + private static final String BUILD_VERSION; + private static final String BUILD_SOURCE; + private static final boolean DEFAULT_CHECK_UPDATES; + + /** + * Return the OpenRocket version number. + */ + public static String getVersion() { + return BUILD_VERSION; + } + + /** + * Return the OpenRocket build source (e.g. "default" or "Debian") + */ + public static String getBuildSource() { + return BUILD_SOURCE; + } + + public static boolean getDefaultCheckUpdates() { + return DEFAULT_CHECK_UPDATES; + } + + static { + try { + InputStream is = ClassLoader.getSystemResourceAsStream("build.properties"); + if (is == null) { + throw new MissingResourceException( + "build.properties not found, distribution built wrong" + + " classpath:" + System.getProperty("java.class.path"), + "build.properties", "build.version"); + } + + PROPERTIES = new Properties(); + PROPERTIES.load(is); + is.close(); + + String version = PROPERTIES.getProperty("build.version"); + if (version == null) { + throw new MissingResourceException( + "build.version not found in property file", + "build.properties", "build.version"); + } + BUILD_VERSION = version.trim(); + + BUILD_SOURCE = PROPERTIES.getProperty("build.source"); + if (BUILD_SOURCE == null) { + throw new MissingResourceException( + "build.source not found in property file", + "build.properties", "build.source"); + } + + String value = PROPERTIES.getProperty("build.checkupdates"); + if (value != null) + DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value); + else + DEFAULT_CHECK_UPDATES = true; + + } catch (IOException e) { + throw new MissingResourceException( + "Error reading build.properties", + "build.properties", "build.version"); + } + } + +} diff --git a/core/src/net/sf/openrocket/util/ChangeSource.java b/core/src/net/sf/openrocket/util/ChangeSource.java new file mode 100644 index 00000000..f6669ef3 --- /dev/null +++ b/core/src/net/sf/openrocket/util/ChangeSource.java @@ -0,0 +1,15 @@ +package net.sf.openrocket.util; + +import java.util.EventListener; + +/** + * An interface defining an object firing ChangeEvents. Why isn't this included in the Java API?? + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public interface ChangeSource { + + public void addChangeListener(EventListener listener); + public void removeChangeListener(EventListener listener); + +} diff --git a/core/src/net/sf/openrocket/util/Chars.java b/core/src/net/sf/openrocket/util/Chars.java new file mode 100644 index 00000000..eeba22bc --- /dev/null +++ b/core/src/net/sf/openrocket/util/Chars.java @@ -0,0 +1,59 @@ +package net.sf.openrocket.util; + +/** + * A class defining various non-ASCII characters for easier use. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Chars { + + /** The fraction 1/2 */ + public static final char FRAC12 = '\u00BD'; + /** The fraction 1/4 */ + public static final char FRAC14 = '\u00BC'; + /** The fraction 3/4 */ + public static final char FRAC34 = '\u00BE'; + + /** Degree sign */ + public static final char DEGREE = '\u00B0'; + + /** Squared, superscript 2 */ + public static final char SQUARED = '\u00B2'; + /** Cubed, superscript 3 */ + public static final char CUBED = '\u00B3'; + + /** Per mille sign */ + public static final char PERMILLE = '\u2030'; + + /** Middle dot, multiplication */ + public static final char DOT = '\u00B7'; + /** Multiplication sign, cross */ + public static final char TIMES = '\u00D7'; + + /** No-break space */ + public static final char NBSP = '\u00A0'; + /** Zero-width space */ + public static final char ZWSP = '\u200B'; + + /** Em dash */ + public static final char EMDASH = '\u2014'; + + /** Micro sign (Greek letter mu) */ + public static final char MICRO = '\u00B5'; + + /** Alpha */ + public static final char ALPHA = '\u03b1'; + /** Theta */ + public static final char THETA = '\u0398'; + + /** Copyright symbol */ + public static final char COPY = '\u00A9'; + /** A centered bullet */ + public static final char BULLET = '\u2022'; + + /** Left arrow (light) */ + public static final char LEFT_ARROW = '\u2190'; + /** Right arrow (light) */ + public static final char RIGHT_ARROW = '\u2192'; + +} diff --git a/core/src/net/sf/openrocket/util/Color.java b/core/src/net/sf/openrocket/util/Color.java new file mode 100644 index 00000000..5f56a173 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Color.java @@ -0,0 +1,58 @@ +package net.sf.openrocket.util; + +public class Color { + + public static Color BLACK = new Color(255,255,255); + + private int red; + private int green; + private int blue; + private int alpha; + + public Color( int red, int green, int blue ) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = 255; + } + + public Color( int red, int green, int blue, int alpha ) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + + public int getRed() { + return red; + } + + public void setRed(int red) { + this.red = red; + } + + public int getGreen() { + return green; + } + + public void setGreen(int green) { + this.green = green; + } + + public int getBlue() { + return blue; + } + + public void setBlue(int blue) { + this.blue = blue; + } + + public int getAlpha() { + return alpha; + } + + public void setAlpha(int alpha) { + this.alpha = alpha; + } + +} diff --git a/core/src/net/sf/openrocket/util/ComparablePair.java b/core/src/net/sf/openrocket/util/ComparablePair.java new file mode 100644 index 00000000..7b879683 --- /dev/null +++ b/core/src/net/sf/openrocket/util/ComparablePair.java @@ -0,0 +1,28 @@ +package net.sf.openrocket.util; + +/** + * Sortable storage of a pair of objects. A list of these objects can be sorted according + * to the first object. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + * @param <U> the first object type, according to which comparisons are performed. + * @param <V> the second object type. + */ +public class ComparablePair<U extends Comparable<U>, V> extends Pair<U, V> + implements Comparable<ComparablePair<U, V>>{ + + public ComparablePair(U u, V v) { + super(u, v); + } + + + /** + * Compares the first objects. If either of the objects is <code>null</code> this + * method throws <code>NullPointerException</code>. + */ + @Override + public int compareTo(ComparablePair<U, V> other) { + return this.getU().compareTo(other.getU()); + } + +} diff --git a/core/src/net/sf/openrocket/util/ConcurrencyException.java b/core/src/net/sf/openrocket/util/ConcurrencyException.java new file mode 100644 index 00000000..252267c8 --- /dev/null +++ b/core/src/net/sf/openrocket/util/ConcurrencyException.java @@ -0,0 +1,26 @@ +package net.sf.openrocket.util; + +/** + * An exception that indicates a concurrency bug in the software. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class ConcurrencyException extends FatalException { + + public ConcurrencyException() { + super(); + } + + public ConcurrencyException(String message, Throwable cause) { + super(message, cause); + } + + public ConcurrencyException(String message) { + super(message); + } + + public ConcurrencyException(Throwable cause) { + super(cause); + } + +} diff --git a/core/src/net/sf/openrocket/util/ConfigurationException.java b/core/src/net/sf/openrocket/util/ConfigurationException.java new file mode 100644 index 00000000..692b3ac1 --- /dev/null +++ b/core/src/net/sf/openrocket/util/ConfigurationException.java @@ -0,0 +1,26 @@ +package net.sf.openrocket.util; + +/** + * An exception to be thrown when a fatal problem with the environment + * is encountered (for example some file cannot be found). + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class ConfigurationException extends FatalException { + + public ConfigurationException() { + } + + public ConfigurationException(String message) { + super(message); + } + + public ConfigurationException(Throwable cause) { + super(cause); + } + + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/util/Coordinate.java b/core/src/net/sf/openrocket/util/Coordinate.java new file mode 100644 index 00000000..3bc981aa --- /dev/null +++ b/core/src/net/sf/openrocket/util/Coordinate.java @@ -0,0 +1,320 @@ +package net.sf.openrocket.util; + +import java.io.Serializable; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * An immutable class of weighted coordinates. The weights are non-negative. + * + * Can also be used as non-weighted coordinates with weight=0. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public final class Coordinate implements Serializable { + private static final LogHelper log = Application.getLogger(); + + //////// Debug section + /* + * Debugging info. If openrocket.debug.coordinatecount is defined, a line is + * printed every 1000000 instantiations (or as many as defined). + */ + private static final boolean COUNT_DEBUG; + private static final int COUNT_DIFF; + static { + String str = System.getProperty("openrocket.debug.coordinatecount"); + int diff = 0; + if (str == null) { + COUNT_DEBUG = false; + COUNT_DIFF = 0; + } else { + COUNT_DEBUG = true; + try { + diff = Integer.parseInt(str); + } catch (NumberFormatException ignore) { + } + if (diff < 1000) + diff = 1000000; + COUNT_DIFF = diff; + } + } + + private static int count = 0; + { + // Debug count + if (COUNT_DEBUG) { + synchronized (Coordinate.class) { + count++; + if ((count % COUNT_DIFF) == 0) { + log.debug("Coordinate instantiated " + count + " times."); + } + } + } + } + + //////// End debug section + + + + public static final Coordinate NUL = new Coordinate(0, 0, 0, 0); + public static final Coordinate NaN = new Coordinate(Double.NaN, Double.NaN, + Double.NaN, Double.NaN); + + public final double x, y, z; + public final double weight; + + + private double length = -1; /* Cached when calculated */ + + + + + public Coordinate() { + this(0, 0, 0, 0); + } + + public Coordinate(double x) { + this(x, 0, 0, 0); + } + + public Coordinate(double x, double y) { + this(x, y, 0, 0); + } + + public Coordinate(double x, double y, double z) { + this(x, y, z, 0); + } + + public Coordinate(double x, double y, double z, double w) { + this.x = x; + this.y = y; + this.z = z; + this.weight = w; + + } + + + public boolean isWeighted() { + return (weight != 0); + } + + /** + * Check whether any of the coordinate values is NaN. + * + * @return true if the x, y, z or weight is NaN + */ + public boolean isNaN() { + return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight); + } + + public Coordinate setX(double x) { + return new Coordinate(x, this.y, this.z, this.weight); + } + + public Coordinate setY(double y) { + return new Coordinate(this.x, y, this.z, this.weight); + } + + public Coordinate setZ(double z) { + return new Coordinate(this.x, this.y, z, this.weight); + } + + public Coordinate setWeight(double weight) { + return new Coordinate(this.x, this.y, this.z, weight); + } + + public Coordinate setXYZ(Coordinate c) { + return new Coordinate(c.x, c.y, c.z, this.weight); + } + + + /** + * Add the coordinate and weight of two coordinates. + * + * @param other the other <code>Coordinate</code> + * @return the sum of the coordinates + */ + public Coordinate add(Coordinate other) { + return new Coordinate(this.x + other.x, this.y + other.y, this.z + other.z, + this.weight + other.weight); + } + + public Coordinate add(double x, double y, double z) { + return new Coordinate(this.x + x, this.y + y, this.z + z, this.weight); + } + + public Coordinate add(double x, double y, double z, double weight) { + return new Coordinate(this.x + x, this.y + y, this.z + z, this.weight + weight); + } + + /** + * Subtract a Coordinate from this Coordinate. The weight of the resulting Coordinate + * is the same as of this Coordinate, the weight of the argument is ignored. + * + * @param other Coordinate to subtract from this. + * @return The result + */ + public Coordinate sub(Coordinate other) { + return new Coordinate(this.x - other.x, this.y - other.y, this.z - other.z, this.weight); + } + + /** + * Subtract the specified values from this Coordinate. The weight of the result + * is the same as the weight of this Coordinate. + * + * @param x x value to subtract + * @param y y value to subtract + * @param z z value to subtract + * @return the result. + */ + public Coordinate sub(double x, double y, double z) { + return new Coordinate(this.x - x, this.y - y, this.z - z, this.weight); + } + + + /** + * Multiply the <code>Coordinate</code> with a scalar. All coordinates and the + * weight are multiplied by the given scalar. + + * @param m Factor to multiply by. + * @return The product. + */ + public Coordinate multiply(double m) { + return new Coordinate(this.x * m, this.y * m, this.z * m, this.weight * m); + } + + /** + * Dot product of two Coordinates, taken as vectors. Equal to + * x1*x2+y1*y2+z1*z2 + * @param other Coordinate to take product with. + * @return The dot product. + */ + public double dot(Coordinate other) { + return this.x * other.x + this.y * other.y + this.z * other.z; + } + + /** + * Dot product of two Coordinates. + */ + public static double dot(Coordinate v1, Coordinate v2) { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; + } + + /** + * Distance from the origin to the Coordinate. + */ + public double length() { + if (length < 0) { + length = MathUtil.safeSqrt(x * x + y * y + z * z); + } + return length; + } + + /** + * Square of the distance from the origin to the Coordinate. + */ + public double length2() { + return x * x + y * y + z * z; + } + + + /** + * Return the largest of the absolute values of the coordinates. This can be + * used as a norm of the vector that is faster to calculate than the + * 2-norm. + * + * @return the largest absolute value of (x,y,z) + */ + public double max() { + return MathUtil.max(Math.abs(x), Math.abs(y), Math.abs(z)); + } + + + /** + * Returns a new coordinate which has the same direction from the origin as this + * coordinate but is at a distance of one. If this coordinate is the origin, + * this method throws an <code>IllegalStateException</code>. The weight of the + * coordinate is unchanged. + * + * @return the coordinate normalized to distance one of the origin. + * @throws IllegalStateException if this coordinate is the origin. + */ + public Coordinate normalize() { + double l = length(); + if (l < 0.0000001) { + throw new IllegalStateException("Cannot normalize zero coordinate"); + } + return new Coordinate(x / l, y / l, z / l, weight); + } + + + + + /** + * Weighted average of two coordinates. If either of the weights are positive, + * the result is the weighted average of the coordinates and the weight is the sum + * of the original weights. If the sum of the weights is zero (and especially if + * both of the weights are zero), the result is the unweighted average of the + * coordinates with weight zero. + * <p> + * If <code>other</code> is <code>null</code> then this <code>Coordinate</code> is + * returned. + */ + public Coordinate average(Coordinate other) { + double x, y, z, w; + + if (other == null) + return this; + + w = this.weight + other.weight; + if (Math.abs(w) < MathUtil.pow2(MathUtil.EPSILON)) { + x = (this.x + other.x) / 2; + y = (this.y + other.y) / 2; + z = (this.z + other.z) / 2; + w = 0; + } else { + x = (this.x * this.weight + other.x * other.weight) / w; + y = (this.y * this.weight + other.y * other.weight) / w; + z = (this.z * this.weight + other.z * other.weight) / w; + } + return new Coordinate(x, y, z, w); + } + + + /** + * Tests whether the coordinates are the equal. + * + * @param other Coordinate to compare to. + * @return true if the coordinates are equal (x, y, z and weight) + */ + @Override + public boolean equals(Object other) { + if (!(other instanceof Coordinate)) + return false; + + final Coordinate c = (Coordinate) other; + return (MathUtil.equals(this.x, c.x) && + MathUtil.equals(this.y, c.y) && + MathUtil.equals(this.z, c.z) && MathUtil.equals(this.weight, c.weight)); + } + + /** + * Hash code method compatible with {@link #equals(Object)}. + */ + @Override + public int hashCode() { + return (int) ((x + y + z) * 100000); + } + + + @Override + public String toString() { + if (isWeighted()) + return String.format("(%.3f,%.3f,%.3f,w=%.3f)", x, y, z, weight); + else + return String.format("(%.3f,%.3f,%.3f)", x, y, z); + } + + +} diff --git a/core/src/net/sf/openrocket/util/FatalException.java b/core/src/net/sf/openrocket/util/FatalException.java new file mode 100644 index 00000000..b50feef1 --- /dev/null +++ b/core/src/net/sf/openrocket/util/FatalException.java @@ -0,0 +1,28 @@ +package net.sf.openrocket.util; + +/** + * A superclass for all types of fatal error conditions. This class is + * abstract so only subclasses can be used. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + * @see BugException + * @see ConfigurationException + */ +public abstract class FatalException extends RuntimeException { + + public FatalException() { + } + + public FatalException(String message) { + super(message); + } + + public FatalException(Throwable cause) { + super(cause); + } + + public FatalException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/core/src/net/sf/openrocket/util/GeodeticComputationStrategy.java b/core/src/net/sf/openrocket/util/GeodeticComputationStrategy.java new file mode 100644 index 00000000..f8a37a09 --- /dev/null +++ b/core/src/net/sf/openrocket/util/GeodeticComputationStrategy.java @@ -0,0 +1,289 @@ +package net.sf.openrocket.util; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +/** + * A strategy that performs computations on WorldCoordinates. + * <p> + * The directions of the coordinate is: + * positive X = EAST + * positive Y = NORTH + * positive Z = UPWARDS + */ +public enum GeodeticComputationStrategy { + + + /** + * Perform computations using a flat Earth approximation. addCoordinate computes the + * location using a direct meters-per-degree scaling and getCoriolisAcceleration always + * returns NUL. + */ + FLAT { + private static final double METERS_PER_DEGREE_LATITUDE = 111325; // "standard figure" + private static final double METERS_PER_DEGREE_LONGITUDE_EQUATOR = 111050; + + + @Override + public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { + + double metersPerDegreeLongitude = METERS_PER_DEGREE_LONGITUDE_EQUATOR * Math.cos(location.getLatitudeRad()); + // Limit to 1 meter per degree near poles + metersPerDegreeLongitude = MathUtil.max(metersPerDegreeLongitude, 1); + + double newLat = location.getLatitudeDeg() + delta.y / METERS_PER_DEGREE_LATITUDE; + double newLon = location.getLongitudeDeg() + delta.x / metersPerDegreeLongitude; + double newAlt = location.getAltitude() + delta.z; + + return new WorldCoordinate(newLat, newLon, newAlt); + } + + @Override + public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { + return Coordinate.NUL; + } + }, + + /** + * Perform geodetic computations with a spherical Earch approximation. + */ + SPHERICAL { + + @Override + public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { + double newAlt = location.getAltitude() + delta.z; + + // bearing (in radians, clockwise from north); + // d/R is the angular distance (in radians), where d is the distance traveled and R is the earth's radius + double d = MathUtil.hypot(delta.x, delta.y); + + // Check for zero movement before computing bearing + if (MathUtil.equals(d, 0)) { + return new WorldCoordinate(location.getLatitudeDeg(), location.getLongitudeDeg(), newAlt); + } + + double bearing = Math.atan2(delta.x, delta.y); + + // Calculate the new lat and lon + double newLat, newLon; + double sinLat = Math.sin(location.getLatitudeRad()); + double cosLat = Math.cos(location.getLatitudeRad()); + double sinDR = Math.sin(d / WorldCoordinate.REARTH); + double cosDR = Math.cos(d / WorldCoordinate.REARTH); + + newLat = Math.asin(sinLat * cosDR + cosLat * sinDR * Math.cos(bearing)); + newLon = location.getLongitudeRad() + Math.atan2(Math.sin(bearing) * sinDR * cosLat, cosDR - sinLat * Math.sin(newLat)); + + if (Double.isNaN(newLat) || Double.isNaN(newLon)) { + throw new BugException("addCoordinate resulted in NaN location: location=" + location + " delta=" + delta + + " newLat=" + newLat + " newLon=" + newLon); + } + + return new WorldCoordinate(Math.toDegrees(newLat), Math.toDegrees(newLon), newAlt); + } + + @Override + public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { + return computeCoriolisAcceleration(location, velocity); + } + + }, + + /** + * Perform geodetic computations on a WGS84 reference ellipsoid using Vincenty Direct Solution. + */ + WGS84 { + + @Override + public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { + double newAlt = location.getAltitude() + delta.z; + + // bearing (in radians, clockwise from north); + // d/R is the angular distance (in radians), where d is the distance traveled and R is the earth's radius + double d = MathUtil.hypot(delta.x, delta.y); + + // Check for zero movement before computing bearing + if (MathUtil.equals(d, 0)) { + return new WorldCoordinate(location.getLatitudeDeg(), location.getLongitudeDeg(), newAlt); + } + + double bearing = Math.atan(delta.x / delta.y); + if (delta.y < 0) + bearing = bearing + Math.PI; + + // Calculate the new lat and lon + double newLat, newLon; + double ret[] = dirct1(location.getLatitudeRad(), location.getLongitudeRad(), bearing, d, 6378137, 1.0 / 298.25722210088); + newLat = ret[0]; + newLon = ret[1]; + + if (Double.isNaN(newLat) || Double.isNaN(newLon)) { + throw new BugException("addCoordinate resulted in NaN location: location=" + location + " delta=" + delta + + " newLat=" + newLat + " newLon=" + newLon); + } + + return new WorldCoordinate(Math.toDegrees(newLat), Math.toDegrees(newLon), newAlt); + } + + @Override + public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { + return computeCoriolisAcceleration(location, velocity); + } + }; + + + private static final Translator trans = Application.getTranslator(); + + private static final double PRECISION_LIMIT = 0.5e-13; + + + /** + * Return the name of this geodetic computation method. + */ + public String getName() { + return trans.get(name().toLowerCase() + ".name"); + } + + /** + * Return a description of the geodetic computation methods. + */ + public String getDescription() { + return trans.get(name().toLowerCase() + ".desc"); + } + + @Override + public String toString() { + return getName(); + } + + + /** + * Add a cartesian movement coordinate to a WorldCoordinate. + */ + public abstract WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta); + + + /** + * Compute the coriolis acceleration at a specified WorldCoordinate and velocity. + */ + public abstract Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity); + + + + + + private static Coordinate computeCoriolisAcceleration(WorldCoordinate latlon, Coordinate velocity) { + + double sinlat = Math.sin(latlon.getLatitudeRad()); + double coslat = Math.cos(latlon.getLatitudeRad()); + + double v_n = velocity.y; + double v_e = -1 * velocity.x; + double v_u = velocity.z; + + // Not exactly sure why I have to reverse the x direction, but this gives the precession in the + // correct direction (e.g, flying north in northern hemisphere should cause defection to the east (+ve x)) + // All the directions are very confusing because they are tied to the wind direction (to/from?), in which + // +ve x or east according to WorldCoordinate is what everything is relative to. + // The directions of everything need so thought, ideally the wind direction and launch rod should be + // able to be set independently and in terms of bearing with north == +ve y. + + Coordinate coriolis = new Coordinate(2.0 * WorldCoordinate.EROT * (v_n * sinlat - v_u * coslat), + 2.0 * WorldCoordinate.EROT * (-1.0 * v_e * sinlat), + 2.0 * WorldCoordinate.EROT * (v_e * coslat) + ); + return coriolis; + } + + + + // ******************************************************************** // + // The Vincenty Direct Solution. + // Code from GeoConstants.java, Ian Cameron Smith, GPL + // ******************************************************************** // + + /** + * Solution of the geodetic direct problem after T. Vincenty. + * Modified Rainsford's method with Helmert's elliptical terms. + * Effective in any azimuth and at any distance short of antipodal. + * + * Programmed for the CDC-6600 by lcdr L. Pfeifer, NGS Rockville MD, + * 20 Feb 1975. + * + * @param glat1 The latitude of the starting point, in radians, + * positive north. + * @param glon1 The latitude of the starting point, in radians, + * positive east. + * @param azimuth The azimuth to the desired location, in radians + * clockwise from north. + * @param dist The distance to the desired location, in meters. + * @param axis The semi-major axis of the reference ellipsoid, + * in meters. + * @param flat The flattening of the reference ellipsoid. + * @return An array containing the latitude and longitude + * of the desired point, in radians, and the + * azimuth back from that point to the starting + * point, in radians clockwise from north. + */ + private static double[] dirct1(double glat1, double glon1, + double azimuth, double dist, + double axis, double flat) { + double r = 1.0 - flat; + + double tu = r * Math.sin(glat1) / Math.cos(glat1); + + double sf = Math.sin(azimuth); + double cf = Math.cos(azimuth); + + double baz = 0.0; + + if (cf != 0.0) + baz = Math.atan2(tu, cf) * 2.0; + + double cu = 1.0 / Math.sqrt(tu * tu + 1.0); + double su = tu * cu; + double sa = cu * sf; + double c2a = -sa * sa + 1.0; + + double x = Math.sqrt((1.0 / r / r - 1.0) * c2a + 1.0) + 1.0; + x = (x - 2.0) / x; + double c = 1.0 - x; + c = (x * x / 4.0 + 1) / c; + double d = (0.375 * x * x - 1.0) * x; + tu = dist / r / axis / c; + double y = tu; + + double sy, cy, cz, e; + do { + sy = Math.sin(y); + cy = Math.cos(y); + cz = Math.cos(baz + y); + e = cz * cz * 2.0 - 1.0; + + c = y; + x = e * cy; + y = e + e - 1.0; + y = (((sy * sy * 4.0 - 3.0) * y * cz * d / 6.0 + x) * + d / 4.0 - cz) * sy * d + tu; + } while (Math.abs(y - c) > PRECISION_LIMIT); + + baz = cu * cy * cf - su * sy; + c = r * Math.sqrt(sa * sa + baz * baz); + d = su * cy + cu * sy * cf; + double glat2 = Math.atan2(d, c); + c = cu * cy - su * sy * cf; + x = Math.atan2(sy * sf, c); + c = ((-3.0 * c2a + 4.0) * flat + 4.0) * c2a * flat / 16.0; + d = ((e * cy * c + cz) * sy * c + y) * sa; + double glon2 = glon1 + x - (1.0 - c) * d * flat; + baz = Math.atan2(sa, baz) + Math.PI; + + double[] ret = new double[3]; + ret[0] = glat2; + ret[1] = glon2; + ret[2] = baz; + return ret; + } + + +} diff --git a/core/src/net/sf/openrocket/util/Inertia.java b/core/src/net/sf/openrocket/util/Inertia.java new file mode 100644 index 00000000..f7d65c41 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Inertia.java @@ -0,0 +1,42 @@ +package net.sf.openrocket.util; + +import static net.sf.openrocket.util.MathUtil.pow2; +public final class Inertia { + + private Inertia() { + } + + + /** + * Return the rotational unit moment of inertia of a solid cylinder. + * + * @param radius the radius of the cylinder. + */ + public static double filledCylinderRotational(double radius) { + return pow2(radius) / 2; + } + + /** + * Return the longitudinal unit moment of inertia of a solid cylinder, + * relative to the midpoint lengthwise. + * + * @param radius the radius of the cylinder. + * @param length the total length of the cylinder (reference at midpoint) + */ + public static double filledCylinderLongitudinal(double radius, double length) { + return (3*pow2(radius) + pow2(length))/12; + } + + + /** + * Return the unit moment of inertia that is shifted from the CG of an object + * by a specified distance. The rotation axis are parallel. + * + * @param cgInertia the unit moment of inertia through the CG of the object + * @param distance the distance to shift the rotation axis + */ + public static double shift(double cgInertia, double distance) { + return cgInertia + pow2(distance); + } + +} diff --git a/core/src/net/sf/openrocket/util/Invalidatable.java b/core/src/net/sf/openrocket/util/Invalidatable.java new file mode 100644 index 00000000..94499894 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Invalidatable.java @@ -0,0 +1,17 @@ +package net.sf.openrocket.util; + +/** + * An object that can be invalidated (in some sense of the word). After calling the + * invalidate method the object should not be used any more and it may enforce + * disusage for certain methods. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public interface Invalidatable { + + /** + * Invalidate this object. + */ + public void invalidate(); + +} diff --git a/core/src/net/sf/openrocket/util/Invalidator.java b/core/src/net/sf/openrocket/util/Invalidator.java new file mode 100644 index 00000000..9e40c102 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Invalidator.java @@ -0,0 +1,73 @@ +package net.sf.openrocket.util; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.startup.Application; + +/** + * A class that performs object invalidation functions. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Invalidator implements Invalidatable { + private static final boolean USE_CHECKS = Application.useSafetyChecks(); + + private static final LogHelper log = Application.getLogger(); + + private final Object monitorable; + private TraceException invalidated = null; + + + /** + * Sole constructor. The parameter is used when writing error messages, and + * is not referenced otherwise. + * + * @param monitorable the object this invalidator is monitoring (may be null or a descriptive string) + */ + public Invalidator(Object monitorable) { + this.monitorable = monitorable; + } + + + /** + * Check whether the object has been invalidated. Depending on the parameter either + * a BugException is thrown or a warning about the object access is logged. + * + * @param throwException whether to throw an exception or log a warning. + * @return <code>true</code> when the object has not been invalidated, <code>false</code> if it has + * @throws BugException if the object has been invalidated and <code>throwException</code> is true. + */ + public boolean check(boolean throwException) { + if (invalidated != null) { + if (throwException) { + throw new BugException(monitorable + ": This object has been invalidated", invalidated); + } else { + log.warn(1, monitorable + ": This object has been invalidated", + new TraceException("Usage was attempted here", invalidated)); + } + return false; + } + return true; + } + + + /** + * Check whether the object has been invalidated. + * @return <code>true</code> if the object has been invalidated, <code>false</code> otherwise. + */ + public boolean isInvalidated() { + return invalidated != null; + } + + + @Override + public void invalidate() { + if (USE_CHECKS) { + if (invalidated != null) { + log.warn(1, monitorable + ": This object has already been invalidated, ignoring", invalidated); + } + invalidated = new TraceException("Invalidation occurred here"); + } + } + +} diff --git a/core/src/net/sf/openrocket/util/JarUtil.java b/core/src/net/sf/openrocket/util/JarUtil.java new file mode 100644 index 00000000..b73438ed --- /dev/null +++ b/core/src/net/sf/openrocket/util/JarUtil.java @@ -0,0 +1,54 @@ +package net.sf.openrocket.util; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.CodeSource; + +import net.sf.openrocket.database.Database; + +public class JarUtil { + + /** + * Return the a File object pointing to the JAR file that this class belongs to, + * or <code>null</code> if it cannot be found. + * + * @return a File object of the current Java archive, or <code>null</code> + */ + public static File getCurrentJarFile() { + // Find the jar file this class is contained in + URL jarUrl = null; + CodeSource codeSource = Database.class.getProtectionDomain().getCodeSource(); + if (codeSource != null) + jarUrl = codeSource.getLocation(); + + if (jarUrl == null) { + return null; + } + + File file = urlToFile(jarUrl); + if (file.isFile()) + return file; + return null; + } + + + + public static File urlToFile(URL url) { + URI uri; + try { + uri = url.toURI(); + } catch (URISyntaxException e) { + try { + uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), + url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + } catch (URISyntaxException e1) { + throw new IllegalArgumentException("Broken URL: " + url); + } + } + return new File(uri); + } + + +} diff --git a/core/src/net/sf/openrocket/util/LimitedInputStream.java b/core/src/net/sf/openrocket/util/LimitedInputStream.java new file mode 100644 index 00000000..4e264fa1 --- /dev/null +++ b/core/src/net/sf/openrocket/util/LimitedInputStream.java @@ -0,0 +1,83 @@ +package net.sf.openrocket.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * A filtering InputStream that limits the number of bytes that can be + * read from a stream. This can be used to enforce security, so that overlong + * input is ignored. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class LimitedInputStream extends FilterInputStream { + + private int remaining; + + public LimitedInputStream(InputStream is, int limit) { + super(is); + this.remaining = limit; + } + + + @Override + public int available() throws IOException { + int available = super.available(); + return Math.min(available, remaining); + } + + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (remaining <= 0) + return -1; + + int result = super.read(b, off, Math.min(len, remaining)); + if (result >= 0) + remaining -= result; + return result; + } + + + @Override + public long skip(long n) throws IOException { + if (n > remaining) + n = remaining; + long result = super.skip(n); + remaining -= result; + return result; + } + + + @Override + public int read() throws IOException { + if (remaining <= 0) + return -1; + + int result = super.read(); + if (result >= 0) + remaining--; + return result; + } + + + + // Disable mark support + + @Override + public void mark(int readlimit) { + + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + +} diff --git a/core/src/net/sf/openrocket/util/LineStyle.java b/core/src/net/sf/openrocket/util/LineStyle.java new file mode 100644 index 00000000..fc2b4fdc --- /dev/null +++ b/core/src/net/sf/openrocket/util/LineStyle.java @@ -0,0 +1,44 @@ +package net.sf.openrocket.util; + +import java.util.Arrays; + +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.startup.Application; + +/** + * An enumeration of line styles. The line styles are defined by an array of + * floats suitable for <code>BasicStroke</code>. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + +public enum LineStyle { + + + //// Solid + SOLID("LineStyle.Solid", new float[] { 10f, 0f }), + //// Dashed + DASHED("LineStyle.Dashed", new float[] { 6f, 4f }), + //// Dotted + DOTTED("LineStyle.Dotted", new float[] { 2f, 3f }), + //// Dash-dotted + DASHDOT("LineStyle.Dash-dotted", new float[] { 8f, 3f, 2f, 3f }); + + private static final Translator trans = Application.getTranslator(); + private final String name; + private final float[] dashes; + + LineStyle(String name, float[] dashes) { + this.name = name; + this.dashes = dashes; + } + + public float[] getDashes() { + return Arrays.copyOf(dashes, dashes.length); + } + + @Override + public String toString() { + return trans.get(name); + } +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/util/LinearInterpolator.java b/core/src/net/sf/openrocket/util/LinearInterpolator.java new file mode 100644 index 00000000..e94a3976 --- /dev/null +++ b/core/src/net/sf/openrocket/util/LinearInterpolator.java @@ -0,0 +1,128 @@ +package net.sf.openrocket.util; + +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +public class LinearInterpolator implements Cloneable { + + private TreeMap<Double, Double> sortMap = new TreeMap<Double,Double>(); + + /** + * Construct a <code>LinearInterpolator</code> with no points. Some points must be + * added using {@link #addPoints(double[], double[])} before using the interpolator. + */ + public LinearInterpolator() { + } + + /** + * Construct a <code>LinearInterpolator</code> with the given points. + * + * @param x the x-coordinates of the points. + * @param y the y-coordinates of the points. + * @throws IllegalArgumentException if the lengths of <code>x</code> and <code>y</code> + * are not equal. + * @see #addPoints(double[], double[]) + */ + public LinearInterpolator(double[] x, double[] y) { + addPoints(x,y); + } + + + /** + * Add the point to the linear interpolation. + * + * @param x the x-coordinate of the point. + * @param y the y-coordinate of the point. + */ + public void addPoint(double x, double y) { + sortMap.put(x, y); + } + + /** + * Add the points to the linear interpolation. + * + * @param x the x-coordinates of the points. + * @param y the y-coordinates of the points. + * @throws IllegalArgumentException if the lengths of <code>x</code> and <code>y</code> + * are not equal. + */ + public void addPoints(double[] x, double[] y) { + if (x.length != y.length) { + throw new IllegalArgumentException("Array lengths do not match, x="+x.length + + " y="+y.length); + } + for (int i=0; i < x.length; i++) { + sortMap.put(x[i],y[i]); + } + } + + + + public double getValue(double x) { + Map.Entry<Double,Double> e1, e2; + double x1, x2; + double y1, y2; + + e1 = sortMap.floorEntry(x); + + if (e1 == null) { + // x smaller than any value in the set + e1 = sortMap.firstEntry(); + if (e1 == null) { + throw new IllegalStateException("No points added yet to the interpolator."); + } + return e1.getValue(); + } + + x1 = e1.getKey(); + e2 = sortMap.higherEntry(x1); + + if (e2 == null) { + // x larger than any value in the set + return e1.getValue(); + } + + x2 = e2.getKey(); + y1 = e1.getValue(); + y2 = e2.getValue(); + + return (x - x1)/(x2-x1) * (y2-y1) + y1; + } + + + public double[] getXPoints() { + double[] x = new double[sortMap.size()]; + Iterator<Double> iter = sortMap.keySet().iterator(); + for (int i=0; iter.hasNext(); i++) { + x[i] = iter.next(); + } + return x; + } + + + @SuppressWarnings("unchecked") + @Override + public LinearInterpolator clone() { + try { + LinearInterpolator other = (LinearInterpolator)super.clone(); + other.sortMap = (TreeMap<Double,Double>)this.sortMap.clone(); + return other; + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException?!",e); + } + } + + + public static void main(String[] args) { + LinearInterpolator interpolator = new LinearInterpolator( + new double[] {1, 1.5, 2, 4, 5}, + new double[] {0, 1, 0, 2, 2} + ); + + for (double x=0; x < 6; x+=0.1) { + System.out.printf("%.1f: %.2f\n", x, interpolator.getValue(x)); + } + } + +} diff --git a/core/src/net/sf/openrocket/util/ListenerList.java b/core/src/net/sf/openrocket/util/ListenerList.java new file mode 100644 index 00000000..b4f4c02b --- /dev/null +++ b/core/src/net/sf/openrocket/util/ListenerList.java @@ -0,0 +1,264 @@ +package net.sf.openrocket.util; + +import java.util.Iterator; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.startup.Application; + +/** + * A list of listeners of a specific type. This class contains various utility, + * safety and debugging methods for handling listeners. + * <p> + * Note that unlike normal listener implementations, this list does NOT allow the + * exact same listener (equality using ==) twice. While adding a listener twice to + * a event source would in principle be valid, in practice it's most likely a bug. + * For example the Swing implementation Sun JRE contains such bugs. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + * @param <T> the type of the listeners. + */ +public class ListenerList<T> implements Invalidatable, Iterable<T> { + private static final LogHelper log = Application.getLogger(); + + private final ArrayList<ListenerData<T>> listeners = new ArrayList<ListenerData<T>>(); + private final TraceException instantiationLocation; + + private TraceException invalidated = null; + + + /** + * Sole contructor. + */ + public ListenerList() { + this.instantiationLocation = new TraceException(1, 1); + } + + + /** + * Adds the specified listener to this list. The listener is not added if it + * already is in the list (checked by the equality operator ==). This method throws + * a BugException if {@link #invalidate()} has been called. + * + * @param listener the listener to add. + * @return whether the listeners was actually added to the list. + * @throws BugException if this listener list has been invalidated. + */ + public boolean addListener(T listener) { + checkState(true); + + ListenerData<T> data = new ListenerData<T>(listener); + if (listeners.contains(data)) { + log.warn(1, "Attempting to add duplicate listener " + listener); + return false; + } + listeners.add(data); + return true; + } + + + /** + * Remove the specified listener from the list. The listener is removed based on the + * quality operator ==, not by the equals() method. + * + * @param listener the listener to remove. + * @return whether the listener was actually removed. + */ + public boolean removeListener(T listener) { + checkState(false); + + Iterator<ListenerData<T>> iterator = listeners.iterator(); + while (iterator.hasNext()) { + if (iterator.next().listener == listener) { + iterator.remove(); + log.verbose(1, "Removing listener " + listener); + return true; + } + } + log.info(1, "Attempting to remove non-existant listener " + listener); + return false; + } + + + /** + * Return the number of listeners in this list. + */ + public int getListenerCount() { + return listeners.size(); + } + + + /** + * Return an iterator that iterates of the listeners. This iterator is backed by + * a copy of the iterator list, so {@link #addListener(Object)} and {@link #removeListener(Object)} + * may be called while iterating the list without effect on the iteration. The returned + * iterator does not support the {@link Iterator#remove()} method. + */ + @Override + public Iterator<T> iterator() { + checkState(false); + return new ListenerDataIterator(); + } + + /** + * Return the instantiation location of this listener list. + * @return the location where this listener list was instantiated. + */ + public TraceException getInstantiationLocation() { + return instantiationLocation; + } + + + /** + * Invalidate this listener list. Invalidation removes all listeners from the list. + * After invalidation {@link #addListener(Object)} will throw an exception, the other + * methods produce a warning log message. + */ + @Override + public void invalidate() { + this.invalidated = new TraceException("Invalidation occurred at this point"); + if (!listeners.isEmpty()) { + log.info("Invalidating " + this + " while still having listeners " + listeners); + } + listeners.clear(); + } + + + public boolean isInvalidated() { + return this.invalidated != null; + } + + + private void checkState(boolean error) { + if (this.invalidated != null) { + if (error) { + throw new BugException(this + ": this ListenerList has been invalidated", invalidated); + } else { + log.warn(1, this + ": this ListenerList has been invalidated", + new TraceException("ListenerList was attempted to be used here", invalidated)); + } + } + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ListenerList["); + + if (this.invalidated != null) { + sb.append("INVALIDATED]"); + return sb.toString(); + } + + if (listeners.isEmpty()) { + sb.append("empty"); + } else { + boolean first = true; + for (ListenerData<T> l : listeners) { + if (!first) { + sb.append("; "); + } + first = false; + sb.append(l); + } + } + sb.append("]"); + return sb.toString(); + } + + + /** + * A class containing data about a listener. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + * @param <T> the listener type + */ + public static class ListenerData<T> { + private final T listener; + private final long addTimestamp; + private final TraceException addLocation; + private long accessTimestamp; + + /** + * Sole constructor. + */ + private ListenerData(T listener) { + if (listener == null) { + throw new NullPointerException("listener is null"); + } + this.listener = listener; + this.addTimestamp = System.currentTimeMillis(); + this.accessTimestamp = this.addTimestamp; + this.addLocation = new TraceException("Listener " + listener + " add position"); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof ListenerData)) + return false; + ListenerData<?> other = (ListenerData<?>) obj; + return this.listener == other.listener; + } + + @Override + public int hashCode() { + return listener.hashCode(); + } + + /** + * Return the listener. + */ + public T getListener() { + return listener; + } + + /** + * Return the millisecond timestamp when this listener was added to the + * listener list. + */ + public long getAddTimestamp() { + return addTimestamp; + } + + /** + * Return the location where this listener was added to the listener list. + */ + public TraceException getAddLocation() { + return addLocation; + } + + /** + * Return the millisecond timestamp when this listener was last accessed through + * the listener list iterator. + */ + public long getAccessTimestamp() { + return accessTimestamp; + } + } + + + private class ListenerDataIterator implements Iterator<T> { + private final Iterator<ListenerData<T>> iterator = listeners.clone().iterator(); + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public T next() { + ListenerData<T> data = iterator.next(); + data.accessTimestamp = System.currentTimeMillis(); + return data.listener; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Remove not supported"); + } + } + +} diff --git a/core/src/net/sf/openrocket/util/MathUtil.java b/core/src/net/sf/openrocket/util/MathUtil.java new file mode 100644 index 00000000..dc2e105d --- /dev/null +++ b/core/src/net/sf/openrocket/util/MathUtil.java @@ -0,0 +1,314 @@ +package net.sf.openrocket.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +public class MathUtil { + private static final LogHelper log = Application.getLogger(); + + public static final double EPSILON = 0.00000001; // 10mm^3 in m^3 + + /** + * The square of x (x^2). On Sun's JRE using this method is as fast as typing x*x. + * @param x x + * @return x^2 + */ + public static double pow2(double x) { + return x * x; + } + + /** + * The cube of x (x^3). + * @param x x + * @return x^3 + */ + public static double pow3(double x) { + return x * x * x; + } + + public static double pow4(double x) { + return (x * x) * (x * x); + } + + /** + * Clamps the value x to the range min - max. + * @param x Original value. + * @param min Minimum value to return. + * @param max Maximum value to return. + * @return The clamped value. + */ + public static double clamp(double x, double min, double max) { + if (x < min) + return min; + if (x > max) + return max; + return x; + } + + public static float clamp(float x, float min, float max) { + if (x < min) + return min; + if (x > max) + return max; + return x; + } + + public static int clamp(int x, int min, int max) { + if (x < min) + return min; + if (x > max) + return max; + return x; + } + + + /** + * Maps a value from one value range to another. + * + * @param value the value to map. + * @param fromMin the minimum of the starting range. + * @param fromMax the maximum of the starting range. + * @param toMin the minimum of the destination range. + * @param toMax the maximum of the destination range. + * @return the mapped value. + * @throws IllegalArgumentException if fromMin == fromMax, but toMin != toMax. + */ + public static double map(double value, double fromMin, double fromMax, + double toMin, double toMax) { + if (equals(toMin, toMax)) + return toMin; + if (equals(fromMin, fromMax)) { + throw new IllegalArgumentException("from range is singular and to range is not: " + + "value=" + value + " fromMin=" + fromMin + " fromMax=" + fromMax + + "toMin=" + toMin + " toMax=" + toMax); + } + return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin; + } + + + /** + * Maps a coordinate from one value range to another. + * + * @param value the value to map. + * @param fromMin the minimum of the starting range. + * @param fromMax the maximum of the starting range. + * @param toMin the minimum coordinate of the destination; + * @param toMax the maximum coordinate of the destination; + * @return the mapped value. + * @throws IllegalArgumentException if fromMin == fromMax, but toMin != toMax. + */ + public static Coordinate map(double value, double fromMin, double fromMax, + Coordinate toMin, Coordinate toMax) { + if (toMin.equals(toMax)) + return toMin; + if (equals(fromMin, fromMax)) { + throw new IllegalArgumentException("from range is singular and to range is not: " + + "value=" + value + " fromMin=" + fromMin + " fromMax=" + fromMax + + "toMin=" + toMin + " toMax=" + toMax); + } + double a = (value - fromMin) / (fromMax - fromMin); + return toMax.multiply(a).add(toMin.multiply(1 - a)); + } + + + /** + * Compute the minimum of two values. This is performed by direct comparison. + * However, if one of the values is NaN and the other is not, the non-NaN value is + * returned. + */ + public static double min(double x, double y) { + if (Double.isNaN(y)) + return x; + return (x < y) ? x : y; + } + + /** + * Compute the maximum of two values. This is performed by direct comparison. + * However, if one of the values is NaN and the other is not, the non-NaN value is + * returned. + */ + public static double max(double x, double y) { + if (Double.isNaN(x)) + return y; + return (x < y) ? y : x; + } + + /** + * Compute the minimum of three values. This is performed by direct comparison. + * However, if one of the values is NaN and the other is not, the non-NaN value is + * returned. + */ + public static double min(double x, double y, double z) { + if (x < y || Double.isNaN(y)) { + return min(x, z); + } else { + return min(y, z); + } + } + + + + /** + * Compute the minimum of three values. This is performed by direct comparison. + * However, if one of the values is NaN and the other is not, the non-NaN value is + * returned. + */ + public static double min(double w, double x, double y, double z) { + return min(min(w, x), min(y, z)); + } + + + /** + * Compute the maximum of three values. This is performed by direct comparison. + * However, if one of the values is NaN and the other is not, the non-NaN value is + * returned. + */ + public static double max(double x, double y, double z) { + if (x > y || Double.isNaN(y)) { + return max(x, z); + } else { + return max(y, z); + } + } + + /** + * Calculates the hypotenuse <code>sqrt(x^2+y^2)</code>. This method is SIGNIFICANTLY + * faster than <code>Math.hypot(x,y)</code>. + */ + public static double hypot(double x, double y) { + return Math.sqrt(x * x + y * y); + } + + /** + * Reduce the angle x to the range 0 - 2*PI. + * @param x Original angle. + * @return The equivalent angle in the range 0 ... 2*PI. + */ + public static double reduce360(double x) { + double d = Math.floor(x / (2 * Math.PI)); + return x - d * 2 * Math.PI; + } + + /** + * Reduce the angle x to the range -PI - PI. + * + * Either -PI and PI might be returned, depending on the rounding function. + * + * @param x Original angle. + * @return The equivalent angle in the range -PI ... PI. + */ + public static double reduce180(double x) { + double d = Math.rint(x / (2 * Math.PI)); + return x - d * 2 * Math.PI; + } + + + /** + * Return the square root of a value. If the value is negative, zero is returned. + * This is safer in cases where rounding errors might make a value slightly negative. + * + * @param d the value of which the square root is to be taken. + * @return the square root of the value. + */ + public static double safeSqrt(double d) { + if (d < 0) { + if (d < 0.01) { + log.warn(1, "Attempting to compute sqrt(" + d + ")"); + } + return 0; + } + return Math.sqrt(d); + } + + + + public static boolean equals(double a, double b) { + double absb = Math.abs(b); + + if (absb < EPSILON / 2) { + // Near zero + return Math.abs(a) < EPSILON / 2; + } + return Math.abs(a - b) < EPSILON * absb; + } + + + /** + * Return the sign of the number. This corresponds to Math.signum, but ignores + * the special cases of zero and NaN. The value returned for those is arbitrary. + * <p> + * This method is about 4 times faster than Math.signum(). + * + * @param x the checked value. + * @return -1.0 if x<0; 1.0 if x>0; otherwise either -1.0 or 1.0. + */ + public static double sign(double x) { + return (x < 0) ? -1.0 : 1.0; + } + + /* Math.abs() is about 3x as fast as this: + + public static double abs(double x) { + return (x<0) ? -x : x; + } + */ + + + public static double average(Collection<? extends Number> values) { + if (values.isEmpty()) { + return Double.NaN; + } + + double avg = 0.0; + int count = 0; + for (Number n : values) { + avg += n.doubleValue(); + count++; + } + return avg / count; + } + + public static double stddev(Collection<? extends Number> values) { + if (values.size() < 2) { + return Double.NaN; + } + + double avg = average(values); + double stddev = 0.0; + int count = 0; + for (Number n : values) { + stddev += pow2(n.doubleValue() - avg); + count++; + } + stddev = Math.sqrt(stddev / (count - 1)); + return stddev; + } + + public static double median(Collection<? extends Number> values) { + if (values.isEmpty()) { + return Double.NaN; + } + + List<Number> sorted = new ArrayList<Number>(values); + Collections.sort(sorted, new Comparator<Number>() { + @Override + public int compare(Number o1, Number o2) { + return Double.compare(o1.doubleValue(), o2.doubleValue()); + } + }); + + int n = sorted.size(); + if (n % 2 == 0) { + return (sorted.get(n / 2).doubleValue() + sorted.get(n / 2 - 1).doubleValue()) / 2; + } else { + return sorted.get(n / 2).doubleValue(); + } + } + +} diff --git a/core/src/net/sf/openrocket/util/MemoryManagement.java b/core/src/net/sf/openrocket/util/MemoryManagement.java new file mode 100644 index 00000000..7b55200b --- /dev/null +++ b/core/src/net/sf/openrocket/util/MemoryManagement.java @@ -0,0 +1,194 @@ +package net.sf.openrocket.util; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * A class that performs certain memory-management operations for debugging purposes. + * For example, complex objects that are being discarded and that should be garbage-collectable + * (such as dialog windows) should be registered for monitoring by calling + * {@link #collectable(Object)}. This will allow monitoring whether the object really is + * garbage-collected or whether it is retained in memory due to a memory leak. + * Only complex objects should be registered due to the overhead of the monitoring. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public final class MemoryManagement { + private static final LogHelper log = Application.getLogger(); + + /** Purge cleared references every this many calls to {@link #collectable(Object)} */ + private static final int PURGE_CALL_COUNT = 1000; + + + /** + * Storage of the objects. This is basically a mapping from the objects (using weak references) + * to + */ + private static List<MemoryData> objects = new LinkedList<MemoryData>(); + private static int collectableCallCount = 0; + + + private static List<WeakReference<ListenerList<?>>> listenerLists = new LinkedList<WeakReference<ListenerList<?>>>(); + private static int listenerCallCount = 0; + + + private MemoryManagement() { + } + + + /** + * Mark an object that should be garbage-collectable by the GC. This class will monitor + * whether the object actually gets garbage-collected or not by holding a weak reference + * to the object. + * + * @param o the object to monitor. + */ + public static synchronized void collectable(Object o) { + if (o == null) { + throw new IllegalArgumentException("object is null"); + } + log.debug("Adding object into collectable list: " + o); + objects.add(new MemoryData(o)); + collectableCallCount++; + if (collectableCallCount % PURGE_CALL_COUNT == 0) { + purgeCollectables(); + } + } + + + /** + * Return a list of MemoryData objects corresponding to the objects that have been + * registered by {@link #collectable(Object)} and have not been garbage-collected properly. + * This method first calls <code>System.gc()</code> multiple times to attempt to + * force any remaining garbage collection. + * + * @return a list of MemoryData objects for objects that have not yet been garbage-collected. + */ + public static synchronized List<MemoryData> getRemainingCollectableObjects() { + for (int i = 0; i < 5; i++) { + System.runFinalization(); + System.gc(); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + purgeCollectables(); + return new ArrayList<MemoryData>(objects); + } + + + + + /** + * Register a new ListenerList object. This can be used to monitor freeing of listeners + * and find memory leaks. The objects are held by a weak reference, allowing them to be + * garbage-collected. + * + * @param list the listener list to register + */ + public static synchronized void registerListenerList(ListenerList<?> list) { + listenerLists.add(new WeakReference<ListenerList<?>>(list)); + listenerCallCount++; + if (listenerCallCount % PURGE_CALL_COUNT == 0) { + purgeListeners(); + } + + } + + /** + * Return a list of listener list objects corresponding to the objects that have been + * registered by {@link #registerListenerList(ListenerList)} and have not been garbage-collected yet. + * This method first calls <code>System.gc()</code> multiple times to attempt to + * force any remaining garbage collection. + * + * @return a list of listener list objects that have not yet been garbage-collected. + */ + public static synchronized List<ListenerList<?>> getRemainingListenerLists() { + for (int i = 0; i < 5; i++) { + System.runFinalization(); + System.gc(); + try { + Thread.sleep(1); + } catch (InterruptedException e) { + } + } + purgeListeners(); + List<ListenerList<?>> list = new ArrayList<ListenerList<?>>(); + for (WeakReference<ListenerList<?>> ref : listenerLists) { + ListenerList<?> l = ref.get(); + if (l != null) { + list.add(l); + } + } + return list; + } + + + + /** + * Purge all cleared references from the object list. + */ + private static void purgeCollectables() { + int origCount = objects.size(); + Iterator<MemoryData> iterator = objects.iterator(); + while (iterator.hasNext()) { + MemoryData data = iterator.next(); + if (data.getReference().get() == null) { + iterator.remove(); + } + } + log.debug(objects.size() + " of " + origCount + " objects remaining in discarded objects list after purge."); + } + + + /** + * Purge all cleared references from the object list. + */ + private static void purgeListeners() { + int origCount = listenerLists.size(); + Iterator<WeakReference<ListenerList<?>>> iterator = listenerLists.iterator(); + while (iterator.hasNext()) { + WeakReference<ListenerList<?>> ref = iterator.next(); + if (ref.get() == null) { + iterator.remove(); + } + } + log.debug(listenerLists.size() + " of " + origCount + " listener lists remaining after purge."); + } + + /** + * A value object class containing data of a discarded object reference. + */ + public static final class MemoryData { + private final WeakReference<Object> reference; + private final long registrationTime; + + private MemoryData(Object object) { + this.reference = new WeakReference<Object>(object); + this.registrationTime = System.currentTimeMillis(); + } + + /** + * Return the weak reference to the discarded object. + */ + public WeakReference<Object> getReference() { + return reference; + } + + /** + * Return the time when the object was discarded. + * @return a millisecond timestamp of when the object was discarded. + */ + public long getRegistrationTime() { + return registrationTime; + } + } + +} diff --git a/core/src/net/sf/openrocket/util/Monitorable.java b/core/src/net/sf/openrocket/util/Monitorable.java new file mode 100644 index 00000000..bff7480e --- /dev/null +++ b/core/src/net/sf/openrocket/util/Monitorable.java @@ -0,0 +1,41 @@ +package net.sf.openrocket.util; + +/** + * Interface describing objects whose state changes can be monitored based on + * a modification ID number. If a specific object has the same modification ID + * at two different points in time, the object state is guaranteed to be the same. + * This does not necessarily hold between two different instances of an object type. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public interface Monitorable { + + /** + * Return a modification ID unique to the current state of this object and contained objects. + * The general contract is that if a specific object has the same modification ID at two moments + * in time, then the state of the object has not changed in between. Additionally the + * modification ID value must be monotonically increasing. This value can be used as a monitor + * to whether an object has been changed between two points of time. + * <p> + * Implementations may optionally fulfill the stronger requirement that any two objects of the same + * type that have the same modification ID will be equal, though for complex objects guaranteeing + * this may be impractical. + * <p> + * Objects that contain only primitive types or immutable objects can implement this method by + * increasing a modification counter or retrieving a new unique ID every time a value is set. + * <p> + * Objects that contain other objects with a mutable state may for example return the sum of the + * object's own modification ID, a modification ID counter (initially zero) and the modification IDs + * of the contained objects. When a mutable object is set, the modification counter is increased by + * the modification ID of the current object in order to preserve monotonicity. + * <p> + * If an object does not have any fields, this method can simply return zero. + * <p> + * Cloned objects may or may not have the same modification ID as the original object. + * + * @return a modification ID value for this object. + * @see UniqueID#next() + */ + public int getModID(); + +} diff --git a/core/src/net/sf/openrocket/util/MonitorableSet.java b/core/src/net/sf/openrocket/util/MonitorableSet.java new file mode 100644 index 00000000..a7601bdf --- /dev/null +++ b/core/src/net/sf/openrocket/util/MonitorableSet.java @@ -0,0 +1,92 @@ +package net.sf.openrocket.util; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; + +/** + * A Set that additionally implements the Monitorable interface. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class MonitorableSet<E> extends HashSet<E> implements Monitorable { + + private int modID; + + @Override + public boolean add(E e) { + modID++; + return super.add(e); + }; + + @Override + public boolean addAll(Collection<? extends E> c) { + modID++; + return super.addAll(c); + } + + @Override + public void clear() { + modID++; + super.clear(); + } + + @Override + public Iterator<E> iterator() { + return new MonitorableIterator<E>(super.iterator()); + } + + @Override + public boolean remove(Object o) { + modID++; + return super.remove(o); + } + + @Override + public boolean removeAll(Collection<?> c) { + modID++; + return super.removeAll(c); + } + + @Override + public boolean retainAll(Collection<?> c) { + modID++; + return super.retainAll(c); + } + + + @Override + public int getModID() { + return modID; + } + + @SuppressWarnings("unchecked") + @Override + public MonitorableSet<E> clone() { + return (MonitorableSet<E>) super.clone(); + } + + private class MonitorableIterator<F> implements Iterator<F> { + private final Iterator<F> iterator; + + public MonitorableIterator(Iterator<F> iterator) { + this.iterator = iterator; + } + + @Override + public boolean hasNext() { + return iterator.hasNext(); + } + + @Override + public F next() { + return iterator.next(); + } + + @Override + public void remove() { + iterator.remove(); + modID++; + } + } +} diff --git a/core/src/net/sf/openrocket/util/Mutable.java b/core/src/net/sf/openrocket/util/Mutable.java new file mode 100644 index 00000000..c7c91359 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Mutable.java @@ -0,0 +1,66 @@ +package net.sf.openrocket.util; + +import net.sf.openrocket.logging.TraceException; + +/** + * A utility class helping an object to be made immutable after a certain point of time. + * An object should contain an instance of Mutable and an immute() method which calls + * {@link #immute()}. Additionally, every method that changes the state of the object + * should call {@link #check()} before modification. + * <p> + * This class also provides a stack trace of the position where the object was made + * immutable to help in debugging modifications of immuted objects. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Mutable implements Cloneable { + + private TraceException immuteTrace = null; + + /** + * Mark the object immutable. Once the object has been called the object + * cannot be made mutable again. Repeated calls to this method have no effect. + */ + public void immute() { + if (immuteTrace == null) { + immuteTrace = new TraceException(1, 2); + } + } + + /** + * Check that the object is still mutable, and throw an exception if it is not. + * <p> + * The thrown exception will contain a trace of the position where the object was made + * immutable to help in debugging. + * + * @throws IllegalStateException if {@link #immute()} has been called for this object. + */ + public void check() { + if (immuteTrace != null) { + throw new IllegalStateException("Object has been made immutable at " + + immuteTrace.getMessage(), immuteTrace); + } + } + + /** + * Check whether this object is still mutable. + * + * @return whether this object is still mutable. + */ + public boolean isMutable() { + return immuteTrace == null; + } + + + /** + * Return a new Mutable instance with the same state as the current object. + */ + @Override + public Mutable clone() { + try { + return (Mutable) super.clone(); + } catch (CloneNotSupportedException e) { + throw new BugException("CloneNotSupportedException", e); + } + } +} diff --git a/core/src/net/sf/openrocket/util/Named.java b/core/src/net/sf/openrocket/util/Named.java new file mode 100644 index 00000000..b91ae46a --- /dev/null +++ b/core/src/net/sf/openrocket/util/Named.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.util; + +import java.text.Collator; + +/** + * An object holder that provides a custom toString return value. + * <p> + * The class supports sorting by the name. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + * @param <T> the holder type + */ +public class Named<T> implements Comparable<Named<T>> { + + private final T object; + private final String name; + + private Collator collator = null; + + /** + * Sole constructor. + * + * @param object the held object + * @param name the value to return by toString(). + */ + public Named(T object, String name) { + this.object = object; + this.name = name; + } + + + /** + * Get the held object. + * + * @return the object. + */ + public T get() { + return object; + } + + @Override + public String toString() { + return name; + } + + + @Override + public int compareTo(Named<T> other) { + if (collator == null) { + collator = Collator.getInstance(); + } + + return collator.compare(this.toString(), other.toString()); + } + +} diff --git a/core/src/net/sf/openrocket/util/NumericComparator.java b/core/src/net/sf/openrocket/util/NumericComparator.java new file mode 100644 index 00000000..a337dee6 --- /dev/null +++ b/core/src/net/sf/openrocket/util/NumericComparator.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.util; + +import java.util.Comparator; + +public class NumericComparator implements Comparator<Object> { + + public static final NumericComparator INSTANCE = new NumericComparator(); + + @Override + public int compare(Object o1, Object o2) { + double v1 = getValue(o1); + double v2 = getValue(o2); + + if (Double.isNaN(v1) || Double.isNaN(v2)) { + String s1 = o1.toString(); + String s2 = o2.toString(); + return s1.compareTo(s2); + } + + return Double.compare(v1, v2); + } + + private double getValue(Object o) { + if (o instanceof Number) { + return ((Number) o).doubleValue(); + } + String s = o.toString(); + try { + return Double.parseDouble(s); + } catch (NumberFormatException e) { + return Double.NaN; + } + } + +} diff --git a/core/src/net/sf/openrocket/util/Pair.java b/core/src/net/sf/openrocket/util/Pair.java new file mode 100644 index 00000000..9a562cb8 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Pair.java @@ -0,0 +1,71 @@ +package net.sf.openrocket.util; + +/** + * Storage for a pair of objects. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + * @param <U> the first object type. + * @param <V> the second object type. + */ +public class Pair<U,V> { + + private final U u; + private final V v; + + + public Pair(U u, V v) { + this.u = u; + this.v = v; + } + + public U getU() { + return u; + } + + public V getV() { + return v; + } + + + /** + * Compare both components of the Pair to another object. + * The pair is equal iff both items are equal (or null). + */ + @SuppressWarnings("unchecked") + @Override + public boolean equals(Object other) { + if (!(other instanceof Pair)) + return false; + Object otherU = ((Pair)other).getU(); + Object otherV = ((Pair)other).getV(); + + if (otherU == null) { + if (this.u != null) + return false; + } else { + if (!otherU.equals(this.u)) + return false; + } + + if (otherV == null) { + if (this.v != null) + return false; + } else { + if (!otherV.equals(this.v)) + return false; + } + return true; + } + + @Override + public int hashCode() { + return ((u != null) ? u.hashCode() : 0) + ((v != null) ? v.hashCode() : 0); + } + + + @Override + public String toString() { + return "[" + u + ";" + v + "]"; + } + +} diff --git a/core/src/net/sf/openrocket/util/PinkNoise.java b/core/src/net/sf/openrocket/util/PinkNoise.java new file mode 100644 index 00000000..572ad032 --- /dev/null +++ b/core/src/net/sf/openrocket/util/PinkNoise.java @@ -0,0 +1,140 @@ +package net.sf.openrocket.util; +import java.util.Random; + + +/** + * A class that provides a source of pink noise with a power spectrum density + * proportional to 1/f^alpha. The values are computed by applying an IIR filter to + * generated Gaussian random numbers. The number of poles used in the filter may be + * specified. Values as low as 3 produce good results, but using a larger number of + * poles allows lower frequencies to be amplified. Below the cutoff frequency the + * power spectrum density if constant. + * <p> + * The IIR filter use by this class is presented by N. Jeremy Kasdin, Proceedings of + * the IEEE, Vol. 83, No. 5, May 1995, p. 822. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class PinkNoise { + private final int poles; + private final double[] multipliers; + + private final double[] values; + private final Random rnd; + + + /** + * Generate pink noise with alpha=1.0 using a five-pole IIR. + */ + public PinkNoise() { + this(1.0, 5, new Random()); + } + + + /** + * Generate a specific pink noise using a five-pole IIR. + * + * @param alpha the exponent of the pink noise, 1/f^alpha. + */ + public PinkNoise(double alpha) { + this(alpha, 5, new Random()); + } + + + /** + * Generate pink noise specifying alpha and the number of poles. The larger the + * number of poles, the lower are the lowest frequency components that are amplified. + * + * @param alpha the exponent of the pink noise, 1/f^alpha. + * @param poles the number of poles to use. + */ + public PinkNoise(double alpha, int poles) { + this(alpha, poles, new Random()); + } + + + /** + * Generate pink noise specifying alpha, the number of poles and the randomness source. + * + * @param alpha the exponent of the pink noise, 1/f^alpha. + * @param poles the number of poles to use. + * @param random the randomness source. + */ + public PinkNoise(double alpha, int poles, Random random) { + this.rnd = random; + this.poles = poles; + this.multipliers = new double[poles]; + this.values = new double[poles]; + + double a = 1; + for (int i=0; i < poles; i++) { + a = (i - alpha/2) * a / (i+1); + multipliers[i] = a; + } + + // Fill the history with random values + for (int i=0; i < 5*poles; i++) + this.nextValue(); + } + + + + public double nextValue() { + double x = rnd.nextGaussian(); +// double x = rnd.nextDouble()-0.5; + + for (int i=0; i < poles; i++) { + x -= multipliers[i] * values[i]; + } + System.arraycopy(values, 0, values, 1, values.length-1); + values[0] = x; + + return x; + } + + + public static void main(String[] arg) { + + PinkNoise source; + + source = new PinkNoise(1.0, 100); + double std = 0; + for (int i=0; i < 1000000; i++) { + + } + + +// int n = 5000000; +// double avgavg=0; +// double avgstd = 0; +// double[] val = new double[n]; +// +// for (int j=0; j < 10; j++) { +// double avg=0, std=0; +// source = new PinkNoise(5.0/3.0, 2); +// +// for (int i=0; i < n; i++) { +// val[i] = source.nextValue(); +// avg += val[i]; +// } +// avg /= n; +// for (int i=0; i < n; i++) { +// std += (val[i]-avg)*(val[i]-avg); +// } +// std /= n; +// std = Math.sqrt(std); +// +// System.out.println("avg:"+avg+" stddev:"+std); +// avgavg += avg; +// avgstd += std; +// } +// avgavg /= 10; +// avgstd /= 10; +// System.out.println("Average avg:"+avgavg+" std:"+avgstd); +// + // Two poles: + + } + + +} diff --git a/core/src/net/sf/openrocket/util/PolyInterpolator.java b/core/src/net/sf/openrocket/util/PolyInterpolator.java new file mode 100644 index 00000000..f97d45c4 --- /dev/null +++ b/core/src/net/sf/openrocket/util/PolyInterpolator.java @@ -0,0 +1,262 @@ +package net.sf.openrocket.util; + +import java.util.Arrays; + +/** + * A class for polynomial interpolation. The interpolation constraints can be specified + * either as function values or values of the n'th derivative of the function. + * Using an interpolation consists of three steps: + * <p> + * 1. constructing a <code>PolyInterpolator</code> using the interpolation x coordinates <br> + * 2. generating the interpolation polynomial using the function and derivative values <br> + * 3. evaluating the polynomial at the desired point + * <p> + * The constructor takes an array of double arrays. The first array defines x coordinate + * values for the function values, the second array x coordinate values for the function's + * derivatives, the third array for second derivatives and so on. Constructing the + * <code>PolyInterpolator</code> is relatively slow, O(n^3) where n is the order of the + * polynomial. (It contains calculation of the inverse of an n x n matrix.) + * <p> + * Generating the interpolation polynomial is performed by the method + * {@link #interpolator(double...)}, which takes as an argument the function and + * derivative values. This operation takes O(n^2) time. + * <p> + * Finally, evaluating the polynomial at different positions takes O(n) time. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class PolyInterpolator { + + // Order of the polynomial + private final int count; + + private final double[][] interpolationMatrix; + + + /** + * Construct a polynomial interpolation generator. All arguments to the constructor + * are the x coordinates of the interpolated function. The first array correspond to + * the function values, the second to function derivatives, the third to second + * derivatives and so forth. The order of the polynomial is automatically calculated + * from the total number of constraints. + * <p> + * The construction takes O(n^3) time. + * + * @param points an array of constraints, the first corresponding to function value + * constraints, the second to derivative constraints etc. + */ + public PolyInterpolator(double[] ... points) { + int count = 0; + for (int i=0; i < points.length; i++) { + count += points[i].length; + } + if (count == 0) { + throw new IllegalArgumentException("No interpolation points defined."); + } + + this.count = count; + + int[] mul = new int[count]; + Arrays.fill(mul, 1); + + double[][] matrix = new double[count][count]; + int row = 0; + for (int j=0; j < points.length; j++) { + + for (int i=0; i < points[j].length; i++) { + double x = 1; + for (int col = count-1-j; col>= 0; col--) { + matrix[row][col] = x*mul[col]; + x *= points[j][i]; + } + row++; + } + + for (int i=0; i < count; i++) { + mul[i] *= (count-i-j-1); + } + } + assert(row == count); + + interpolationMatrix = inverse(matrix); + } + + + /** + * Generates an interpolation polynomial. The arguments supplied to this method + * are the function values, derivatives, second derivatives etc. in the order + * specified in the constructor (i.e. values first, then derivatives etc). + * <p> + * This method takes O(n^2) time. + * + * @param values the function values, derivatives etc. at positions defined in the + * constructor. + * @return the coefficients of the interpolation polynomial, the highest order + * term first and the constant last. + */ + public double[] interpolator(double... values) { + if (values.length != count) { + throw new IllegalArgumentException("Wrong number of arguments "+values.length+ + " expected "+count); + } + + double[] ret = new double[count]; + + for (int j=0; j < count; j++) { + for (int i=0; i < count; i++) { + ret[j] += interpolationMatrix[j][i] * values[i]; + } + } + + return ret; + } + + + /** + * Interpolate the given values at the point <code>x</code>. This is equivalent + * to generating an interpolation polynomial and evaluating the polynomial at the + * specified point. + * + * @param x point at which to evaluate the interpolation polynomial. + * @param values the function, derivatives etc. at position defined in the + * constructor. + * @return the value of the interpolation. + */ + public double interpolate(double x, double... values) { + return eval(x, interpolator(values)); + } + + + /** + * Evaluate a polynomial at the specified point <code>x</code>. The coefficients are + * assumed to have the highest order coefficient first and the constant term last. + * + * @param x position at which to evaluate the polynomial. + * @param coefficients polynomial coefficients, highest term first and constant last. + * @return the value of the polynomial. + */ + public static double eval(double x, double[] coefficients) { + double v = 1; + double result = 0; + for (int i = coefficients.length-1; i >= 0; i--) { + result += coefficients[i] * v; + v *= x; + } + return result; + } + + + + + private static double[][] inverse(double[][] matrix) { + int n = matrix.length; + + double x[][] = new double[n][n]; + double b[][] = new double[n][n]; + int index[] = new int[n]; + for (int i=0; i<n; ++i) + b[i][i] = 1; + + // Transform the matrix into an upper triangle + gaussian(matrix, index); + + // Update the matrix b[i][j] with the ratios stored + for (int i=0; i<n-1; ++i) + for (int j=i+1; j<n; ++j) + for (int k=0; k<n; ++k) + b[index[j]][k] -= matrix[index[j]][i]*b[index[i]][k]; + + // Perform backward substitutions + for (int i=0; i<n; ++i) { + x[n-1][i] = b[index[n-1]][i]/matrix[index[n-1]][n-1]; + for (int j=n-2; j>=0; --j) { + x[j][i] = b[index[j]][i]; + for (int k=j+1; k<n; ++k) { + x[j][i] -= matrix[index[j]][k]*x[k][i]; + } + x[j][i] /= matrix[index[j]][j]; + } + } + return x; + } + + private static void gaussian(double a[][], + int index[]) { + int n = index.length; + double c[] = new double[n]; + + // Initialize the index + for (int i=0; i<n; ++i) index[i] = i; + + // Find the rescaling factors, one from each row + for (int i=0; i<n; ++i) { + double c1 = 0; + for (int j=0; j<n; ++j) { + double c0 = Math.abs(a[i][j]); + if (c0 > c1) c1 = c0; + } + c[i] = c1; + } + + // Search the pivoting element from each column + int k = 0; + for (int j=0; j<n-1; ++j) { + double pi1 = 0; + for (int i=j; i<n; ++i) { + double pi0 = Math.abs(a[index[i]][j]); + pi0 /= c[index[i]]; + if (pi0 > pi1) { + pi1 = pi0; + k = i; + } + } + + // Interchange rows according to the pivoting order + int itmp = index[j]; + index[j] = index[k]; + index[k] = itmp; + for (int i=j+1; i<n; ++i) { + double pj = a[index[i]][j]/a[index[j]][j]; + + // Record pivoting ratios below the diagonal + a[index[i]][j] = pj; + + // Modify other elements accordingly + for (int l=j+1; l<n; ++l) + a[index[i]][l] -= pj*a[index[j]][l]; + } + } + } + + + + + public static void main(String[] arg) { + + PolyInterpolator p0 = new PolyInterpolator( + new double[] {0.6, 1.1}, + new double[] {0.6, 1.1} + ); + double[] r0 = p0.interpolator(1.5, 1.6, 2, -3); + + PolyInterpolator p1 = new PolyInterpolator( + new double[] {0.6, 1.1}, + new double[] {0.6, 1.1}, + new double[] {0.6} + ); + double[] r1 = p1.interpolator(1.5, 1.6, 2, -3, 0); + + PolyInterpolator p2 = new PolyInterpolator( + new double[] {0.6, 1.1}, + new double[] {0.6, 1.1}, + new double[] {0.6, 1.1} + ); + double[] r2 = p2.interpolator(1.5, 1.6, 2, -3, 0, 0); + + + for (double x=0.6; x <= 1.11; x += 0.01) { + System.out.println(x + " " + eval(x,r0) + " " + eval(x,r1) + " " + eval(x,r2)); + } + + } +} diff --git a/core/src/net/sf/openrocket/util/PrintProperties.java b/core/src/net/sf/openrocket/util/PrintProperties.java new file mode 100644 index 00000000..d3d4fe11 --- /dev/null +++ b/core/src/net/sf/openrocket/util/PrintProperties.java @@ -0,0 +1,22 @@ +package net.sf.openrocket.util; + +import java.util.SortedSet; +import java.util.TreeSet; + +public class PrintProperties { + + public static void main(String[] args) { + + // Sort the keys + SortedSet<String> keys = new TreeSet<String>(); + for (Object key: System.getProperties().keySet()) { + keys.add((String)key); + } + + for (String key: keys) { + System.out.println(key + "=" + System.getProperty((String)key)); + } + + } + +} diff --git a/core/src/net/sf/openrocket/util/Quaternion.java b/core/src/net/sf/openrocket/util/Quaternion.java new file mode 100644 index 00000000..5f277662 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Quaternion.java @@ -0,0 +1,356 @@ +package net.sf.openrocket.util; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.startup.Application; + +/** + * An immutable quaternion class. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class Quaternion { + private static final LogHelper log = Application.getLogger(); + + + //////// Debug section + /* + * Debugging info. If openrocket.debug.quaternioncount is defined, a line is + * printed every 1000000 instantiations (or as many as defined). + */ + private static final boolean COUNT_DEBUG; + private static final int COUNT_DIFF; + static { + String str = System.getProperty("openrocket.debug.quaternioncount"); + int diff = 0; + if (str == null) { + COUNT_DEBUG = false; + COUNT_DIFF = 0; + } else { + COUNT_DEBUG = true; + try { + diff = Integer.parseInt(str); + } catch (NumberFormatException ignore) { + } + if (diff < 1000) + diff = 1000000; + COUNT_DIFF = diff; + } + } + + private static int count = 0; + { + // Debug count + if (COUNT_DEBUG) { + synchronized (Quaternion.class) { + count++; + if ((count % COUNT_DIFF) == 0) { + log.debug("Quaternion instantiated " + count + " times."); + } + } + } + } + + //////// End debug section + + + + private final double w, x, y, z; + private double norm = -1; + + + /** + * Construct a new "one" quaternion. This is equivalent to <code>new Quaternion(1,0,0,0)</code>. + */ + public Quaternion() { + this(1, 0, 0, 0); + } + + + public Quaternion(double w, double x, double y, double z) { + this.w = w; + this.x = x; + this.y = y; + this.z = z; + } + + + /** + * Create a rotation quaternion corresponding to the rotation vector. The rotation axis is + * the direction of vector, and rotation angle is the length of the vector. + * <p> + * The cost of the operation is approximately that of computing the length of the coordinate + * and computing two trigonometric functions. + * + * @param rotation the rotation vector + * @return the quaternion corresponding to the rotation vector + */ + public static Quaternion rotation(Coordinate rotation) { + double length = rotation.length(); + if (length < 0.000001) { + return new Quaternion(1, 0, 0, 0); + } + double sin = Math.sin(length / 2); + double cos = Math.cos(length / 2); + return new Quaternion(cos, + sin * rotation.x / length, sin * rotation.y / length, sin * rotation.z / length); + } + + /** + * Create a rotation quaternion corresponding to the rotation around the provided vector with + * the provided angle. + * + * @param axis the rotation axis + * @param angle the rotation angle + * @return the corresponding quaternion + */ + public static Quaternion rotation(Coordinate axis, double angle) { + Coordinate a = axis.normalize(); + double sin = Math.sin(angle); + double cos = Math.cos(angle); + return new Quaternion(cos, sin * a.x, sin * a.y, sin * a.z); + } + + + public double getW() { + return w; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getZ() { + return z; + } + + + /** + * Check whether any of the quaternion values is NaN. + * + * @return true if w, x, y or z is NaN + */ + public boolean isNaN() { + return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(w); + } + + + /** + * Multiply this quaternion by the other quaternion from the right side. This + * calculates the product <code>result = this * other</code>. + * + * @param other the quaternion to multiply this quaternion by. + * @return this quaternion. + */ + public Quaternion multiplyRight(Quaternion other) { + double newW = (this.w * other.w - this.x * other.x - this.y * other.y - this.z * other.z); + double newX = (this.w * other.x + this.x * other.w + this.y * other.z - this.z * other.y); + double newY = (this.w * other.y + this.y * other.w + this.z * other.x - this.x * other.z); + double newZ = (this.w * other.z + this.z * other.w + this.x * other.y - this.y * other.x); + + return new Quaternion(newW, newX, newY, newZ); + } + + /** + * Multiply this quaternion by the other quaternion from the left side. This + * calculates the product <code>result = other * this</code>. + * + * @param other the quaternion to multiply this quaternion by. + * @return this quaternion. + */ + public Quaternion multiplyLeft(Quaternion other) { + /* other(abcd) * this(wxyz) */ + + double newW = (other.w * this.w - other.x * this.x - other.y * this.y - other.z * this.z); + double newX = (other.w * this.x + other.x * this.w + other.y * this.z - other.z * this.y); + double newY = (other.w * this.y + other.y * this.w + other.z * this.x - other.x * this.z); + double newZ = (other.w * this.z + other.z * this.w + other.x * this.y - other.y * this.x); + + return new Quaternion(newW, newX, newY, newZ); + } + + + + + + /** + * Return a normalized version of this quaternion. If this quaternion is the zero quaternion, throws + * <code>IllegalStateException</code>. + * + * @return a normalized version of this quaternion. + * @throws IllegalStateException if the norm of this quaternion is zero. + */ + public Quaternion normalize() { + double n = norm(); + if (n < 0.0000001) { + throw new IllegalStateException("attempting to normalize zero-quaternion"); + } + return new Quaternion(w / n, x / n, y / n, z / n); + } + + + /** + * Normalize the quaternion if the norm is more than 1ppm from one. + * + * @return this quaternion or a normalized version of this quaternion. + * @throws IllegalStateException if the norm of this quaternion is zero. + */ + public Quaternion normalizeIfNecessary() { + double n2 = norm2(); + if (n2 < 0.999999 || n2 > 1.000001) { + return normalize(); + } else { + return this; + } + } + + + + /** + * Return the norm of this quaternion. + * + * @return the norm of this quaternion sqrt(w^2 + x^2 + y^2 + z^2). + */ + public double norm() { + if (norm < 0) { + norm = MathUtil.safeSqrt(x * x + y * y + z * z + w * w); + } + return norm; + } + + /** + * Return the square of the norm of this quaternion. + * + * @return the square of the norm of this quaternion (w^2 + x^2 + y^2 + z^2). + */ + public double norm2() { + return x * x + y * y + z * z + w * w; + } + + + /** + * Perform a coordinate rotation using this unit quaternion. The result is + * <code>this * coord * this^(-1)</code>. + * <p> + * This method assumes that the norm of this quaternion is one. + * + * @param coord the coordinate to rotate. + * @return the rotated coordinate. + */ + public Coordinate rotate(Coordinate coord) { + double a, b, c, d; + + assert (Math.abs(norm2() - 1) < 0.00001) : "Quaternion not unit length: " + this; + + + // (a,b,c,d) = this * coord = (w,x,y,z) * (0,cx,cy,cz) + a = -x * coord.x - y * coord.y - z * coord.z; // w + b = w * coord.x + y * coord.z - z * coord.y; // x i + c = w * coord.y - x * coord.z + z * coord.x; // y j + d = w * coord.z + x * coord.y - y * coord.x; // z k + + + // return = (a,b,c,d) * (this)^-1 = (a,b,c,d) * (w,-x,-y,-z) + + // Assert that the w-value is zero + assert (Math.abs(a * w + b * x + c * y + d * z) < coord.max() * MathUtil.EPSILON) : ("Should be zero: " + (a * w + b * x + c * y + d * z) + " in " + this + " c=" + coord); + + return new Coordinate( + -a * x + b * w - c * z + d * y, + -a * y + b * z + c * w - d * x, + -a * z - b * y + c * x + d * w, + coord.weight); + } + + /** + * Perform an inverse coordinate rotation using this unit quaternion. The result is + * <code>this^(-1) * coord * this</code>. + * <p> + * This method assumes that the norm of this quaternion is one. + * + * @param coord the coordinate to rotate. + * @return the rotated coordinate. + */ + public Coordinate invRotate(Coordinate coord) { + double a, b, c, d; + + assert (Math.abs(norm2() - 1) < 0.00001) : "Quaternion not unit length: " + this; + + // (a,b,c,d) = (this)^-1 * coord = (w,-x,-y,-z) * (0,cx,cy,cz) + a = +x * coord.x + y * coord.y + z * coord.z; + b = w * coord.x - y * coord.z + z * coord.y; + c = w * coord.y + x * coord.z - z * coord.x; + d = w * coord.z - x * coord.y + y * coord.x; + + + // return = (a,b,c,d) * this = (a,b,c,d) * (w,x,y,z) + assert (Math.abs(a * w - b * x - c * y - d * z) < Math.max(coord.max(), 1) * MathUtil.EPSILON) : ("Should be zero: " + (a * w - b * x - c * y - d * z) + " in " + this + " c=" + coord); + + return new Coordinate( + a * x + b * w + c * z - d * y, + a * y - b * z + c * w + d * x, + a * z + b * y - c * x + d * w, + coord.weight); + } + + + /** + * Rotate the coordinate (0,0,1) using this quaternion. The result is returned + * as a Coordinate. This method is equivalent to calling + * <code>q.rotate(new Coordinate(0,0,1))</code> but requires only about half of the + * multiplications. + * + * @return The coordinate (0,0,1) rotated using this quaternion. + */ + public Coordinate rotateZ() { + return new Coordinate( + 2 * (w * y + x * z), + 2 * (y * z - w * x), + w * w - x * x - y * y + z * z); + } + + + @Override + public String toString() { + return String.format("Quaternion[%f,%f,%f,%f,norm=%f]", w, x, y, z, this.norm()); + } + + public static void main(String[] arg) { + + Quaternion q = new Quaternion(Math.random() - 0.5, Math.random() - 0.5, + Math.random() - 0.5, Math.random() - 0.5); + q.normalize(); + + q = new Quaternion(-0.998717, 0.000000, 0.050649, -0.000000); + + Coordinate coord = new Coordinate(10 * (Math.random() - 0.5), + 10 * (Math.random() - 0.5), 10 * (Math.random() - 0.5)); + + System.out.println("Quaternion: " + q); + System.out.println("Coordinate: " + coord); + coord = q.invRotate(coord); + System.out.println("Rotated: " + coord); + coord = q.rotate(coord); + System.out.println("Back: " + coord); + + + q = new Quaternion(0.237188, 0.570190, -0.514542, 0.594872); + q.normalize(); + Coordinate c = new Coordinate(148578428.914, 8126778.954, -607.741); + + System.out.println("Rotated: " + q.rotate(c)); + + // Coordinate c = new Coordinate(0,1,0); + // Coordinate rot = new Coordinate(Math.PI/4,0,0); + // + // System.out.println("Before: "+c); + // c = rotation(rot).invRotate(c); + // System.out.println("After: "+c); + + + } + +} diff --git a/core/src/net/sf/openrocket/util/QuaternionMultiply.java b/core/src/net/sf/openrocket/util/QuaternionMultiply.java new file mode 100644 index 00000000..099ecf8d --- /dev/null +++ b/core/src/net/sf/openrocket/util/QuaternionMultiply.java @@ -0,0 +1,90 @@ +package net.sf.openrocket.util; + +public class QuaternionMultiply { + + private static class Value { + public int sign = 1; + public String value; + + public Value multiply(Value other) { + Value result = new Value(); + result.sign = this.sign * other.sign; + if (this.value.compareTo(other.value) < 0) + result.value = this.value + "*" + other.value; + else + result.value = other.value + "*" + this.value; + return result; + } + @Override + public String toString() { + String s; + + if (sign < 0) + s = "-"; + else + s = "+"; + + if (sign == 0) + s += " 0"; + else + s += " " + value; + + return s; + } + } + + + + private static Value[] multiply(Value[] first, Value[] second) { + return null; + } + + + public static void main(String[] arg) { + if (arg.length % 4 != 0 || arg.length < 4) { + System.out.println("Must have modulo 4 args, at least 4"); + return; + } + + Value[][] values = new Value[arg.length/4][4]; + + for (int i=0; i<arg.length; i++) { + Value value = new Value(); + + if (arg[i].equals("")) { + value.sign = 0; + } else { + if (arg[i].startsWith("-")) { + value.sign = -1; + value.value = arg[i].substring(1); + } else if (arg[i].startsWith("+")) { + value.sign = 1; + value.value = arg[i].substring(1); + } else { + value.sign = 1; + value.value = arg[i]; + } + } + + values[i/4][i%4] = value; + } + + System.out.println("Multiplying:"); + for (int i=0; i < values.length; i++) { + print(values[i]); + } + System.out.println("Result:"); + + Value[] result = values[0]; + for (int i=1; i < values.length; i++) { + result = multiply(result, values[i]); + } + print(result); + } + + private static void print(Value[] q) { + System.out.println(" " + q[0] + " " + q[1] + " i " + q[2] + " j " + q[3] + " k"); + } + +} + diff --git a/core/src/net/sf/openrocket/util/Reflection.java b/core/src/net/sf/openrocket/util/Reflection.java new file mode 100644 index 00000000..b3c66f62 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Reflection.java @@ -0,0 +1,197 @@ +package net.sf.openrocket.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +import net.sf.openrocket.rocketcomponent.RocketComponent; + + +public class Reflection { + + private static final String ROCKETCOMPONENT_PACKAGE = "net.sf.openrocket.rocketcomponent"; + + /** + * Simple wrapper class that converts the Method.invoke() exceptions into suitable + * RuntimeExceptions. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + public static class Method { + private final java.lang.reflect.Method method; + + public Method(java.lang.reflect.Method m) { + if (m == null) { + throw new IllegalArgumentException("method is null"); + } + method = m; + } + + /** + * Same as Method.invoke(), but the possible exceptions are wrapped into + * RuntimeExceptions. + */ + public Object invoke(Object obj, Object... args) { + try { + return method.invoke(obj, args); + } catch (IllegalArgumentException e) { + throw new BugException("Error while invoking method '" + method + "'. " + + "Please report this as a bug.", e); + } catch (IllegalAccessException e) { + throw new BugException("Error while invoking method '" + method + "'. " + + "Please report this as a bug.", e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + } + + /** + * Invoke static method. Equivalent to invoke(null, args...). + */ + public Object invokeStatic(Object... args) { + return invoke(null, args); + } + + /** + * Same as Method.toString(). + */ + @Override + public String toString() { + return method.toString(); + } + } + + + /** + * Handles an InvocationTargetException gracefully. If the cause is an unchecked + * exception it is thrown, otherwise it is encapsulated in a BugException. + * <p> + * This method has a return type of Error in order to allow writing code like: + * <pre>throw Reflection.handleInvocationTargetException(e)</pre> + * This allows the compiler verifying that the call will never succeed correctly + * and ending that branch of execution. + * + * @param e the InvocationTargetException that occurred (not null). + * @return never returns normally. + */ + public static Error handleWrappedException(Exception e) { + Throwable cause = e.getCause(); + if (cause == null) { + throw new BugException("wrapped exception without cause", e); + } + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new BugException("wrapped exception occurred", cause); + } + + + + /** + * Find a method from the rocket component classes. + * Throws an exception if method not found. + */ + public static Reflection.Method findMethod( + Class<? extends RocketComponent> componentClass, + String method, Class<?>... params) { + Reflection.Method m = findMethod(ROCKETCOMPONENT_PACKAGE, componentClass, + "", method, params); + if (m == null) { + throw new BugException("Could not find method for componentClass=" + + componentClass + " method=" + method); + } + return m; + } + + + + public static Reflection.Method findMethod(String pack, RocketComponent component, + String method, Class<?>... params) { + return findMethod(pack, component.getClass(), "", method, params); + } + + + public static Reflection.Method findMethod(String pack, RocketComponent component, + String suffix, String method, Class<?>... params) { + return findMethod(pack, component.getClass(), suffix, method, params); + } + + + public static Reflection.Method findMethod(String pack, + Class<? extends RocketComponent> componentClass, + String suffix, String method, Class<?>... params) { + Class<?> currentclass; + String name; + + currentclass = componentClass; + while ((currentclass != null) && (currentclass != Object.class)) { + name = currentclass.getCanonicalName(); + if (name.lastIndexOf('.') >= 0) + name = name.substring(name.lastIndexOf(".") + 1); + name = pack + "." + name + suffix; + + try { + Class<?> c = Class.forName(name); + java.lang.reflect.Method m = c.getMethod(method, params); + return new Reflection.Method(m); + } catch (ClassNotFoundException ignore) { + } catch (NoSuchMethodException ignore) { + } + + currentclass = currentclass.getSuperclass(); + } + return null; + } + + + public static Object construct(String pack, RocketComponent component, String suffix, + Object... params) { + + Class<?> currentclass; + String name; + + currentclass = component.getClass(); + while ((currentclass != null) && (currentclass != Object.class)) { + name = currentclass.getCanonicalName(); + if (name.lastIndexOf('.') >= 0) + name = name.substring(name.lastIndexOf(".") + 1); + name = pack + "." + name + suffix; + + try { + Class<?> c = Class.forName(name); + Class<?>[] paramClasses = new Class<?>[params.length]; + for (int i = 0; i < params.length; i++) { + paramClasses[i] = params[i].getClass(); + } + + // Constructors must be searched manually. Why?! + main: for (Constructor<?> constructor : c.getConstructors()) { + Class<?>[] parameterTypes = constructor.getParameterTypes(); + if (params.length != parameterTypes.length) + continue; + for (int i = 0; i < params.length; i++) { + if (!parameterTypes[i].isInstance(params[i])) + continue main; + } + // Matching constructor found + return constructor.newInstance(params); + } + } catch (ClassNotFoundException ignore) { + } catch (IllegalArgumentException e) { + throw new BugException("Construction of " + name + " failed", e); + } catch (InstantiationException e) { + throw new BugException("Construction of " + name + " failed", e); + } catch (IllegalAccessException e) { + throw new BugException("Construction of " + name + " failed", e); + } catch (InvocationTargetException e) { + throw Reflection.handleWrappedException(e); + } + + currentclass = currentclass.getSuperclass(); + } + throw new BugException("Suitable constructor for component " + component + + " not found"); + } +} diff --git a/core/src/net/sf/openrocket/util/Rotation2D.java b/core/src/net/sf/openrocket/util/Rotation2D.java new file mode 100644 index 00000000..1672a011 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Rotation2D.java @@ -0,0 +1,44 @@ +package net.sf.openrocket.util; + +public class Rotation2D { + + public static final Rotation2D ID = new Rotation2D(0.0, 1.0); + + public final double sin, cos; + + + public Rotation2D(double angle) { + this(Math.sin(angle), Math.cos(angle)); + } + + public Rotation2D(double sin, double cos) { + this.sin = sin; + this.cos = cos; + } + + public Coordinate rotateX(Coordinate c) { + return new Coordinate(c.x, cos*c.y - sin*c.z, cos*c.z + sin*c.y, c.weight); + } + + public Coordinate rotateY(Coordinate c) { + return new Coordinate(cos*c.x + sin*c.z, c.y, cos*c.z - sin*c.x, c.weight); + } + + public Coordinate rotateZ(Coordinate c) { + return new Coordinate(cos*c.x - sin*c.y, cos*c.y + sin*c.x, c.z, c.weight); + } + + + public Coordinate invRotateX(Coordinate c) { + return new Coordinate(c.x, cos*c.y + sin*c.z, cos*c.z - sin*c.y, c.weight); + } + + public Coordinate invRotateY(Coordinate c) { + return new Coordinate(cos*c.x - sin*c.z, c.y, cos*c.z + sin*c.x, c.weight); + } + + public Coordinate invRotateZ(Coordinate c) { + return new Coordinate(cos*c.x + sin*c.y, cos*c.y - sin*c.x, c.z, c.weight); + } + +} diff --git a/core/src/net/sf/openrocket/util/SafetyMutex.java b/core/src/net/sf/openrocket/util/SafetyMutex.java new file mode 100644 index 00000000..646c54e4 --- /dev/null +++ b/core/src/net/sf/openrocket/util/SafetyMutex.java @@ -0,0 +1,237 @@ +package net.sf.openrocket.util; + +import java.util.LinkedList; + +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.TraceException; +import net.sf.openrocket.startup.Application; + +/** + * A mutex that can be used for verifying thread safety. This class cannot be + * used to perform synchronization, only to detect concurrency issues. This + * class can be used by the main methods of non-thread-safe classes to ensure + * the class is not wrongly used from multiple threads concurrently. + * <p> + * This mutex is not reentrant even for the same thread that has locked it. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public abstract class SafetyMutex { + private static final boolean USE_CHECKS = Application.useSafetyChecks(); + private static final LogHelper log = Application.getLogger(); + + + /** + * Return a new instance of a safety mutex. This returns an actual implementation + * or a bogus implementation depending on whether safety checks are enabled or disabled. + * + * @return a new instance of a safety mutex + */ + public static SafetyMutex newInstance() { + if (USE_CHECKS) { + return new ConcreteSafetyMutex(); + } else { + return new BogusSafetyMutex(); + } + } + + + /** + * Verify that this mutex is unlocked, but don't lock it. This has the same effect + * as <code>mutex.lock(); mutex.unlock();</code> and is useful for methods that return + * immediately (e.g. getters). + * + * @throws ConcurrencyException if this mutex is already locked. + */ + public abstract void verify(); + + + /** + * Lock this mutex. If this mutex is already locked an error is raised and + * a ConcurrencyException is thrown. The location parameter is used to distinguish + * the locking location, and it should be e.g. the method name. + * + * @param location a string describing the location where this mutex was locked (cannot be null). + * + * @throws ConcurrencyException if this mutex is already locked. + */ + public abstract void lock(String location); + + + /** + * Unlock this mutex. If this mutex is not locked at the position of the parameter + * or was locked by another thread than the current thread an error is raised, + * but an exception is not thrown. + * <p> + * This method is guaranteed never to throw an exception, so it can safely be used in finally blocks. + * + * @param location a location string matching that which locked the mutex + * @return whether the unlocking was successful (this normally doesn't need to be checked) + */ + public abstract boolean unlock(String location); + + + + /** + * Bogus implementation of a safety mutex (used when safety checking is not performed). + */ + static class BogusSafetyMutex extends SafetyMutex { + + @Override + public void verify() { + } + + @Override + public void lock(String location) { + } + + @Override + public boolean unlock(String location) { + return true; + } + + } + + /** + * A concrete, working implementation of a safety mutex. + */ + static class ConcreteSafetyMutex extends SafetyMutex { + private static final boolean STORE_LOCKING_LOCATION = (System.getProperty("openrocket.debug.mutexlocation") != null); + + // Package-private for unit testing + static volatile boolean errorReported = false; + + // lockingThread is set when this mutex is locked. + Thread lockingThread = null; + // longingLocation is set when lockingThread is, if STORE_LOCKING_LOCATION is true + TraceException lockingLocation = null; + // Stack of places that have locked this mutex + final LinkedList<String> locations = new LinkedList<String>(); + + + + @Override + public synchronized void verify() { + checkState(true); + if (lockingThread != null && lockingThread != Thread.currentThread()) { + error("Mutex is already locked", true); + } + } + + + + @Override + public synchronized void lock(String location) { + if (location == null) { + throw new IllegalArgumentException("location is null"); + } + checkState(true); + + Thread currentThread = Thread.currentThread(); + if (lockingThread != null && lockingThread != currentThread) { + error("Mutex is already locked", true); + } + + lockingThread = currentThread; + if (STORE_LOCKING_LOCATION) { + lockingLocation = new TraceException("Location where mutex was locked '" + location + "'"); + } + locations.push(location); + } + + + + + @Override + public synchronized boolean unlock(String location) { + try { + + if (location == null) { + Application.getExceptionHandler().handleErrorCondition("location is null"); + location = ""; + } + checkState(false); + + + // Check that the mutex is locked + if (lockingThread == null) { + error("Mutex was not locked", false); + return false; + } + + // Check that the mutex is locked by the current thread + if (lockingThread != Thread.currentThread()) { + error("Mutex is being unlocked from differerent thread than where it was locked", false); + return false; + } + + // Check that the unlock location is correct + String lastLocation = locations.pop(); + if (!location.equals(lastLocation)) { + locations.push(lastLocation); + error("Mutex unlocking location does not match locking location, location=" + location, false); + return false; + } + + // Unlock the mutex if the last one + if (locations.isEmpty()) { + lockingThread = null; + lockingLocation = null; + } + return true; + } catch (Exception e) { + Application.getExceptionHandler().handleErrorCondition("An exception occurred while unlocking a mutex, " + + "locking thread=" + lockingThread + " locations=" + locations, e); + return false; + } + } + + + + /** + * Check that the internal state of the mutex (lockingThread vs. locations) is correct. + */ + private void checkState(boolean throwException) { + /* + * Disallowed states: + * lockingThread == null && !locations.isEmpty() + * lockingThread != null && locations.isEmpty() + */ + if ((lockingThread == null) ^ (locations.isEmpty())) { + // Clear the mutex only after error() has executed (and possibly thrown an exception) + try { + error("Mutex data inconsistency occurred - unlocking mutex", throwException); + } finally { + lockingThread = null; + lockingLocation = null; + locations.clear(); + } + } + } + + + /** + * Raise an error. The first occurrence is passed directly to the exception handler, + * later errors are simply logged. + */ + private void error(String message, boolean throwException) { + message = message + + ", current thread = " + Thread.currentThread() + + ", locking thread=" + lockingThread + + ", locking locations=" + locations; + + ConcurrencyException ex = new ConcurrencyException(message, lockingLocation); + + if (!errorReported) { + errorReported = true; + Application.getExceptionHandler().handleErrorCondition(ex); + } else { + log.error(message, ex); + } + + if (throwException) { + throw ex; + } + } + } +} diff --git a/core/src/net/sf/openrocket/util/StateChangeListener.java b/core/src/net/sf/openrocket/util/StateChangeListener.java new file mode 100644 index 00000000..2d5e5df9 --- /dev/null +++ b/core/src/net/sf/openrocket/util/StateChangeListener.java @@ -0,0 +1,10 @@ +package net.sf.openrocket.util; + +import java.util.EventListener; +import java.util.EventObject; + +public interface StateChangeListener extends EventListener { + + public void stateChanged( EventObject e ); + +} diff --git a/core/src/net/sf/openrocket/util/Statistics.java b/core/src/net/sf/openrocket/util/Statistics.java new file mode 100644 index 00000000..f96adf19 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Statistics.java @@ -0,0 +1,9 @@ +package net.sf.openrocket.util; + +public interface Statistics { + + public String getStatistics(); + + public void resetStatistics(); + +} diff --git a/core/src/net/sf/openrocket/util/TestRockets.java b/core/src/net/sf/openrocket/util/TestRockets.java new file mode 100644 index 00000000..0dfc5629 --- /dev/null +++ b/core/src/net/sf/openrocket/util/TestRockets.java @@ -0,0 +1,569 @@ +package net.sf.openrocket.util; + +import java.util.Random; + +import net.sf.openrocket.material.Material; +import net.sf.openrocket.material.Material.Type; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.rocketcomponent.BodyTube; +import net.sf.openrocket.rocketcomponent.Bulkhead; +import net.sf.openrocket.rocketcomponent.CenteringRing; +import net.sf.openrocket.rocketcomponent.ExternalComponent; +import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; +import net.sf.openrocket.rocketcomponent.FinSet.CrossSection; +import net.sf.openrocket.rocketcomponent.FreeformFinSet; +import net.sf.openrocket.rocketcomponent.IllegalFinPointException; +import net.sf.openrocket.rocketcomponent.InnerTube; +import net.sf.openrocket.rocketcomponent.InternalComponent; +import net.sf.openrocket.rocketcomponent.LaunchLug; +import net.sf.openrocket.rocketcomponent.MassComponent; +import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; +import net.sf.openrocket.rocketcomponent.NoseCone; +import net.sf.openrocket.rocketcomponent.ReferenceType; +import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.rocketcomponent.RocketComponent; +import net.sf.openrocket.rocketcomponent.RocketComponent.Position; +import net.sf.openrocket.rocketcomponent.Stage; +import net.sf.openrocket.rocketcomponent.Transition; +import net.sf.openrocket.rocketcomponent.Transition.Shape; +import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; +import net.sf.openrocket.rocketcomponent.TubeCoupler; +import net.sf.openrocket.startup.Application; + +public class TestRockets { + + private final String key; + private final Random rnd; + + + public TestRockets(String key) { + + if (key == null) { + Random rnd = new Random(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 6; i++) { + int n = rnd.nextInt(62); + if (n < 10) { + sb.append((char) ('0' + n)); + } else if (n < 36) { + sb.append((char) ('A' + n - 10)); + } else { + sb.append((char) ('a' + n - 36)); + } + } + key = sb.toString(); + } + + this.key = key; + this.rnd = new Random(key.hashCode()); + + } + + + /** + * Create a new test rocket based on the value 'key'. The rocket utilizes most of the + * properties and features available. The same key always returns the same rocket, + * but different key values produce slightly different rockets. A key value of + * <code>null</code> generates a rocket using a random key. + * <p> + * The rocket created by this method is not fly-worthy. It is also NOT guaranteed + * that later versions would produce exactly the same rocket! + * + * @return a rocket design. + */ + public Rocket makeTestRocket() { + + Rocket rocket = new Rocket(); + setBasics(rocket); + rocket.setCustomReferenceLength(rnd(0.05)); + rocket.setDesigner("Designer " + key); + rocket.setReferenceType((ReferenceType) randomEnum(ReferenceType.class)); + rocket.setRevision("Rocket revision " + key); + rocket.setName(key); + + + Stage stage = new Stage(); + setBasics(stage); + rocket.addChild(stage); + + + NoseCone nose = new NoseCone(); + setBasics(stage); + nose.setAftRadius(rnd(0.03)); + nose.setAftRadiusAutomatic(rnd.nextBoolean()); + nose.setAftShoulderCapped(rnd.nextBoolean()); + nose.setAftShoulderLength(rnd(0.02)); + nose.setAftShoulderRadius(rnd(0.02)); + nose.setAftShoulderThickness(rnd(0.002)); + nose.setClipped(rnd.nextBoolean()); + nose.setThickness(rnd(0.002)); + nose.setFilled(rnd.nextBoolean()); + nose.setForeRadius(rnd(0.1)); // Unset + nose.setLength(rnd(0.15)); + nose.setShapeParameter(rnd(0.5)); + nose.setType((Shape) randomEnum(Shape.class)); + stage.addChild(nose); + + + Transition shoulder = new Transition(); + setBasics(shoulder); + shoulder.setAftRadius(rnd(0.06)); + shoulder.setAftRadiusAutomatic(rnd.nextBoolean()); + shoulder.setAftShoulderCapped(rnd.nextBoolean()); + shoulder.setAftShoulderLength(rnd(0.02)); + shoulder.setAftShoulderRadius(rnd(0.05)); + shoulder.setAftShoulderThickness(rnd(0.002)); + shoulder.setClipped(rnd.nextBoolean()); + shoulder.setThickness(rnd(0.002)); + shoulder.setFilled(rnd.nextBoolean()); + shoulder.setForeRadius(rnd(0.03)); + shoulder.setForeRadiusAutomatic(rnd.nextBoolean()); + shoulder.setForeShoulderCapped(rnd.nextBoolean()); + shoulder.setForeShoulderLength(rnd(0.02)); + shoulder.setForeShoulderRadius(rnd(0.02)); + shoulder.setForeShoulderThickness(rnd(0.002)); + shoulder.setLength(rnd(0.15)); + shoulder.setShapeParameter(rnd(0.5)); + shoulder.setThickness(rnd(0.003)); + shoulder.setType((Shape) randomEnum(Shape.class)); + stage.addChild(shoulder); + + + BodyTube body = new BodyTube(); + setBasics(body); + body.setThickness(rnd(0.002)); + body.setFilled(rnd.nextBoolean()); + body.setIgnitionDelay(rnd.nextDouble() * 3); + body.setIgnitionEvent((IgnitionEvent) randomEnum(IgnitionEvent.class)); + body.setLength(rnd(0.3)); + body.setMotorMount(rnd.nextBoolean()); + body.setMotorOverhang(rnd.nextGaussian() * 0.03); + body.setOuterRadius(rnd(0.06)); + body.setOuterRadiusAutomatic(rnd.nextBoolean()); + stage.addChild(body); + + + Transition boattail = new Transition(); + setBasics(boattail); + boattail.setAftRadius(rnd(0.03)); + boattail.setAftRadiusAutomatic(rnd.nextBoolean()); + boattail.setAftShoulderCapped(rnd.nextBoolean()); + boattail.setAftShoulderLength(rnd(0.02)); + boattail.setAftShoulderRadius(rnd(0.02)); + boattail.setAftShoulderThickness(rnd(0.002)); + boattail.setClipped(rnd.nextBoolean()); + boattail.setThickness(rnd(0.002)); + boattail.setFilled(rnd.nextBoolean()); + boattail.setForeRadius(rnd(0.06)); + boattail.setForeRadiusAutomatic(rnd.nextBoolean()); + boattail.setForeShoulderCapped(rnd.nextBoolean()); + boattail.setForeShoulderLength(rnd(0.02)); + boattail.setForeShoulderRadius(rnd(0.05)); + boattail.setForeShoulderThickness(rnd(0.002)); + boattail.setLength(rnd(0.15)); + boattail.setShapeParameter(rnd(0.5)); + boattail.setThickness(rnd(0.003)); + boattail.setType((Shape) randomEnum(Shape.class)); + stage.addChild(boattail); + + + MassComponent mass = new MassComponent(); + setBasics(mass); + mass.setComponentMass(rnd(0.05)); + mass.setLength(rnd(0.05)); + mass.setRadialDirection(rnd(100)); + mass.setRadialPosition(rnd(0.02)); + mass.setRadius(rnd(0.05)); + nose.addChild(mass); + + + + + return rocket; + } + + + private void setBasics(RocketComponent c) { + c.setComment(c.getComponentName() + " comment " + key); + c.setName(c.getComponentName() + " name " + key); + + c.setCGOverridden(rnd.nextBoolean()); + c.setMassOverridden(rnd.nextBoolean()); + c.setOverrideCGX(rnd(0.2)); + c.setOverrideMass(rnd(0.05)); + c.setOverrideSubcomponents(rnd.nextBoolean()); + + if (c.isMassive()) { + // Only massive components are drawn + c.setColor(randomColor()); + c.setLineStyle((LineStyle) randomEnum(LineStyle.class)); + } + + if (c instanceof ExternalComponent) { + ExternalComponent e = (ExternalComponent) c; + e.setFinish((Finish) randomEnum(Finish.class)); + double d = rnd(100); + e.setMaterial(Material.newMaterial(Type.BULK, "Testmat " + d, d, rnd.nextBoolean())); + } + + if (c instanceof InternalComponent) { + InternalComponent i = (InternalComponent) c; + i.setRelativePosition((Position) randomEnum(Position.class)); + i.setPositionValue(rnd(0.3)); + } + } + + + + private double rnd(double scale) { + return (rnd.nextDouble() * 0.2 + 0.9) * scale; + } + + private Color randomColor() { + return new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)); + } + + private <T extends Enum<T>> Enum<T> randomEnum(Class<T> c) { + Enum<T>[] values = c.getEnumConstants(); + if (values.length == 0) + return null; + + return values[rnd.nextInt(values.length)]; + } + + + + + + public Rocket makeSmallFlyable() { + double noseconeLength = 0.10, noseconeRadius = 0.01; + double bodytubeLength = 0.20, bodytubeRadius = 0.01, bodytubeThickness = 0.001; + + int finCount = 3; + double finRootChord = 0.04, finTipChord = 0.05, finSweep = 0.01, finThickness = 0.003, finHeight = 0.03; + + + Rocket rocket; + Stage stage; + NoseCone nosecone; + BodyTube bodytube; + TrapezoidFinSet finset; + + rocket = new Rocket(); + stage = new Stage(); + stage.setName("Stage1"); + + nosecone = new NoseCone(Transition.Shape.ELLIPSOID, noseconeLength, noseconeRadius); + bodytube = new BodyTube(bodytubeLength, bodytubeRadius, bodytubeThickness); + + finset = new TrapezoidFinSet(finCount, finRootChord, finTipChord, finSweep, finHeight); + + + // Stage construction + rocket.addChild(stage); + + + // Component construction + stage.addChild(nosecone); + stage.addChild(bodytube); + + bodytube.addChild(finset); + + Material material = Application.getPreferences().getDefaultComponentMaterial(null, Material.Type.BULK); + nosecone.setMaterial(material); + bodytube.setMaterial(material); + finset.setMaterial(material); + + String id = rocket.newMotorConfigurationID(); + bodytube.setMotorMount(true); + + Motor m = Application.getMotorSetDatabase().findMotors(null, null, "B4", Double.NaN, Double.NaN).get(0); + bodytube.setMotor(id, m); + bodytube.setMotorOverhang(0.005); + rocket.getDefaultConfiguration().setMotorConfigurationID(id); + + rocket.getDefaultConfiguration().setAllStages(); + + + return rocket; + } + + + public static Rocket makeBigBlue() { + Rocket rocket; + Stage stage; + NoseCone nosecone; + BodyTube bodytube; + FreeformFinSet finset; + MassComponent mcomp; + + rocket = new Rocket(); + stage = new Stage(); + stage.setName("Stage1"); + + nosecone = new NoseCone(Transition.Shape.ELLIPSOID, 0.105, 0.033); + nosecone.setThickness(0.001); + bodytube = new BodyTube(0.69, 0.033, 0.001); + + finset = new FreeformFinSet(); + try { + finset.setPoints(new Coordinate[] { + new Coordinate(0, 0), + new Coordinate(0.115, 0.072), + new Coordinate(0.255, 0.072), + new Coordinate(0.255, 0.037), + new Coordinate(0.150, 0) + }); + } catch (IllegalFinPointException e) { + e.printStackTrace(); + } + finset.setThickness(0.003); + finset.setFinCount(4); + + finset.setCantAngle(0 * Math.PI / 180); + System.err.println("Fin cant angle: " + (finset.getCantAngle() * 180 / Math.PI)); + + mcomp = new MassComponent(0.2, 0.03, 0.045 + 0.060); + mcomp.setRelativePosition(Position.TOP); + mcomp.setPositionValue(0); + + // Stage construction + rocket.addChild(stage); + rocket.setPerfectFinish(false); + + + // Component construction + stage.addChild(nosecone); + stage.addChild(bodytube); + + bodytube.addChild(finset); + + bodytube.addChild(mcomp); + + // Material material = new Material("Test material", 500); + // nosecone.setMaterial(material); + // bodytube.setMaterial(material); + // finset.setMaterial(material); + + String id = rocket.newMotorConfigurationID(); + bodytube.setMotorMount(true); + + // Motor m = Application.getMotorSetDatabase().findMotors(null, null, "F12J", Double.NaN, Double.NaN).get(0); + // bodytube.setMotor(id, m); + // bodytube.setMotorOverhang(0.005); + rocket.getDefaultConfiguration().setMotorConfigurationID(id); + + rocket.getDefaultConfiguration().setAllStages(); + + + return rocket; + } + + + + public static Rocket makeIsoHaisu() { + Rocket rocket; + Stage stage; + NoseCone nosecone; + BodyTube tube1, tube2, tube3; + TrapezoidFinSet finset; + TrapezoidFinSet auxfinset; + MassComponent mcomp; + + final double R = 0.07; + + rocket = new Rocket(); + stage = new Stage(); + stage.setName("Stage1"); + + nosecone = new NoseCone(Transition.Shape.OGIVE, 0.53, R); + nosecone.setThickness(0.005); + nosecone.setMassOverridden(true); + nosecone.setOverrideMass(0.588); + stage.addChild(nosecone); + + tube1 = new BodyTube(0.505, R, 0.005); + tube1.setMassOverridden(true); + tube1.setOverrideMass(0.366); + stage.addChild(tube1); + + tube2 = new BodyTube(0.605, R, 0.005); + tube2.setMassOverridden(true); + tube2.setOverrideMass(0.427); + stage.addChild(tube2); + + tube3 = new BodyTube(1.065, R, 0.005); + tube3.setMassOverridden(true); + tube3.setOverrideMass(0.730); + stage.addChild(tube3); + + + LaunchLug lug = new LaunchLug(); + tube1.addChild(lug); + + TubeCoupler coupler = new TubeCoupler(); + coupler.setOuterRadiusAutomatic(true); + coupler.setThickness(0.005); + coupler.setLength(0.28); + coupler.setMassOverridden(true); + coupler.setOverrideMass(0.360); + coupler.setRelativePosition(Position.BOTTOM); + coupler.setPositionValue(-0.14); + tube1.addChild(coupler); + + + // Parachute + MassComponent mass = new MassComponent(0.05, 0.05, 0.280); + mass.setRelativePosition(Position.TOP); + mass.setPositionValue(0.2); + tube1.addChild(mass); + + // Cord + mass = new MassComponent(0.05, 0.05, 0.125); + mass.setRelativePosition(Position.TOP); + mass.setPositionValue(0.2); + tube1.addChild(mass); + + // Payload + mass = new MassComponent(0.40, R, 1.500); + mass.setRelativePosition(Position.TOP); + mass.setPositionValue(0.25); + tube1.addChild(mass); + + + auxfinset = new TrapezoidFinSet(); + auxfinset.setName("CONTROL"); + auxfinset.setFinCount(2); + auxfinset.setRootChord(0.05); + auxfinset.setTipChord(0.05); + auxfinset.setHeight(0.10); + auxfinset.setSweep(0); + auxfinset.setThickness(0.008); + auxfinset.setCrossSection(CrossSection.AIRFOIL); + auxfinset.setRelativePosition(Position.TOP); + auxfinset.setPositionValue(0.28); + auxfinset.setBaseRotation(Math.PI / 2); + tube1.addChild(auxfinset); + + + + + coupler = new TubeCoupler(); + coupler.setOuterRadiusAutomatic(true); + coupler.setLength(0.28); + coupler.setRelativePosition(Position.TOP); + coupler.setPositionValue(0.47); + coupler.setMassOverridden(true); + coupler.setOverrideMass(0.360); + tube2.addChild(coupler); + + + + // Parachute + mass = new MassComponent(0.1, 0.05, 0.028); + mass.setRelativePosition(Position.TOP); + mass.setPositionValue(0.14); + tube2.addChild(mass); + + Bulkhead bulk = new Bulkhead(); + bulk.setOuterRadiusAutomatic(true); + bulk.setMassOverridden(true); + bulk.setOverrideMass(0.050); + bulk.setRelativePosition(Position.TOP); + bulk.setPositionValue(0.27); + tube2.addChild(bulk); + + // Chord + mass = new MassComponent(0.1, 0.05, 0.125); + mass.setRelativePosition(Position.TOP); + mass.setPositionValue(0.19); + tube2.addChild(mass); + + + + InnerTube inner = new InnerTube(); + inner.setOuterRadius(0.08 / 2); + inner.setInnerRadius(0.0762 / 2); + inner.setLength(0.86); + inner.setMassOverridden(true); + inner.setOverrideMass(0.388); + tube3.addChild(inner); + + + CenteringRing center = new CenteringRing(); + center.setInnerRadiusAutomatic(true); + center.setOuterRadiusAutomatic(true); + center.setLength(0.005); + center.setMassOverridden(true); + center.setOverrideMass(0.038); + center.setRelativePosition(Position.BOTTOM); + center.setPositionValue(0); + tube3.addChild(center); + + + center = new CenteringRing(); + center.setInnerRadiusAutomatic(true); + center.setOuterRadiusAutomatic(true); + center.setLength(0.005); + center.setMassOverridden(true); + center.setOverrideMass(0.038); + center.setRelativePosition(Position.TOP); + center.setPositionValue(0.28); + tube3.addChild(center); + + + center = new CenteringRing(); + center.setInnerRadiusAutomatic(true); + center.setOuterRadiusAutomatic(true); + center.setLength(0.005); + center.setMassOverridden(true); + center.setOverrideMass(0.038); + center.setRelativePosition(Position.TOP); + center.setPositionValue(0.83); + tube3.addChild(center); + + + + + + finset = new TrapezoidFinSet(); + finset.setRootChord(0.495); + finset.setTipChord(0.1); + finset.setHeight(0.185); + finset.setThickness(0.005); + finset.setSweep(0.3); + finset.setRelativePosition(Position.BOTTOM); + finset.setPositionValue(-0.03); + finset.setBaseRotation(Math.PI / 2); + tube3.addChild(finset); + + + finset.setCantAngle(0 * Math.PI / 180); + System.err.println("Fin cant angle: " + (finset.getCantAngle() * 180 / Math.PI)); + + + // Stage construction + rocket.addChild(stage); + rocket.setPerfectFinish(false); + + + + String id = rocket.newMotorConfigurationID(); + tube3.setMotorMount(true); + + // Motor m = Application.getMotorSetDatabase().findMotors(null, null, "L540", Double.NaN, Double.NaN).get(0); + // tube3.setMotor(id, m); + // tube3.setMotorOverhang(0.02); + rocket.getDefaultConfiguration().setMotorConfigurationID(id); + + // tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER); + + rocket.getDefaultConfiguration().setAllStages(); + + + return rocket; + } + + + +} diff --git a/core/src/net/sf/openrocket/util/TextUtil.java b/core/src/net/sf/openrocket/util/TextUtil.java new file mode 100644 index 00000000..32dffd84 --- /dev/null +++ b/core/src/net/sf/openrocket/util/TextUtil.java @@ -0,0 +1,167 @@ +package net.sf.openrocket.util; + + +public class TextUtil { + private static final char[] HEX = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + + /** + * Return the bytes formatted as a hexadecimal string. The length of the + * string will be twice the number of bytes, with no spacing between the bytes + * and lowercase letters utilized. + * + * @param bytes the bytes to convert. + * @return the bytes in hexadecimal notation. + */ + public static final String hexString(byte[] bytes) { + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + sb.append(HEX[(b >>> 4) & 0xF]); + sb.append(HEX[b & 0xF]); + } + return sb.toString(); + } + + /** + * Return a string of the double value with suitable precision (5 digits). + * The string is the shortest representation of the value including the + * required precision. + * + * @param d the value to present. + * @return a representation with suitable precision. + */ + public static final String doubleToString(double d) { + + // Check for special cases + if (MathUtil.equals(d, 0)) + return "0"; + + if (Double.isNaN(d)) + return "NaN"; + + if (Double.isInfinite(d)) { + if (d < 0) + return "-Inf"; + else + return "Inf"; + } + + + final String sign = (d < 0) ? "-" : ""; + double abs = Math.abs(d); + + // Small and large values always in exponential notation + if (abs < 0.001 || abs >= 100000000) { + return sign + exponentialFormat(abs); + } + + // Check whether decimal or exponential notation is shorter + + String exp = exponentialFormat(abs); + String dec = decimalFormat(abs); + + if (dec.length() <= exp.length()) + return sign + dec; + else + return sign + exp; + } + + + /* + * value must be positive and not zero! + */ + private static String exponentialFormat(double value) { + int exp; + + exp = 0; + while (value < 1.0) { + value *= 10; + exp--; + } + while (value >= 10.0) { + value /= 10; + exp++; + } + + return shortDecimal(value, 4) + "e" + exp; + } + + + /* + * value must be positive and not zero! + */ + private static String decimalFormat(double value) { + if (value >= 10000) + return "" + (int) (value + 0.5); + + int decimals = 1; + double v = value; + while (v < 1000) { + v *= 10; + decimals++; + } + + return shortDecimal(value, decimals); + } + + + + + /* + * value must be positive! + */ + private static String shortDecimal(double value, int decimals) { + + // Calculate rounding and limit values (rounding slightly smaller) + int rounding = 1; + double limit = 0.5; + for (int i = 0; i < decimals; i++) { + rounding *= 10; + limit /= 10; + } + + // Round value + value = (Math.rint(value * rounding) + 0.1) / rounding; + + + int whole = (int) value; + value -= whole; + + + if (value < limit) + return "" + whole; + limit *= 10; + + StringBuilder sb = new StringBuilder(); + sb.append("" + whole); + sb.append('.'); + + + for (int i = 0; i < decimals; i++) { + + value *= 10; + whole = (int) value; + value -= whole; + sb.append((char) ('0' + whole)); + + if (value < limit) + return sb.toString(); + limit *= 10; + + } + + return sb.toString(); + } + + + public static String htmlEncode(String s) { + s = s.replace("&", "&"); + s = s.replace("\"", """); + s = s.replace("<", "<"); + s = s.replace(">", ">"); + return s; + } +} diff --git a/core/src/net/sf/openrocket/util/Transformation.java b/core/src/net/sf/openrocket/util/Transformation.java new file mode 100644 index 00000000..2c546974 --- /dev/null +++ b/core/src/net/sf/openrocket/util/Transformation.java @@ -0,0 +1,279 @@ +package net.sf.openrocket.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; + +/** + * Defines an affine transformation of the form A*x+c, where x and c are Coordinates and + * A is a 3x3 matrix. + * + * The Transformations are immutable. All modification methods return a new transformation. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ + +public class Transformation implements java.io.Serializable { + + + public static final Transformation IDENTITY = + new Transformation(); + + public static final Transformation PROJECT_XY = + new Transformation(new double[][]{{1,0,0},{0,1,0},{0,0,0}}); + public static final Transformation PROJECT_YZ = + new Transformation(new double[][]{{0,0,0},{0,1,0},{0,0,1}}); + public static final Transformation PROJECT_XZ = + new Transformation(new double[][]{{1,0,0},{0,0,0},{0,0,1}}); + + private static final int X = 0; + private static final int Y = 1; + private static final int Z = 2; + + private final Coordinate translate; + private final double[][] rotation = new double[3][3]; + + /** + * Create identity transformation. + */ + public Transformation() { + translate = new Coordinate(0,0,0); + rotation[X][X]=1; + rotation[Y][Y]=1; + rotation[Z][Z]=1; + } + + /** + * Create transformation with only translation. + * @param x Translation in x-axis. + * @param y Translation in y-axis. + * @param z Translation in z-axis. + */ + public Transformation(double x,double y,double z) { + translate = new Coordinate(x,y,z); + rotation[X][X]=1; + rotation[Y][Y]=1; + rotation[Z][Z]=1; + } + + /** + * Create transformation with only translation. + * @param translation The translation term. + */ + public Transformation(Coordinate translation) { + this.translate = translation; + rotation[X][X]=1; + rotation[Y][Y]=1; + rotation[Z][Z]=1; + } + + /** + * Create transformation with given rotation matrix and translation. + * @param rotation + * @param translation + */ + public Transformation(double[][] rotation, Coordinate translation) { + for (int i=0; i<3; i++) + for (int j=0; j<3; j++) + this.rotation[i][j] = rotation[i][j]; + this.translate = translation; + } + + + /** + * Create transformation with given rotation matrix and translation. + * @param rotation + * @param translation + */ + public Transformation(double[][] rotation) { + for (int i=0; i<3; i++) + for (int j=0; j<3; j++) + this.rotation[i][j] = rotation[i][j]; + this.translate = Coordinate.NUL; + } + + + + + + /** + * Transform a coordinate according to this transformation. + * + * @param orig the coordinate to transform. + * @return the result. + */ + public Coordinate transform(Coordinate orig) { + final double x,y,z; + + x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z + translate.x; + y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z + translate.y; + z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z + translate.z; + + return new Coordinate(x,y,z,orig.weight); + } + + + /** + * Transform an array of coordinates. The transformed coordinates are stored + * in the same array, and the array is returned. + * + * @param orig the coordinates to transform. + * @return <code>orig</code>, with the coordinates transformed. + */ + public Coordinate[] transform(Coordinate[] orig) { + for (int i=0; i < orig.length; i++) { + orig[i] = transform(orig[i]); + } + return orig; + } + + /** + * Transforms all coordinates in a Collection. The original coordinate elements are + * removed from the set and replaced with the transformed ones. The Collection given + * must implement the .clear() and .addAll() methods. + * + * @param set Collection of coordinates to transform. + */ + public void transform(Collection<Coordinate> set) { + ArrayList<Coordinate> temp = new ArrayList<Coordinate>(set.size()); + Iterator<Coordinate> iter = set.iterator(); + while (iter.hasNext()) + temp.add(this.transform(iter.next())); + set.clear(); + set.addAll(temp); + } + + /** + * Applies only the linear transformation A*x + * @param orig Coordinate to transform. + */ + public Coordinate linearTransform(Coordinate orig) { + final double x,y,z; + + x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z; + y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z; + z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z; + + return new Coordinate(x,y,z,orig.weight); + } + + /** + * Applies the given transformation before this tranformation. The resulting + * transformation result.transform(c) will equal this.transform(other.transform(c)). + * + * @param other Transformation to apply + * @return The new transformation + */ + public Transformation applyTransformation(Transformation other) { + // other = Ax+b + // this = Cx+d + // C(Ax+b)+d = CAx + Cb+d + + // Translational portion + Transformation combined = new Transformation( + this.linearTransform(other.translate).add(this.translate) + ); + + // Linear portion + for (int i=0; i<3; i++) { + final double x,y,z; + x = rotation[i][X]; + y = rotation[i][Y]; + z = rotation[i][Z]; + combined.rotation[i][X] = + x*other.rotation[X][X] + y*other.rotation[Y][X] + z*other.rotation[Z][X]; + combined.rotation[i][Y] = + x*other.rotation[X][Y] + y*other.rotation[Y][Y] + z*other.rotation[Z][Y]; + combined.rotation[i][Z] = + x*other.rotation[X][Z] + y*other.rotation[Y][Z] + z*other.rotation[Z][Z]; + } + return combined; + } + + + /** + * Rotate around x-axis a given angle. + * @param theta The angle to rotate in radians. + * @return The transformation. + */ + public static Transformation rotate_x(double theta) { + return new Transformation(new double[][]{ + {1,0,0}, + {0,Math.cos(theta),-Math.sin(theta)}, + {0,Math.sin(theta),Math.cos(theta)}}); + } + + /** + * Rotate around y-axis a given angle. + * @param theta The angle to rotate in radians. + * @return The transformation. + */ + public static Transformation rotate_y(double theta) { + return new Transformation(new double[][]{ + {Math.cos(theta),0,Math.sin(theta)}, + {0,1,0}, + {-Math.sin(theta),0,Math.cos(theta)}}); + } + + /** + * Rotate around z-axis a given angle. + * @param theta The angle to rotate in radians. + * @return The transformation. + */ + public static Transformation rotate_z(double theta) { + return new Transformation(new double[][]{ + {Math.cos(theta),-Math.sin(theta),0}, + {Math.sin(theta),Math.cos(theta),0}, + {0,0,1}}); + } + + + + public void print(String... str) { + for (String s: str) { + System.out.println(s); + } + System.out.printf("[%3.2f %3.2f %3.2f] [%3.2f]\n", + rotation[X][X],rotation[X][Y],rotation[X][Z],translate.x); + System.out.printf("[%3.2f %3.2f %3.2f] + [%3.2f]\n", + rotation[Y][X],rotation[Y][Y],rotation[Y][Z],translate.y); + System.out.printf("[%3.2f %3.2f %3.2f] [%3.2f]\n", + rotation[Z][X],rotation[Z][Y],rotation[Z][Z],translate.z); + System.out.println(); + } + + + @Override + public boolean equals(Object other) { + if (!(other instanceof Transformation)) + return false; + Transformation o = (Transformation)other; + for (int i=0; i<3; i++) { + for (int j=0; j<3; j++) { + if (!MathUtil.equals(this.rotation[i][j], o.rotation[i][j])) + return false; + } + } + return this.translate.equals(o.translate); + } + + public static void main(String[] arg) { + Transformation t; + + t = new Transformation(); + t.print("Empty"); + t = new Transformation(1,2,3); + t.print("1,2,3"); + t = new Transformation(new Coordinate(2,3,4)); + t.print("coord 2,3 4"); + + t = Transformation.rotate_y(0.01); + t.print("rotate_y 0.01"); + + t = new Transformation(-1,0,0); + t = t.applyTransformation(Transformation.rotate_y(0.01)); + t = t.applyTransformation(new Transformation(1,0,0)); + t.print("shift-rotate-shift"); + } + +} diff --git a/core/src/net/sf/openrocket/util/UncloseableInputStream.java b/core/src/net/sf/openrocket/util/UncloseableInputStream.java new file mode 100644 index 00000000..ca0779fc --- /dev/null +++ b/core/src/net/sf/openrocket/util/UncloseableInputStream.java @@ -0,0 +1,23 @@ +package net.sf.openrocket.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An InputStream filter that prevents closing the source stream. The + * {@link #close()} method is overridden to do nothing. + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class UncloseableInputStream extends FilterInputStream { + + public UncloseableInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + // No-op + } +} diff --git a/core/src/net/sf/openrocket/util/UniqueID.java b/core/src/net/sf/openrocket/util/UniqueID.java new file mode 100644 index 00000000..f9987c76 --- /dev/null +++ b/core/src/net/sf/openrocket/util/UniqueID.java @@ -0,0 +1,38 @@ +package net.sf.openrocket.util; + +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +public class UniqueID { + + private static AtomicInteger nextId = new AtomicInteger(1); + + /** + * Return a positive integer ID unique during this program execution. + * <p> + * The following is guaranteed of the returned ID values: + * <ul> + * <li>The value is unique during this program execution + * <li>The value is positive + * <li>The values are monotonically increasing + * </ul> + * <p> + * This method is thread-safe and fast. + * + * @return a positive integer ID unique in this program execution. + */ + public static int next() { + return nextId.getAndIncrement(); + } + + + /** + * Return a new universally unique ID string. + * + * @return a unique identifier string. + */ + public static String uuid() { + return UUID.randomUUID().toString(); + } + +} diff --git a/core/src/net/sf/openrocket/util/Utils.java b/core/src/net/sf/openrocket/util/Utils.java new file mode 100644 index 00000000..93d320ef --- /dev/null +++ b/core/src/net/sf/openrocket/util/Utils.java @@ -0,0 +1,37 @@ +package net.sf.openrocket.util; + +public class Utils { + + /** + * Null-safe equals method. + * + * @param first the first object to compare + * @param second the second object to compare + * @return whether the two objects are both equal or both <code>null</code> + */ + public static boolean equals(Object first, Object second) { + if (first == null) { + return second == null; + } else { + return first.equals(second); + } + } + + + /** + * Check whether an array contains a specified object. + * + * @param array the array to search + * @param search the object to search for + * @return whether the object was in the array + */ + public static boolean contains(Object[] array, Object search) { + for (Object o : array) { + if (equals(o, search)) { + return true; + } + } + return false; + } + +} diff --git a/core/src/net/sf/openrocket/util/WorldCoordinate.java b/core/src/net/sf/openrocket/util/WorldCoordinate.java new file mode 100644 index 00000000..762b0295 --- /dev/null +++ b/core/src/net/sf/openrocket/util/WorldCoordinate.java @@ -0,0 +1,89 @@ +package net.sf.openrocket.util; + +/** + * A WorldCoordinate contains the latitude, longitude and altitude position of a rocket. + */ +public class WorldCoordinate { + + /** Mean Earth radius */ + public static final double REARTH = 6371000.0; + /** Sidearial Earth rotation rate */ + public static final double EROT = 7.2921150e-5; + + + private final double lat, lon, alt; + + /** + * Constructs a new WorldCoordinate + * + * @param lat latitude in degrees north. From -90 to 90, values outside are clamped. + * @param lon longitude in degrees east. From -180 to 180, values outside are reduced to the range. + * @param alt altitude in meters. Unbounded. + */ + public WorldCoordinate(double lat, double lon, double alt) { + this.lat = MathUtil.clamp(Math.toRadians(lat), -Math.PI / 2, Math.PI / 2); + this.lon = MathUtil.reduce180(Math.toRadians(lon)); + this.alt = alt; + } + + + /** + * Returns the altitude. + */ + public double getAltitude() { + return this.alt; + } + + /** + * Returns Longitude in radians + */ + public double getLongitudeRad() { + return this.lon; + } + + /** + * Returns Longitude in degrees + */ + public double getLongitudeDeg() { + return Math.toDegrees(this.lon); + } + + /** + * Returns latitude in radians + */ + public double getLatitudeRad() { + return this.lat; + } + + /** + * Returns latitude in degrees + */ + public double getLatitudeDeg() { + return Math.toDegrees(this.lat); + } + + + + @Override + public String toString() { + return "WorldCoordinate[lat=" + getLatitudeDeg() + ", lon=" + getLongitudeDeg() + ", alt=" + getAltitude() + "]"; + } + + + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof WorldCoordinate)) { + return false; + } + WorldCoordinate other = (WorldCoordinate) obj; + return (MathUtil.equals(this.lat, other.lat) && + MathUtil.equals(this.lon, other.lon) && MathUtil.equals(this.alt, other.alt)); + } + + @Override + public int hashCode() { + return ((int) (1000 * (lat + lon + alt))); + } + +} diff --git a/core/src/net/sf/openrocket/utils/GraphicalMotorSelector.java b/core/src/net/sf/openrocket/utils/GraphicalMotorSelector.java new file mode 100644 index 00000000..1087c5df --- /dev/null +++ b/core/src/net/sf/openrocket/utils/GraphicalMotorSelector.java @@ -0,0 +1,145 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.util.Pair; + +public class GraphicalMotorSelector { + + public static void main(String[] args) throws IOException { + + if (args.length == 0) { + System.err.println("MotorPlot <files>"); + System.exit(1); + } + + // Load files + Map<String, List<Pair<String, ThrustCurveMotor>>> map = + new LinkedHashMap<String, List<Pair<String, ThrustCurveMotor>>>(); + + GeneralMotorLoader loader = new GeneralMotorLoader(); + for (String file : args) { + + for (Motor motor : loader.load(new FileInputStream(file), file)) { + ThrustCurveMotor m = (ThrustCurveMotor) motor; + System.out.println("Loaded " + m + " from file " + file); + + Pair<String, ThrustCurveMotor> pair = new Pair<String, ThrustCurveMotor>(file, m); + String key = m.getManufacturer() + ":" + m.getDesignation(); + + List<Pair<String, ThrustCurveMotor>> list = map.get(key); + if (list == null) { + list = new ArrayList<Pair<String, ThrustCurveMotor>>(); + map.put(key, list); + } + + list.add(pair); + } + } + + + // Go through different motors + int count = 0; + for (String key : map.keySet()) { + count++; + List<Pair<String, ThrustCurveMotor>> list = map.get(key); + + + // Select best one of identical motors + List<String> filenames = new ArrayList<String>(); + List<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); + for (Pair<String, ThrustCurveMotor> pair : list) { + String file = pair.getU(); + ThrustCurveMotor m = pair.getV(); + + int index = indexOf(motors, m); + if (index >= 0) { + // Replace previous if this has more delays, a known type or longer comment + ThrustCurveMotor m2 = motors.get(index); + if (m.getStandardDelays().length > m2.getStandardDelays().length || + (m2.getMotorType() == Motor.Type.UNKNOWN && + m.getMotorType() != Motor.Type.UNKNOWN) || + (m.getDescription().trim().length() > + m2.getDescription().trim().length())) { + + filenames.set(index, file); + motors.set(index, m); + + } + } else { + filenames.add(file); + motors.add(m); + } + } + + if (filenames.size() == 0) { + + System.out.println("ERROR selecting from " + list); + System.exit(1); + + } else if (filenames.size() == 1) { + + select(filenames.get(0), list, false); + + } else { + + System.out.println("Choosing from " + filenames + + " (" + count + "/" + map.size() + ")"); + MotorPlot plot = new MotorPlot(filenames, motors); + plot.setVisible(true); + plot.dispose(); + int n = plot.getSelected(); + if (n < 0) { + System.out.println("NONE SELECTED from " + filenames); + } else { + select(filenames.get(n), list, true); + } + + } + + } + + } + + private static void select(String selected, List<Pair<String, ThrustCurveMotor>> list, boolean manual) { + System.out.print("SELECT " + selected + " "); + if (manual) { + System.out.println("(manual)"); + } else if (list.size() == 1) { + System.out.println("(only)"); + } else { + System.out.println("(identical)"); + } + + for (Pair<String, ThrustCurveMotor> pair : list) { + String file = pair.getU(); + if (!file.equals(selected)) { + System.out.println("IGNORE " + file); + } + } + } + + + private static int indexOf(List<ThrustCurveMotor> motors, ThrustCurveMotor motor) { + for (int i = 0; i < motors.size(); i++) { + ThrustCurveMotor m = motors.get(i); + // TODO: Similar? + if (m.equals(motor)) { + if (m.getStandardDelays().length == 0 || motor.getStandardDelays().length == 0 || + Arrays.equals(m.getStandardDelays(), motor.getStandardDelays())) { + return i; + } + } + } + return -1; + } +} diff --git a/core/src/net/sf/openrocket/utils/LogSpeedTest.java b/core/src/net/sf/openrocket/utils/LogSpeedTest.java new file mode 100644 index 00000000..4ca2f9a4 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/LogSpeedTest.java @@ -0,0 +1,35 @@ +package net.sf.openrocket.utils; + +import net.sf.openrocket.logging.DelegatorLogger; +import net.sf.openrocket.logging.LogHelper; +import net.sf.openrocket.logging.LogLevel; + +public class LogSpeedTest { + private static LogHelper log; + + private static final int COUNT = 1000000; + + public static void main(String[] args) { + + System.setProperty("openrocket.log.tracelevel", "user"); + log = new DelegatorLogger(); + + for (LogLevel l : LogLevel.values()) { + for (int i = 0; i < 3; i++) { + long t0 = System.currentTimeMillis(); + test(l); + long t1 = System.currentTimeMillis(); + System.out.println("Level " + l + ": " + (t1 - t0) + " ms for " + COUNT + " lines"); + } + } + + } + + + private static void test(LogLevel level) { + for (int i = 0; i < COUNT; i++) { + log.log(level, "Message " + i); + } + } + +} diff --git a/core/src/net/sf/openrocket/utils/MotorCheck.java b/core/src/net/sf/openrocket/utils/MotorCheck.java new file mode 100644 index 00000000..5710486d --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorCheck.java @@ -0,0 +1,89 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.file.motor.MotorLoader; +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; + +public class MotorCheck { + + // Warn if less that this many points + public static final int WARN_POINTS = 6; + + + public static void main(String[] args) { + MotorLoader loader = new GeneralMotorLoader(); + + // Load files + for (String file : args) { + System.out.print("Checking " + file + "... "); + System.out.flush(); + + boolean ok = true; + + List<Motor> motors = null; + try { + InputStream stream = new FileInputStream(file); + motors = loader.load(stream, file); + stream.close(); + } catch (IOException e) { + System.out.println("ERROR: " + e.getMessage()); + e.printStackTrace(System.out); + ok = false; + } + + String base = file.split("_")[0]; + Manufacturer mfg = Manufacturer.getManufacturer(base); + + if (motors != null) { + if (motors.size() == 0) { + System.out.println("ERROR: File contained no motors"); + ok = false; + } else { + for (Motor motor : motors) { + ThrustCurveMotor m = (ThrustCurveMotor) motor; + double sum = 0; + sum += m.getAverageThrustEstimate(); + sum += m.getBurnTimeEstimate(); + sum += m.getTotalImpulseEstimate(); + // sum += m.getTotalTime(); + sum += m.getDiameter(); + sum += m.getLength(); + sum += m.getEmptyCG().weight; + sum += m.getEmptyCG().x; + sum += m.getLaunchCG().weight; + sum += m.getLaunchCG().x; + sum += m.getMaxThrustEstimate(); + if (Double.isInfinite(sum) || Double.isNaN(sum)) { + System.out.println("ERROR: Invalid motor values"); + ok = false; + } + + if (m.getManufacturer() != mfg) { + System.out.println("ERROR: Inconsistent manufacturer " + + m.getManufacturer() + " (file name indicates " + mfg + + ")"); + ok = false; + } + + int points = ((ThrustCurveMotor) m).getTimePoints().length; + if (points < WARN_POINTS) { + System.out.println("WARNING: Only " + points + " data points"); + ok = false; + } + } + } + } + + if (ok) { + System.out.println("OK"); + } + } + } +} diff --git a/core/src/net/sf/openrocket/utils/MotorCompare.java b/core/src/net/sf/openrocket/utils/MotorCompare.java new file mode 100644 index 00000000..dcb16aeb --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorCompare.java @@ -0,0 +1,336 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.file.motor.MotorLoader; +import net.sf.openrocket.motor.Manufacturer; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; + +public class MotorCompare { + + /** Maximum allowed difference in maximum thrust */ + private static final double MAX_THRUST_MARGIN = 0.30; + /** Maximum allowed difference in total impulse */ + private static final double TOTAL_IMPULSE_MARGIN = 0.20; + /** Maximum allowed difference in mass values */ + private static final double MASS_MARGIN = 0.20; + + /** Number of time points in thrust curve to compare */ + private static final int DIVISIONS = 100; + /** Maximum difference in thrust for a time point to be considered invalid */ + private static final double THRUST_MARGIN = 0.20; + /** Number of invalid time points allowed */ + private static final int ALLOWED_INVALID_POINTS = 20; + + /** Minimum number of thrust curve points allowed (incl. start and end points) */ + private static final int MIN_POINTS = 7; + + + public static void main(String[] args) throws IOException { + final double maxThrust; + final double maxTime; + int maxDelays; + int maxPoints; + int maxCommentLen; + + double min, max; + double diff; + + int[] goodness; + + boolean bad = false; + List<String> cause = new ArrayList<String>(); + + MotorLoader loader = new GeneralMotorLoader(); + List<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); + List<String> files = new ArrayList<String>(); + + // Load files + System.out.printf("Files :"); + for (String file : args) { + System.out.printf("\t%s", file); + List<Motor> m = null; + try { + InputStream stream = new FileInputStream(file); + m = loader.load(stream, file); + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + System.out.print("(ERR:" + e.getMessage() + ")"); + } + if (m != null) { + motors.addAll((List) m); + for (int i = 0; i < m.size(); i++) + files.add(file); + } + } + System.out.println(); + + compare(motors, files); + } + + + + + + public static void compare(List<ThrustCurveMotor> motors, List<String> files) { + final double maxThrust; + final double maxTime; + int maxDelays; + int maxPoints; + int maxCommentLen; + + double min, max; + double diff; + + int[] goodness; + + boolean bad = false; + List<String> cause = new ArrayList<String>(); + + + if (motors.size() == 0) { + System.err.println("No motors loaded."); + System.out.println("ERROR: No motors loaded.\n"); + return; + + } + + if (motors.size() == 1) { + System.out.println("Best (ONLY): " + files.get(0)); + System.out.println(); + return; + } + + final int n = motors.size(); + goodness = new int[n]; + + + for (String s : files) { + System.out.print("\t" + s); + } + System.out.println(); + + + // Designations + System.out.printf("Designation:"); + String des = motors.get(0).getDesignation(); + for (Motor m : motors) { + System.out.printf("\t%s", m.getDesignation()); + if (!m.getDesignation().equals(des)) { + cause.add("Designation"); + bad = true; + } + } + System.out.println(); + + // Manufacturers + System.out.printf("Manufacture:"); + Manufacturer mfg = motors.get(0).getManufacturer(); + for (ThrustCurveMotor m : motors) { + System.out.printf("\t%s", m.getManufacturer()); + if (m.getManufacturer() != mfg) { + cause.add("Manufacturer"); + bad = true; + } + } + System.out.println(); + + + // Max. thrust + max = 0; + min = Double.MAX_VALUE; + System.out.printf("Max.thrust :"); + for (Motor m : motors) { + double f = m.getMaxThrustEstimate(); + System.out.printf("\t%.2f", f); + max = Math.max(max, f); + min = Math.min(min, f); + } + diff = (max - min) / min; + if (diff > MAX_THRUST_MARGIN) { + bad = true; + cause.add("Max thrust"); + } + System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); + maxThrust = (min + max) / 2; + + + // Total time + max = 0; + min = Double.MAX_VALUE; + System.out.printf("Burn time :"); + for (Motor m : motors) { + double t = m.getBurnTimeEstimate(); + System.out.printf("\t%.2f", t); + max = Math.max(max, t); + min = Math.min(min, t); + } + diff = (max - min) / min; + System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); + maxTime = max; + + + // Total impulse + max = 0; + min = Double.MAX_VALUE; + System.out.printf("Impulse :"); + for (Motor m : motors) { + double f = m.getTotalImpulseEstimate(); + System.out.printf("\t%.2f", f); + max = Math.max(max, f); + min = Math.min(min, f); + } + diff = (max - min) / min; + if (diff > TOTAL_IMPULSE_MARGIN) { + bad = true; + cause.add("Total impulse"); + } + System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); + + + // Initial mass + max = 0; + min = Double.MAX_VALUE; + System.out.printf("Init mass :"); + for (Motor m : motors) { + double f = m.getLaunchCG().weight; + System.out.printf("\t%.2f", f * 1000); + max = Math.max(max, f); + min = Math.min(min, f); + } + diff = (max - min) / min; + if (diff > MASS_MARGIN) { + bad = true; + cause.add("Initial mass"); + } + System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); + + + // Empty mass + max = 0; + min = Double.MAX_VALUE; + System.out.printf("Empty mass :"); + for (Motor m : motors) { + double f = m.getEmptyCG().weight; + System.out.printf("\t%.2f", f * 1000); + max = Math.max(max, f); + min = Math.min(min, f); + } + diff = (max - min) / min; + if (diff > MASS_MARGIN) { + bad = true; + cause.add("Empty mass"); + } + System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); + + + // Delays + maxDelays = 0; + System.out.printf("Delays :"); + for (ThrustCurveMotor m : motors) { + System.out.printf("\t%d", m.getStandardDelays().length); + maxDelays = Math.max(maxDelays, m.getStandardDelays().length); + } + System.out.println(); + + + // Data points + maxPoints = 0; + System.out.printf("Points :"); + for (Motor m : motors) { + System.out.printf("\t%d", ((ThrustCurveMotor) m).getTimePoints().length); + maxPoints = Math.max(maxPoints, ((ThrustCurveMotor) m).getTimePoints().length); + } + System.out.println(); + + + // Comment length + maxCommentLen = 0; + System.out.printf("Comment len:"); + for (Motor m : motors) { + System.out.printf("\t%d", m.getDescription().length()); + maxCommentLen = Math.max(maxCommentLen, m.getDescription().length()); + } + System.out.println(); + + + if (bad) { + String str = "ERROR: "; + for (int i = 0; i < cause.size(); i++) { + str += cause.get(i); + if (i < cause.size() - 1) + str += ", "; + } + str += " differs"; + System.out.println(str); + System.out.println(); + return; + } + + // Check consistency + // TODO: Does not check consistency + // int invalidPoints = 0; + // for (int i = 0; i < DIVISIONS; i++) { + // double t = maxTime * i / (DIVISIONS - 1); + // min = Double.MAX_VALUE; + // max = 0; + // // System.out.printf("%.2f:", t); + // for (Motor m : motors) { + // double f = m.getThrust(t); + // // System.out.printf("\t%.2f", f); + // min = Math.min(min, f); + // max = Math.max(max, f); + // } + // diff = (max - min) / maxThrust; + // // System.out.printf("\t(diff %.1f%%)\n", diff*100); + // if (diff > THRUST_MARGIN) + // invalidPoints++; + // } + // + // if (invalidPoints > ALLOWED_INVALID_POINTS) { + // System.out.println("ERROR: " + invalidPoints + "/" + DIVISIONS + // + " points have thrust differing over " + (THRUST_MARGIN * 100) + "%"); + // System.out.println(); + // return; + // } + + + // Check goodness + for (int i = 0; i < n; i++) { + ThrustCurveMotor m = motors.get(i); + if (m.getStandardDelays().length == maxDelays) + goodness[i] += 1000; + if (((ThrustCurveMotor) m).getTimePoints().length == maxPoints) + goodness[i] += 100; + if (m.getDescription().length() == maxCommentLen) + goodness[i] += 10; + if (files.get(i).matches(".*\\.[rR][sS][eE]$")) + goodness[i] += 1; + } + int best = 0; + for (int i = 1; i < n; i++) { + if (goodness[i] > goodness[best]) + best = i; + } + + + // Verify enough points + int pts = ((ThrustCurveMotor) motors.get(best)).getTimePoints().length; + if (pts < MIN_POINTS) { + System.out.println("WARNING: Best has only " + pts + " data points"); + } + + System.out.println("Best (" + goodness[best] + "): " + files.get(best)); + System.out.println(); + + + } + +} diff --git a/core/src/net/sf/openrocket/utils/MotorCompareAll.java b/core/src/net/sf/openrocket/utils/MotorCompareAll.java new file mode 100644 index 00000000..f3a6f8a7 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorCompareAll.java @@ -0,0 +1,59 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.file.motor.MotorLoader; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.util.Pair; + +public class MotorCompareAll { + + /* + * Usage: + * + * java MotorCompareAll *.eng *.rse + */ + public static void main(String[] args) throws IOException { + + Map<String, Pair<List<ThrustCurveMotor>, List<String>>> map = + new HashMap<String, Pair<List<ThrustCurveMotor>, List<String>>>(); + + MotorLoader loader = new GeneralMotorLoader(); + + for (String filename : args) { + + List<ThrustCurveMotor> motors = (List) loader.load(new FileInputStream(filename), filename); + + for (ThrustCurveMotor m : motors) { + String key = m.getManufacturer() + ":" + m.getDesignation(); + Pair<List<ThrustCurveMotor>, List<String>> pair = map.get(key); + if (pair == null) { + pair = new Pair<List<ThrustCurveMotor>, List<String>> + (new ArrayList<ThrustCurveMotor>(), new ArrayList<String>()); + map.put(key, pair); + } + pair.getU().add(m); + pair.getV().add(filename); + } + } + + Collator collator = Collator.getInstance(); + + List<String> keys = new ArrayList<String>(map.keySet()); + Collections.sort(keys, collator); + for (String basename : keys) { + Pair<List<ThrustCurveMotor>, List<String>> pair = map.get(basename); + System.err.println(basename + ": " + pair.getV()); + MotorCompare.compare(pair.getU(), pair.getV()); + } + } + +} diff --git a/core/src/net/sf/openrocket/utils/MotorCorrelation.java b/core/src/net/sf/openrocket/utils/MotorCorrelation.java new file mode 100644 index 00000000..3394a59e --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorCorrelation.java @@ -0,0 +1,145 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.file.motor.MotorLoader; +import net.sf.openrocket.logging.LogLevel; +import net.sf.openrocket.models.atmosphere.AtmosphericConditions; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.MotorInstance; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.MathUtil; + +public class MotorCorrelation { + + /** + * Return a measure of motor similarity. The measure is a value between 0.0 and 1.0. + * The larger the value, the more similar the motor thrust curves are, for value 1.0 they + * are identical. + * <p> + * This method takes into account the thrust curve shape, average thrust, burn time and + * total impulse of the motor. The similarity is the minimum of all of these. + * + * @param motor1 the first motor + * @param motor2 the second motor + * @return the similarity of the two motors + */ + public static double similarity(Motor motor1, Motor motor2) { + double d; + + d = crossCorrelation(motor1, motor2); + d = Math.min(d, diff(motor1.getAverageThrustEstimate(), motor2.getAverageThrustEstimate())); + d = Math.min(d, 2 * diff(motor1.getBurnTimeEstimate(), motor2.getBurnTimeEstimate())); + d = Math.min(d, diff(motor1.getTotalImpulseEstimate(), motor2.getTotalImpulseEstimate())); + + return d; + } + + + private static double diff(double a, double b) { + double min = Math.min(a, b); + double max = Math.max(a, b); + + if (MathUtil.equals(max, 0)) + return 1.0; + return min / max; + } + + + /** + * Compute the cross-correlation of the thrust curves of the two motors. The result is + * a double between 0 and 1 (inclusive). The closer the return value is to one the more + * similar the thrust curves are. + * + * @param motor1 the first motor. + * @param motor2 the second motor. + * @return the scaled cross-correlation of the two thrust curves. + */ + public static double crossCorrelation(Motor motor1, Motor motor2) { + MotorInstance m1 = motor1.getInstance(); + MotorInstance m2 = motor2.getInstance(); + + AtmosphericConditions cond = new AtmosphericConditions(); + + double t; + double auto1 = 0; + double auto2 = 0; + double cross = 0; + for (t = 0; t < 1000; t += 0.01) { + m1.step(t, 0, cond); + m2.step(t, 0, cond); + + double t1 = m1.getThrust(); + double t2 = m2.getThrust(); + + if (t1 < 0 || t2 < 0) { + throw new BugException("Negative thrust, t1=" + t1 + " t2=" + t2); + } + + auto1 += t1 * t1; + auto2 += t2 * t2; + cross += t1 * t2; + } + + double auto = Math.max(auto1, auto2); + + if (MathUtil.equals(auto, 0)) { + return 1.0; + } + + return cross / auto; + } + + + + + public static void main(String[] args) { + Application.setLogOutputLevel(LogLevel.WARN); + + MotorLoader loader = new GeneralMotorLoader(); + List<Motor> motors = new ArrayList<Motor>(); + List<String> files = new ArrayList<String>(); + + // Load files + for (String file : args) { + List<Motor> m = null; + try { + InputStream stream = new FileInputStream(file); + m = loader.load(stream, file); + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + System.exit(1); + } + if (m != null) { + motors.addAll(m); + for (int i = 0; i < m.size(); i++) + files.add(file); + } + } + + // Output motor digests + final int count = motors.size(); + for (int i = 0; i < count; i++) { + System.out.println(files.get(i) + ": " + MotorDigest.digestMotor((ThrustCurveMotor) motors.get(i))); + } + + // Cross-correlate every pair + for (int i = 0; i < count; i++) { + for (int j = i + 1; j < count; j++) { + System.out.println(files.get(i) + " " + files.get(j) + " : " + + crossCorrelation(motors.get(i), motors.get(j))); + } + } + + } + +} \ No newline at end of file diff --git a/core/src/net/sf/openrocket/utils/MotorDigester.java b/core/src/net/sf/openrocket/utils/MotorDigester.java new file mode 100644 index 00000000..754d4075 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorDigester.java @@ -0,0 +1,60 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.file.motor.MotorLoader; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.ThrustCurveMotor; + +public class MotorDigester { + + public static void main(String[] args) { + final MotorLoader loader = new GeneralMotorLoader(); + final boolean printFileNames; + + if (args.length == 0) { + System.err.println("Usage: MotorDigester <files>"); + printFileNames = false; + System.exit(1); + } else if (args.length == 1) { + printFileNames = false; + } else { + printFileNames = true; + } + + + for (String file : args) { + + List<Motor> motors = null; + try { + InputStream stream = new FileInputStream(file); + motors = loader.load(stream, file); + stream.close(); + } catch (IOException e) { + System.err.println("ERROR: " + e.getMessage()); + e.printStackTrace(); + continue; + } + + for (Motor m : motors) { + if (!(m instanceof ThrustCurveMotor)) { + System.err.println(file + ": Not ThrustCurveMotor: " + m); + continue; + } + + String digest = MotorDigest.digestMotor((ThrustCurveMotor) m); + if (printFileNames) { + System.out.print(file + ": "); + } + System.out.println(digest); + } + } + + } + +} diff --git a/core/src/net/sf/openrocket/utils/MotorPlot.java b/core/src/net/sf/openrocket/utils/MotorPlot.java new file mode 100644 index 00000000..f3b74310 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorPlot.java @@ -0,0 +1,178 @@ +package net.sf.openrocket.utils; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFrame; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.JTextArea; +import javax.swing.SwingUtilities; + +import net.miginfocom.swing.MigLayout; +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.gui.util.GUIUtil; +import net.sf.openrocket.l10n.Translator; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.ThrustCurveMotor; +import net.sf.openrocket.startup.Application; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartPanel; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; + +public class MotorPlot extends JDialog { + + private int selected = -1; + private static final Translator trans = Application.getTranslator(); + + public MotorPlot(List<String> filenames, List<ThrustCurveMotor> motors) { + //// Motor plot + super((JFrame) null, trans.get("MotorPlot.title.Motorplot"), true); + + JTabbedPane tabs = new JTabbedPane(); + for (int i = 0; i < filenames.size(); i++) { + JPanel pane = createPlotPanel((ThrustCurveMotor) motors.get(i)); + + //// Select button + JButton button = new JButton(trans.get("MotorPlot.but.Select")); + final int number = i; + button.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + selected = number; + MotorPlot.this.setVisible(false); + } + }); + pane.add(button, "wrap", 0); + + tabs.addTab(filenames.get(i), pane); + } + + this.add(tabs); + + this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + this.setLocationByPlatform(true); + this.validate(); + this.pack(); + } + + + private JPanel createPlotPanel(ThrustCurveMotor motor) { + JPanel panel = new JPanel(new MigLayout()); + + + XYSeries series = new XYSeries("", false, true); + double[] time = motor.getTimePoints(); + double[] thrust = motor.getThrustPoints(); + + for (int i = 0; i < time.length; i++) { + series.add(time[i], thrust[i]); + } + + // Create the chart using the factory to get all default settings + JFreeChart chart = ChartFactory.createXYLineChart( + //// Motor thrust curve + trans.get("MotorPlot.Chart.Motorthrustcurve"), + //// Time / s + trans.get("MotorPlot.Chart.Time"), + //// Thrust / N + trans.get("MotorPlot.Chart.Thrust"), + new XYSeriesCollection(series), + PlotOrientation.VERTICAL, + true, + true, + false + ); + + ((XYLineAndShapeRenderer) chart.getXYPlot().getRenderer()).setShapesVisible(true); + + ChartPanel chartPanel = new ChartPanel(chart, + false, // properties + true, // save + false, // print + true, // zoom + true); // tooltips + chartPanel.setMouseWheelEnabled(true); + chartPanel.setEnforceFileExtensions(true); + chartPanel.setInitialDelay(500); + + chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); + + panel.add(chartPanel, "grow, wrap para"); + + + JTextArea area = new JTextArea(5, 40); + StringBuilder sb = new StringBuilder(); + //// Designation: + sb.append("MotorPlot.txt.Designation" + " ").append(motor.getDesignation()).append(" "); + //// Manufacturer: + sb.append("MotorPlot.txt.Manufacturer" + " ").append(motor.getManufacturer()).append(" "); + //// Type: + sb.append("MotorPlot.txt.Type" + " ").append(motor.getMotorType()).append('\n'); + //// Delays: + sb.append("MotorPlot.txt.Delays" +" ").append(Arrays.toString(motor.getStandardDelays())).append('\n'); + //// Comment:\n + sb.append("MotorPlot.txt.Comment" + " ").append(motor.getDescription()); + area.setText(sb.toString()); + panel.add(area, "grow, wrap"); + + + return panel; + } + + + + public int getSelected() { + return selected; + } + + + public static void main(String[] args) throws IOException { + if (args.length == 0) { + System.err.println("MotorPlot <files>"); + System.exit(1); + } + + final List<String> filenames = new ArrayList<String>(); + final List<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); + + GeneralMotorLoader loader = new GeneralMotorLoader(); + for (String file : args) { + for (Motor m : loader.load(new FileInputStream(file), file)) { + filenames.add(file); + motors.add((ThrustCurveMotor) m); + } + } + + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + GUIUtil.setBestLAF(); + + MotorPlot plot = new MotorPlot(filenames, motors); + plot.setVisible(true); + } + + }); + + } + + + + +} diff --git a/core/src/net/sf/openrocket/utils/MotorPrinter.java b/core/src/net/sf/openrocket/utils/MotorPrinter.java new file mode 100644 index 00000000..0ca4e987 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/MotorPrinter.java @@ -0,0 +1,60 @@ +package net.sf.openrocket.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; + +import net.sf.openrocket.file.motor.GeneralMotorLoader; +import net.sf.openrocket.file.motor.MotorLoader; +import net.sf.openrocket.motor.Motor; +import net.sf.openrocket.motor.MotorDigest; +import net.sf.openrocket.motor.ThrustCurveMotor; + +public class MotorPrinter { + + public static void main(String[] args) throws IOException { + + MotorLoader loader = new GeneralMotorLoader(); + + System.out.println(); + for (String arg : args) { + InputStream stream = new FileInputStream(arg); + + List<Motor> motors = loader.load(stream, arg); + + System.out.println("*** " + arg + " ***"); + System.out.println(); + for (Motor motor : motors) { + ThrustCurveMotor m = (ThrustCurveMotor) motor; + System.out.println(" Manufacturer: " + m.getManufacturer()); + System.out.println(" Designation: " + m.getDesignation()); + System.out.println(" Delays: " + + Arrays.toString(m.getStandardDelays())); + System.out.printf(" Nominal time: %.2f s\n", m.getBurnTimeEstimate()); + // System.out.printf(" Total time: %.2f s\n", m.getTotalTime()); + System.out.printf(" Avg. thrust: %.2f N\n", m.getAverageThrustEstimate()); + System.out.printf(" Max. thrust: %.2f N\n", m.getMaxThrustEstimate()); + System.out.printf(" Total impulse: %.2f Ns\n", m.getTotalImpulseEstimate()); + System.out.println(" Diameter: " + m.getDiameter() * 1000 + " mm"); + System.out.println(" Length: " + m.getLength() * 1000 + " mm"); + System.out.println(" Digest: " + MotorDigest.digestMotor(m)); + + if (m instanceof ThrustCurveMotor) { + ThrustCurveMotor tc = (ThrustCurveMotor) m; + System.out.println(" Data points: " + tc.getTimePoints().length); + for (int i = 0; i < m.getTimePoints().length; i++) { + double time = m.getTimePoints()[i]; + double thrust = m.getThrustPoints()[i]; + System.out.printf(" t=%.3f F=%.3f\n", time, thrust); + } + } + + System.out.println(" Comment:"); + System.out.println(m.getDescription()); + System.out.println(); + } + } + } +} diff --git a/core/src/net/sf/openrocket/utils/RocksimConverter.java b/core/src/net/sf/openrocket/utils/RocksimConverter.java new file mode 100644 index 00000000..91530a30 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/RocksimConverter.java @@ -0,0 +1,85 @@ +package net.sf.openrocket.utils; + +import net.sf.openrocket.document.OpenRocketDocument; +import net.sf.openrocket.document.StorageOptions; +import net.sf.openrocket.file.RocketLoadException; +import net.sf.openrocket.file.RocketLoader; +import net.sf.openrocket.file.RocketSaver; +import net.sf.openrocket.file.openrocket.OpenRocketSaver; +import net.sf.openrocket.gui.util.SwingPreferences; +import net.sf.openrocket.l10n.ResourceBundleTranslator; +import net.sf.openrocket.logging.LogLevel; +import net.sf.openrocket.startup.Application; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; + +/** + * Utility that loads Rocksim file formats and saves them in ORK format. + * File is saved with the .rkt extension replaced with .ork. + * + * Usage: + * java -cp OpenRocket.jar net.sf.openrocket.utils.RocksimConverter <files...> + * + * @author Sampo Niskanen <sampo.niskanen@iki.fi> + */ +public class RocksimConverter { + + + public static void main(String[] args) { + + setup(); + + RocketLoader loader = new net.sf.openrocket.file.rocksim.importt.RocksimLoader(); + RocketSaver saver = new OpenRocketSaver(); + + for (String inputFile : args) { + System.out.println("Converting " + inputFile + "..."); + + if (!inputFile.matches(".*\\.[rR][kK][tT]$")) { + System.err.println("ERROR: File '" + inputFile + "' does not end in .rkt, skipping."); + continue; + } + + String outputFile = inputFile.replaceAll("\\.[rR][kK][tT]$", ".ork"); + + File input = new File(inputFile); + File output = new File(outputFile); + + if (!input.isFile()) { + System.err.println("ERROR: File '" + inputFile + "' does not exist, skipping."); + continue; + } + if (output.exists()) { + System.err.println("ERROR: File '" + outputFile + "' already exists, skipping."); + continue; + } + + try { + StorageOptions opts = new StorageOptions(); + opts.setCompressionEnabled(true); + opts.setSimulationTimeSkip(StorageOptions.SIMULATION_DATA_NONE); + opts.setExplicitlySet(true); + + OpenRocketDocument document = loader.load(input); + saver.save(output, document, opts); + + } catch (RocketLoadException e) { + System.err.println("ERROR: Error loading '" + inputFile + "': " + e.getMessage()); + } catch (IOException e) { + System.err.println("ERROR: Error saving '" + outputFile + "': " + e.getMessage()); + } + + } + + } + + private static void setup() { + Locale.setDefault(Locale.US); + Application.setBaseTranslator(new ResourceBundleTranslator("l10n.messages")); + + Application.setLogOutputLevel(LogLevel.WARN); + Application.setPreferences(new SwingPreferences()); + } +} diff --git a/core/src/net/sf/openrocket/utils/TestFunctionOptimizer.java b/core/src/net/sf/openrocket/utils/TestFunctionOptimizer.java new file mode 100644 index 00000000..e5f9da71 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/TestFunctionOptimizer.java @@ -0,0 +1,119 @@ +package net.sf.openrocket.utils; + +import net.sf.openrocket.optimization.general.Function; +import net.sf.openrocket.optimization.general.FunctionOptimizer; +import net.sf.openrocket.optimization.general.OptimizationController; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.ParallelExecutorCache; +import net.sf.openrocket.optimization.general.ParallelFunctionCache; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer; + + + + +public class TestFunctionOptimizer { + + private static final int LOOP_COUNT = 1000000; + + private volatile int evaluations = 0; + private volatile int aborted = 0; + private volatile int stepCount = 0; + + + + private void go(final ParallelFunctionCache functionCache, + final FunctionOptimizer optimizer, final Point optimum, final int maxSteps) throws OptimizationException { + + Function function = new Function() { + @Override + public double evaluate(Point p) throws InterruptedException { + if (loop(LOOP_COUNT)) { + evaluations++; + return p.sub(optimum).length2(); + } else { + aborted++; + return Double.NaN; + } + } + }; + + OptimizationController control = new OptimizationController() { + + @Override + public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { + stepCount++; + // System.out.println("CSV " + count + ", " + evaluations + ", " + newPoint.sub(optimum).length()); + // System.out.println("Steps: " + count + " Function evaluations: " + evaluations); + // System.out.println("Distance: " + newPoint.sub(optimum).length() + " " + newPoint + " value=" + newValue); + return stepCount < maxSteps; + } + }; + ; + + functionCache.setFunction(function); + optimizer.setFunctionCache(functionCache); + optimizer.optimize(new Point(optimum.dim(), 0.5), control); + System.err.println("Result: " + optimizer.getOptimumPoint() + " value=" + optimizer.getOptimumValue()); + System.err.println("Steps: " + stepCount + " Evaluations: " + evaluations); + } + + + public static double counter; + + private static boolean loop(int count) { + counter = 1.0; + for (int i = 0; i < count; i++) { + counter += Math.sin(counter); + if (i % 1024 == 0) { + if (Thread.interrupted()) { + return false; + } + } + } + return true; + } + + + public static void main(String[] args) throws InterruptedException, OptimizationException { + + System.err.println("Number of processors: " + Runtime.getRuntime().availableProcessors()); + + for (int i = 0; i < 20; i++) { + long t0 = System.currentTimeMillis(); + loop(LOOP_COUNT); + long t1 = System.currentTimeMillis(); + System.err.println("Loop delay at startup: " + (t1 - t0) + "ms"); + } + System.err.println(); + + for (int threadCount = 1; threadCount <= 10; threadCount++) { + + System.err.println("THREAD COUNT: " + threadCount); + TestFunctionOptimizer test = new TestFunctionOptimizer(); + + ParallelExecutorCache executor = new ParallelExecutorCache(threadCount); + MultidirectionalSearchOptimizer optimizer = new MultidirectionalSearchOptimizer(); + long t0 = System.currentTimeMillis(); + test.go(executor, optimizer, new Point(0.2, 0.3, 0.85), 30); + long t1 = System.currentTimeMillis(); + + System.err.println("Optimization took " + (t1 - t0) + "ms"); + System.err.println("" + test.stepCount + " steps, " + test.evaluations + + " function evaluations, " + test.aborted + " aborted evaluations"); + System.err.println("Statistics: " + optimizer.getStatistics()); + + executor.getExecutor().shutdownNow(); + Thread.sleep(1000); + + t0 = System.currentTimeMillis(); + loop(LOOP_COUNT); + t1 = System.currentTimeMillis(); + System.err.println("Loop delay afterwards: " + (t1 - t0) + "ms"); + System.err.println(); + } + } + + + +} diff --git a/core/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java b/core/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java new file mode 100644 index 00000000..e09a0ee8 --- /dev/null +++ b/core/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java @@ -0,0 +1,101 @@ +package net.sf.openrocket.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import net.sf.openrocket.optimization.general.Function; +import net.sf.openrocket.optimization.general.FunctionOptimizer; +import net.sf.openrocket.optimization.general.OptimizationController; +import net.sf.openrocket.optimization.general.OptimizationException; +import net.sf.openrocket.optimization.general.ParallelExecutorCache; +import net.sf.openrocket.optimization.general.Point; +import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer; +import net.sf.openrocket.util.MathUtil; + + +public class TestFunctionOptimizerLoop { + + private static final double PRECISION = 0.01; + + private Point optimum; + private int stepCount = 0; + private int evaluations = 0; + + + + private void go(final FunctionOptimizer optimizer, final Point optimum, final int maxSteps, ExecutorService executor) throws OptimizationException { + + Function function = new Function() { + @Override + public double evaluate(Point p) throws InterruptedException { + evaluations++; + return p.sub(optimum).length2(); + } + }; + + OptimizationController control = new OptimizationController() { + + @Override + public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { + stepCount++; + if (stepCount % 1000 == 0) { + System.err.println("WARNING: Over " + stepCount + " steps required for optimum=" + optimum + + " position=" + newPoint); + } + double distance = newPoint.sub(optimum).length(); + return distance >= PRECISION; + } + }; + ; + + ParallelExecutorCache cache = new ParallelExecutorCache(executor); + cache.setFunction(function); + optimizer.setFunctionCache(cache); + optimizer.optimize(new Point(optimum.dim(), 0.5), control); + } + + + public static void main(String[] args) throws OptimizationException { + + System.err.println("PRECISION = " + PRECISION); + + ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100)); + + for (int dim = 1; dim <= 10; dim++) { + + List<Integer> stepCount = new ArrayList<Integer>(); + List<Integer> functionCount = new ArrayList<Integer>(); + + MultidirectionalSearchOptimizer optimizer = new MultidirectionalSearchOptimizer(); + for (int count = 0; count < 200; count++) { + TestFunctionOptimizerLoop test = new TestFunctionOptimizerLoop(); + double[] point = new double[dim]; + for (int i = 0; i < dim; i++) { + point[i] = Math.random(); + } + // point[0] = 0.7; + test.go(optimizer, new Point(point), 20, executor); + stepCount.add(test.stepCount); + functionCount.add(test.evaluations); + } + + // System.err.println("StepCount = " + stepCount); + + System.out.printf("dim=%d Steps avg=%5.2f dev=%5.2f median=%.1f " + + "Evaluations avg=%5.2f dev=%5.2f median=%.1f\n", + dim, MathUtil.average(stepCount), MathUtil.stddev(stepCount), MathUtil.median(stepCount), + MathUtil.average(functionCount), MathUtil.stddev(functionCount), MathUtil.median(functionCount)); + System.out.println("stat: " + optimizer.getStatistics()); + + } + + executor.shutdownNow(); + } + + + +} diff --git a/src/META-INF/services/net.sf.openrocket.optimization.services.OptimizableParameterService b/src/META-INF/services/net.sf.openrocket.optimization.services.OptimizableParameterService deleted file mode 100644 index 522a653f..00000000 --- a/src/META-INF/services/net.sf.openrocket.optimization.services.OptimizableParameterService +++ /dev/null @@ -1,2 +0,0 @@ -# Default service implementation: -net.sf.openrocket.optimization.services.DefaultOptimizableParameterService diff --git a/src/META-INF/services/net.sf.openrocket.optimization.services.SimulationModifierService b/src/META-INF/services/net.sf.openrocket.optimization.services.SimulationModifierService deleted file mode 100644 index 7b5cda9b..00000000 --- a/src/META-INF/services/net.sf.openrocket.optimization.services.SimulationModifierService +++ /dev/null @@ -1,2 +0,0 @@ -# Default service implementation: -net.sf.openrocket.optimization.services.DefaultSimulationModifierService diff --git a/src/net/sf/openrocket/aerodynamics/AbstractAerodynamicCalculator.java b/src/net/sf/openrocket/aerodynamics/AbstractAerodynamicCalculator.java deleted file mode 100644 index a92d0ab4..00000000 --- a/src/net/sf/openrocket/aerodynamics/AbstractAerodynamicCalculator.java +++ /dev/null @@ -1,116 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import java.util.Map; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; - - -/** - * An abstract aerodynamic calculator implementation, that offers basic implementation - * of some methods and methods for cache validation and purging. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public abstract class AbstractAerodynamicCalculator implements AerodynamicCalculator { - private static final LogHelper log = Application.getLogger(); - - /** Number of divisions used when calculating worst CP. */ - public static final int DIVISIONS = 360; - - /** - * A <code>WarningSet</code> that can be used if <code>null</code> is passed - * to a calculation method. - */ - protected WarningSet ignoreWarningSet = new WarningSet(); - - /** The aerodynamic modification ID of the latest rocket */ - private int rocketAeroModID = -1; - private int rocketTreeModID = -1; - - - - - //////////////// Aerodynamic calculators //////////////// - - @Override - public abstract Coordinate getCP(Configuration configuration, FlightConditions conditions, - WarningSet warnings); - - @Override - public abstract Map<RocketComponent, AerodynamicForces> getForceAnalysis(Configuration configuration, FlightConditions conditions, - WarningSet warnings); - - @Override - public abstract AerodynamicForces getAerodynamicForces(Configuration configuration, - FlightConditions conditions, WarningSet warnings); - - - - /* - * The worst theta angle is stored in conditions. - */ - @Override - public Coordinate getWorstCP(Configuration configuration, FlightConditions conditions, - WarningSet warnings) { - FlightConditions cond = conditions.clone(); - Coordinate worst = new Coordinate(Double.MAX_VALUE); - Coordinate cp; - double theta = 0; - - for (int i = 0; i < DIVISIONS; i++) { - cond.setTheta(2 * Math.PI * i / DIVISIONS); - cp = getCP(configuration, cond, warnings); - if (cp.x < worst.x) { - worst = cp; - theta = cond.getTheta(); - } - } - - conditions.setTheta(theta); - - return worst; - } - - - - /** - * Check the current cache consistency. This method must be called by all - * methods that may use any cached data before any other operations are - * performed. If the rocket has changed since the previous call to - * <code>checkCache()</code>, then {@link #voidAerodynamicCache()} is called. - * <p> - * This method performs the checking based on the rocket's modification IDs, - * so that these method may be called from listeners of the rocket itself. - * - * @param configuration the configuration of the current call - */ - protected final void checkCache(Configuration configuration) { - if (rocketAeroModID != configuration.getRocket().getAerodynamicModID() || - rocketTreeModID != configuration.getRocket().getTreeModID()) { - rocketAeroModID = configuration.getRocket().getAerodynamicModID(); - rocketTreeModID = configuration.getRocket().getTreeModID(); - log.debug("Voiding the aerodynamic cache"); - voidAerodynamicCache(); - } - } - - - - /** - * Void cached aerodynamic data. This method is called whenever a change occurs in - * the rocket structure that affects the aerodynamics of the rocket and when a new - * Rocket is set. This method must be overridden to void any cached data - * necessary. The method must call <code>super.voidAerodynamicCache()</code> during - * its execution. - */ - protected void voidAerodynamicCache() { - // No-op - } - - -} diff --git a/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java b/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java deleted file mode 100644 index 0ce3efc5..00000000 --- a/src/net/sf/openrocket/aerodynamics/AerodynamicCalculator.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import java.util.Map; - -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Monitorable; - -/** - * An interface for performing aerodynamic calculations on rockets. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface AerodynamicCalculator extends Monitorable { - - /** - * Calculate the CP of the specified configuration. - * - * @param configuration the rocket configuration - * @param conditions the flight conditions - * @param warnings the set in which to place warnings, or <code>null</code> - * @return the CP position in absolute coordinates - */ - public Coordinate getCP(Configuration configuration, FlightConditions conditions, WarningSet warnings); - - /** - * Calculate the aerodynamic forces acting upon the rocket. - * - * @param configuration the rocket configuration. - * @param conditions the flight conditions. - * @param warnings the set in which to place warnings, or <code>null</code>. - * @return the aerodynamic forces acting upon the rocket. - */ - public AerodynamicForces getAerodynamicForces(Configuration configuration, - FlightConditions conditions, WarningSet warnings); - - /** - * Calculate the aerodynamic forces acting upon the rocket with a component analysis. - * - * @param configuration the rocket configuration. - * @param conditions the flight conditions. - * @param warnings the set in which to place warnings, or <code>null</code>. - * @return a map from the rocket components to the aerodynamic force portions that component - * exerts. The map contains an value for the base rocket, which is the total - * aerodynamic forces. - */ - public Map<RocketComponent, AerodynamicForces> getForceAnalysis(Configuration configuration, - FlightConditions conditions, WarningSet warnings); - - /** - * Calculate the worst CP occurring for any lateral wind angle. The worst CP is returned and the theta angle - * that produces the worst CP is stored in the flight conditions. - * - * @param configuration the rocket configuration. - * @param conditions the flight conditions. - * @param warnings the set in which to place warnings, or <code>null</code>. - * @return the worst (foremost) CP position for any lateral wind angle. - */ - public Coordinate getWorstCP(Configuration configuration, FlightConditions conditions, - WarningSet warnings); - - /** - * Return a new instance of this aerodynamic calculator type. - * - * @return a new, independent instance of this aerodynamic calculator type - */ - public AerodynamicCalculator newInstance(); - -} diff --git a/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java b/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java deleted file mode 100644 index 2bb6d75b..00000000 --- a/src/net/sf/openrocket/aerodynamics/AerodynamicForces.java +++ /dev/null @@ -1,355 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Monitorable; - -public class AerodynamicForces implements Cloneable, Monitorable { - - /** - * The component this data is referring to. Used in analysis methods. - * A total value is indicated by the component being the Rocket component. - */ - private RocketComponent component = null; - - - /** CP and CNa. */ - private Coordinate cp = null; - - - /** - * Normal force coefficient derivative. At values close to zero angle of attack - * this value may be poorly defined or NaN if the calculation method does not - * compute CNa directly. - */ - private double CNa = Double.NaN; - - - /** Normal force coefficient. */ - private double CN = Double.NaN; - - /** Pitching moment coefficient, relative to the coordinate origin. */ - private double Cm = Double.NaN; - - /** Side force coefficient, Cy */ - private double Cside = Double.NaN; - - /** Yaw moment coefficient, Cn, relative to the coordinate origin. */ - private double Cyaw = Double.NaN; - - /** Roll moment coefficient, Cl, relative to the coordinate origin. */ - private double Croll = Double.NaN; - - /** Roll moment damping coefficient */ - private double CrollDamp = Double.NaN; - - /** Roll moment forcing coefficient */ - private double CrollForce = Double.NaN; - - - - /** Axial drag coefficient, CA */ - private double Caxial = Double.NaN; - - /** Total drag force coefficient, parallel to the airflow. */ - private double CD = Double.NaN; - - /** Drag coefficient due to fore pressure drag. */ - private double pressureCD = Double.NaN; - - /** Drag coefficient due to base drag. */ - private double baseCD = Double.NaN; - - /** Drag coefficient due to friction drag. */ - private double frictionCD = Double.NaN; - - - private double pitchDampingMoment = Double.NaN; - private double yawDampingMoment = Double.NaN; - - private int modID = 0; - - - public void setComponent(RocketComponent component) { - this.component = component; - modID++; - } - - public RocketComponent getComponent() { - return component; - } - - public void setCP(Coordinate cp) { - this.cp = cp; - modID++; - } - - public Coordinate getCP() { - return cp; - } - - public void setCNa(double cNa) { - CNa = cNa; - modID++; - } - - public double getCNa() { - return CNa; - } - - public void setCN(double cN) { - CN = cN; - modID++; - } - - public double getCN() { - return CN; - } - - public void setCm(double cm) { - Cm = cm; - modID++; - } - - public double getCm() { - return Cm; - } - - public void setCside(double cside) { - Cside = cside; - modID++; - } - - public double getCside() { - return Cside; - } - - public void setCyaw(double cyaw) { - Cyaw = cyaw; - modID++; - } - - public double getCyaw() { - return Cyaw; - } - - public void setCroll(double croll) { - Croll = croll; - modID++; - } - - public double getCroll() { - return Croll; - } - - public void setCrollDamp(double crollDamp) { - CrollDamp = crollDamp; - modID++; - } - - public double getCrollDamp() { - return CrollDamp; - } - - public void setCrollForce(double crollForce) { - CrollForce = crollForce; - modID++; - } - - public double getCrollForce() { - return CrollForce; - } - - public void setCaxial(double caxial) { - Caxial = caxial; - modID++; - } - - public double getCaxial() { - return Caxial; - } - - public void setCD(double cD) { - CD = cD; - modID++; - } - - public double getCD() { - return CD; - } - - public void setPressureCD(double pressureCD) { - this.pressureCD = pressureCD; - modID++; - } - - public double getPressureCD() { - return pressureCD; - } - - public void setBaseCD(double baseCD) { - this.baseCD = baseCD; - modID++; - } - - public double getBaseCD() { - return baseCD; - } - - public void setFrictionCD(double frictionCD) { - this.frictionCD = frictionCD; - modID++; - } - - public double getFrictionCD() { - return frictionCD; - } - - public void setPitchDampingMoment(double pitchDampingMoment) { - this.pitchDampingMoment = pitchDampingMoment; - modID++; - } - - public double getPitchDampingMoment() { - return pitchDampingMoment; - } - - public void setYawDampingMoment(double yawDampingMoment) { - this.yawDampingMoment = yawDampingMoment; - modID++; - } - - public double getYawDampingMoment() { - return yawDampingMoment; - } - - - - /** - * Reset all values to null/NaN. - */ - public void reset() { - setComponent(null); - - setCP(null); - setCNa(Double.NaN); - setCN(Double.NaN); - setCm(Double.NaN); - setCside(Double.NaN); - setCyaw(Double.NaN); - setCroll(Double.NaN); - setCrollDamp(Double.NaN); - setCrollForce(Double.NaN); - setCaxial(Double.NaN); - setCD(Double.NaN); - setPitchDampingMoment(Double.NaN); - setYawDampingMoment(Double.NaN); - } - - /** - * Zero all values to 0 / Coordinate.NUL. Component is left as it was. - */ - public void zero() { - // component untouched - - setCP(Coordinate.NUL); - setCNa(0); - setCN(0); - setCm(0); - setCside(0); - setCyaw(0); - setCroll(0); - setCrollDamp(0); - setCrollForce(0); - setCaxial(0); - setCD(0); - setPitchDampingMoment(0); - setYawDampingMoment(0); - } - - - @Override - public AerodynamicForces clone() { - try { - return (AerodynamicForces)super.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException?!?"); - } - } - - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof AerodynamicForces)) - return false; - AerodynamicForces other = (AerodynamicForces) obj; - - return (MathUtil.equals(this.getCNa(), other.getCNa()) && - MathUtil.equals(this.getCN(), other.getCN()) && - MathUtil.equals(this.getCm(), other.getCm()) && - MathUtil.equals(this.getCside(), other.getCside()) && - MathUtil.equals(this.getCyaw(), other.getCyaw()) && - MathUtil.equals(this.getCroll(), other.getCroll()) && - MathUtil.equals(this.getCrollDamp(), other.getCrollDamp()) && - MathUtil.equals(this.getCrollForce(), other.getCrollForce()) && - MathUtil.equals(this.getCaxial(), other.getCaxial()) && - MathUtil.equals(this.getCD(), other.getCD()) && - MathUtil.equals(this.getPressureCD(), other.getPressureCD()) && - MathUtil.equals(this.getBaseCD(), other.getBaseCD()) && - MathUtil.equals(this.getFrictionCD(), other.getFrictionCD()) && - MathUtil.equals(this.getPitchDampingMoment(), other.getPitchDampingMoment()) && - MathUtil.equals(this.getYawDampingMoment(), other.getYawDampingMoment()) && - this.getCP().equals(other.getCP())); - } - - @Override - public int hashCode() { - return (int) (1000*(this.getCD()+this.getCaxial()+this.getCNa())) + this.getCP().hashCode(); - } - - - @Override - public String toString() { - String text="AerodynamicForces["; - - if (getComponent() != null) - text += "component:" + getComponent() + ","; - if (getCP() != null) - text += "cp:" + getCP() + ","; - - if (!Double.isNaN(getCNa())) - text += "CNa:" + getCNa() + ","; - if (!Double.isNaN(getCN())) - text += "CN:" + getCN() + ","; - if (!Double.isNaN(getCm())) - text += "Cm:" + getCm() + ","; - - if (!Double.isNaN(getCside())) - text += "Cside:" + getCside() + ","; - if (!Double.isNaN(getCyaw())) - text += "Cyaw:" + getCyaw() + ","; - - if (!Double.isNaN(getCroll())) - text += "Croll:" + getCroll() + ","; - if (!Double.isNaN(getCaxial())) - text += "Caxial:" + getCaxial() + ","; - - if (!Double.isNaN(getCD())) - text += "CD:" + getCD() + ","; - - if (text.charAt(text.length()-1) == ',') - text = text.substring(0, text.length()-1); - - text += "]"; - return text; - } - - @Override - public int getModID() { - return modID; - } -} diff --git a/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java deleted file mode 100644 index be139e14..00000000 --- a/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java +++ /dev/null @@ -1,715 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; - -import net.sf.openrocket.aerodynamics.barrowman.FinSetCalc; -import net.sf.openrocket.aerodynamics.barrowman.RocketComponentCalc; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.PolyInterpolator; -import net.sf.openrocket.util.Reflection; - -/** - * An aerodynamic calculator that uses the extended Barrowman method to - * calculate the CP of a rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class BarrowmanCalculator extends AbstractAerodynamicCalculator { - - private static final String BARROWMAN_PACKAGE = "net.sf.openrocket.aerodynamics.barrowman"; - private static final String BARROWMAN_SUFFIX = "Calc"; - - - private Map<RocketComponent, RocketComponentCalc> calcMap = null; - - private double cacheDiameter = -1; - private double cacheLength = -1; - - - - public BarrowmanCalculator() { - - } - - - @Override - public BarrowmanCalculator newInstance() { - return new BarrowmanCalculator(); - } - - - /** - * Calculate the CP according to the extended Barrowman method. - */ - @Override - public Coordinate getCP(Configuration configuration, FlightConditions conditions, - WarningSet warnings) { - checkCache(configuration); - AerodynamicForces forces = calculateNonAxialForces(configuration, conditions, null, warnings); - return forces.getCP(); - } - - - - @Override - public Map<RocketComponent, AerodynamicForces> getForceAnalysis(Configuration configuration, - FlightConditions conditions, WarningSet warnings) { - checkCache(configuration); - - AerodynamicForces f; - Map<RocketComponent, AerodynamicForces> map = - new LinkedHashMap<RocketComponent, AerodynamicForces>(); - - // Add all components to the map - for (RocketComponent c : configuration) { - f = new AerodynamicForces(); - f.setComponent(c); - - map.put(c, f); - } - - - // Calculate non-axial force data - AerodynamicForces total = calculateNonAxialForces(configuration, conditions, map, warnings); - - - // Calculate friction data - total.setFrictionCD(calculateFrictionDrag(configuration, conditions, map, warnings)); - total.setPressureCD(calculatePressureDrag(configuration, conditions, map, warnings)); - total.setBaseCD(calculateBaseDrag(configuration, conditions, map, warnings)); - - total.setComponent(configuration.getRocket()); - map.put(total.getComponent(), total); - - - for (RocketComponent c : map.keySet()) { - f = map.get(c); - if (Double.isNaN(f.getBaseCD()) && Double.isNaN(f.getPressureCD()) && - Double.isNaN(f.getFrictionCD())) - continue; - if (Double.isNaN(f.getBaseCD())) - f.setBaseCD(0); - if (Double.isNaN(f.getPressureCD())) - f.setPressureCD(0); - if (Double.isNaN(f.getFrictionCD())) - f.setFrictionCD(0); - f.setCD(f.getBaseCD() + f.getPressureCD() + f.getFrictionCD()); - f.setCaxial(calculateAxialDrag(conditions, f.getCD())); - } - - return map; - } - - - - @Override - public AerodynamicForces getAerodynamicForces(Configuration configuration, - FlightConditions conditions, WarningSet warnings) { - checkCache(configuration); - - if (warnings == null) - warnings = ignoreWarningSet; - - // Calculate non-axial force data - AerodynamicForces total = calculateNonAxialForces(configuration, conditions, null, warnings); - - // Calculate friction data - total.setFrictionCD(calculateFrictionDrag(configuration, conditions, null, warnings)); - total.setPressureCD(calculatePressureDrag(configuration, conditions, null, warnings)); - total.setBaseCD(calculateBaseDrag(configuration, conditions, null, warnings)); - - total.setCD(total.getFrictionCD() + total.getPressureCD() + total.getBaseCD()); - - total.setCaxial(calculateAxialDrag(conditions, total.getCD())); - - // Calculate pitch and yaw damping moments - calculateDampingMoments(configuration, conditions, total); - total.setCm(total.getCm() - total.getPitchDampingMoment()); - total.setCyaw(total.getCyaw() - total.getYawDampingMoment()); - - - return total; - } - - - - - /* - * Perform the actual CP calculation. - */ - private AerodynamicForces calculateNonAxialForces(Configuration configuration, FlightConditions conditions, - Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) { - - checkCache(configuration); - - AerodynamicForces total = new AerodynamicForces(); - total.zero(); - - double radius = 0; // aft radius of previous component - double componentX = 0; // aft coordinate of previous component - AerodynamicForces forces = new AerodynamicForces(); - - if (warnings == null) - warnings = ignoreWarningSet; - - if (conditions.getAOA() > 17.5 * Math.PI / 180) - warnings.add(new Warning.LargeAOA(conditions.getAOA())); - - - if (calcMap == null) - buildCalcMap(configuration); - - for (RocketComponent component : configuration) { - - // Skip non-aerodynamic components - if (!component.isAerodynamic()) - continue; - - // Check for discontinuities - if (component instanceof SymmetricComponent) { - SymmetricComponent sym = (SymmetricComponent) component; - // TODO:LOW: Ignores other cluster components (not clusterable) - double x = component.toAbsolute(Coordinate.NUL)[0].x; - - // Check for lengthwise discontinuity - if (x > componentX + 0.0001) { - if (!MathUtil.equals(radius, 0)) { - warnings.add(Warning.DISCONTINUITY); - radius = 0; - } - } - componentX = component.toAbsolute(new Coordinate(component.getLength()))[0].x; - - // Check for radius discontinuity - if (!MathUtil.equals(sym.getForeRadius(), radius)) { - warnings.add(Warning.DISCONTINUITY); - // TODO: MEDIUM: Apply correction to values to cp and to map - } - radius = sym.getAftRadius(); - } - - // Call calculation method - forces.zero(); - calcMap.get(component).calculateNonaxialForces(conditions, forces, warnings); - forces.setCP(component.toAbsolute(forces.getCP())[0]); - forces.setCm(forces.getCN() * forces.getCP().x / conditions.getRefLength()); - - if (map != null) { - AerodynamicForces f = map.get(component); - - f.setCP(forces.getCP()); - f.setCNa(forces.getCNa()); - f.setCN(forces.getCN()); - f.setCm(forces.getCm()); - f.setCside(forces.getCside()); - f.setCyaw(forces.getCyaw()); - f.setCroll(forces.getCroll()); - f.setCrollDamp(forces.getCrollDamp()); - f.setCrollForce(forces.getCrollForce()); - } - - total.setCP(total.getCP().average(forces.getCP())); - total.setCNa(total.getCNa() + forces.getCNa()); - total.setCN(total.getCN() + forces.getCN()); - total.setCm(total.getCm() + forces.getCm()); - total.setCside(total.getCside() + forces.getCside()); - total.setCyaw(total.getCyaw() + forces.getCyaw()); - total.setCroll(total.getCroll() + forces.getCroll()); - total.setCrollDamp(total.getCrollDamp() + forces.getCrollDamp()); - total.setCrollForce(total.getCrollForce() + forces.getCrollForce()); - } - - return total; - } - - - - - //////////////// DRAG CALCULATIONS //////////////// - - - private double calculateFrictionDrag(Configuration configuration, FlightConditions conditions, - Map<RocketComponent, AerodynamicForces> map, WarningSet set) { - double c1 = 1.0, c2 = 1.0; - - double mach = conditions.getMach(); - double Re; - double Cf; - - if (calcMap == null) - buildCalcMap(configuration); - - Re = conditions.getVelocity() * configuration.getLength() / - conditions.getAtmosphericConditions().getKinematicViscosity(); - - // Calculate the skin friction coefficient (assume non-roughness limited) - if (configuration.getRocket().isPerfectFinish()) { - - // Assume partial laminar layer. Roughness-limitation is checked later. - if (Re < 1e4) { - // Too low, constant - Cf = 1.33e-2; - } else if (Re < 5.39e5) { - // Fully laminar - Cf = 1.328 / MathUtil.safeSqrt(Re); - } else { - // Transitional - Cf = 1.0 / pow2(1.50 * Math.log(Re) - 5.6) - 1700 / Re; - } - - // Compressibility correction - - if (mach < 1.1) { - // Below Re=1e6 no correction - if (Re > 1e6) { - if (Re < 3e6) { - c1 = 1 - 0.1 * pow2(mach) * (Re - 1e6) / 2e6; // transition to turbulent - } else { - c1 = 1 - 0.1 * pow2(mach); - } - } - } - if (mach > 0.9) { - if (Re > 1e6) { - if (Re < 3e6) { - c2 = 1 + (1.0 / Math.pow(1 + 0.045 * pow2(mach), 0.25) - 1) * (Re - 1e6) / 2e6; - } else { - c2 = 1.0 / Math.pow(1 + 0.045 * pow2(mach), 0.25); - } - } - } - - // Applying continuously around Mach 1 - if (mach < 0.9) { - Cf *= c1; - } else if (mach < 1.1) { - Cf *= (c2 * (mach - 0.9) / 0.2 + c1 * (1.1 - mach) / 0.2); - } else { - Cf *= c2; - } - - - } else { - - // Assume fully turbulent. Roughness-limitation is checked later. - if (Re < 1e4) { - // Too low, constant - Cf = 1.48e-2; - } else { - // Turbulent - Cf = 1.0 / pow2(1.50 * Math.log(Re) - 5.6); - } - - // Compressibility correction - - if (mach < 1.1) { - c1 = 1 - 0.1 * pow2(mach); - } - if (mach > 0.9) { - c2 = 1 / Math.pow(1 + 0.15 * pow2(mach), 0.58); - } - // Applying continuously around Mach 1 - if (mach < 0.9) { - Cf *= c1; - } else if (mach < 1.1) { - Cf *= c2 * (mach - 0.9) / 0.2 + c1 * (1.1 - mach) / 0.2; - } else { - Cf *= c2; - } - - } - - // Roughness-limited value correction term - double roughnessCorrection; - if (mach < 0.9) { - roughnessCorrection = 1 - 0.1 * pow2(mach); - } else if (mach > 1.1) { - roughnessCorrection = 1 / (1 + 0.18 * pow2(mach)); - } else { - c1 = 1 - 0.1 * pow2(0.9); - c2 = 1.0 / (1 + 0.18 * pow2(1.1)); - roughnessCorrection = c2 * (mach - 0.9) / 0.2 + c1 * (1.1 - mach) / 0.2; - } - - - - /* - * Calculate the friction drag coefficient. - * - * The body wetted area is summed up and finally corrected with the rocket - * fineness ratio (calculated in the same iteration). The fins are corrected - * for thickness as we go on. - */ - - double finFriction = 0; - double bodyFriction = 0; - double maxR = 0, len = 0; - - double[] roughnessLimited = new double[Finish.values().length]; - Arrays.fill(roughnessLimited, Double.NaN); - - for (RocketComponent c : configuration) { - - // Consider only SymmetricComponents and FinSets: - if (!(c instanceof SymmetricComponent) && - !(c instanceof FinSet)) - continue; - - // Calculate the roughness-limited friction coefficient - Finish finish = ((ExternalComponent) c).getFinish(); - if (Double.isNaN(roughnessLimited[finish.ordinal()])) { - roughnessLimited[finish.ordinal()] = - 0.032 * Math.pow(finish.getRoughnessSize() / configuration.getLength(), 0.2) * - roughnessCorrection; - } - - /* - * Actual Cf is maximum of Cf and the roughness-limited value. - * For perfect finish require additionally that Re > 1e6 - */ - double componentCf; - if (configuration.getRocket().isPerfectFinish()) { - - // For perfect finish require Re > 1e6 - if ((Re > 1.0e6) && (roughnessLimited[finish.ordinal()] > Cf)) { - componentCf = roughnessLimited[finish.ordinal()]; - } else { - componentCf = Cf; - } - - } else { - - // For fully turbulent use simple max - componentCf = Math.max(Cf, roughnessLimited[finish.ordinal()]); - - } - - - - // Calculate the friction drag: - if (c instanceof SymmetricComponent) { - - SymmetricComponent s = (SymmetricComponent) c; - - bodyFriction += componentCf * s.getComponentWetArea(); - - if (map != null) { - // Corrected later - map.get(c).setFrictionCD(componentCf * s.getComponentWetArea() - / conditions.getRefArea()); - } - - double r = Math.max(s.getForeRadius(), s.getAftRadius()); - if (r > maxR) - maxR = r; - len += c.getLength(); - - } else if (c instanceof FinSet) { - - FinSet f = (FinSet) c; - double mac = ((FinSetCalc) calcMap.get(c)).getMACLength(); - double cd = componentCf * (1 + 2 * f.getThickness() / mac) * - 2 * f.getFinCount() * f.getFinArea(); - finFriction += cd; - - if (map != null) { - map.get(c).setFrictionCD(cd / conditions.getRefArea()); - } - - } - - } - // fB may be POSITIVE_INFINITY, but that's ok for us - double fB = (len + 0.0001) / maxR; - double correction = (1 + 1.0 / (2 * fB)); - - // Correct body data in map - if (map != null) { - for (RocketComponent c : map.keySet()) { - if (c instanceof SymmetricComponent) { - map.get(c).setFrictionCD(map.get(c).getFrictionCD() * correction); - } - } - } - - return (finFriction + correction * bodyFriction) / conditions.getRefArea(); - } - - - - private double calculatePressureDrag(Configuration configuration, FlightConditions conditions, - Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) { - - double stagnation, base, total; - double radius = 0; - - if (calcMap == null) - buildCalcMap(configuration); - - stagnation = calculateStagnationCD(conditions.getMach()); - base = calculateBaseCD(conditions.getMach()); - - total = 0; - for (RocketComponent c : configuration) { - if (!c.isAerodynamic()) - continue; - - // Pressure fore drag - double cd = calcMap.get(c).calculatePressureDragForce(conditions, stagnation, base, - warnings); - total += cd; - - if (map != null) { - map.get(c).setPressureCD(cd); - } - - - // Stagnation drag - if (c instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent) c; - - if (radius < s.getForeRadius()) { - double area = Math.PI * (pow2(s.getForeRadius()) - pow2(radius)); - cd = stagnation * area / conditions.getRefArea(); - total += cd; - if (map != null) { - map.get(c).setPressureCD(map.get(c).getPressureCD() + cd); - } - } - - radius = s.getAftRadius(); - } - } - - return total; - } - - - private double calculateBaseDrag(Configuration configuration, FlightConditions conditions, - Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) { - - double base, total; - double radius = 0; - RocketComponent prevComponent = null; - - if (calcMap == null) - buildCalcMap(configuration); - - base = calculateBaseCD(conditions.getMach()); - total = 0; - - for (RocketComponent c : configuration) { - if (!(c instanceof SymmetricComponent)) - continue; - - SymmetricComponent s = (SymmetricComponent) c; - - if (radius > s.getForeRadius()) { - double area = Math.PI * (pow2(radius) - pow2(s.getForeRadius())); - double cd = base * area / conditions.getRefArea(); - total += cd; - if (map != null) { - map.get(prevComponent).setBaseCD(cd); - } - } - - radius = s.getAftRadius(); - prevComponent = c; - } - - if (radius > 0) { - double area = Math.PI * pow2(radius); - double cd = base * area / conditions.getRefArea(); - total += cd; - if (map != null) { - map.get(prevComponent).setBaseCD(cd); - } - } - - return total; - } - - - - public static double calculateStagnationCD(double m) { - double pressure; - if (m <= 1) { - pressure = 1 + pow2(m) / 4 + pow2(pow2(m)) / 40; - } else { - pressure = 1.84 - 0.76 / pow2(m) + 0.166 / pow2(pow2(m)) + 0.035 / pow2(m * m * m); - } - return 0.85 * pressure; - } - - - public static double calculateBaseCD(double m) { - if (m <= 1) { - return 0.12 + 0.13 * m * m; - } else { - return 0.25 / m; - } - } - - - - private static final double[] axialDragPoly1, axialDragPoly2; - static { - PolyInterpolator interpolator; - interpolator = new PolyInterpolator( - new double[] { 0, 17 * Math.PI / 180 }, - new double[] { 0, 17 * Math.PI / 180 } - ); - axialDragPoly1 = interpolator.interpolator(1, 1.3, 0, 0); - - interpolator = new PolyInterpolator( - new double[] { 17 * Math.PI / 180, Math.PI / 2 }, - new double[] { 17 * Math.PI / 180, Math.PI / 2 }, - new double[] { Math.PI / 2 } - ); - axialDragPoly2 = interpolator.interpolator(1.3, 0, 0, 0, 0); - } - - - /** - * Calculate the axial drag from the total drag coefficient. - * - * @param conditions - * @param cd - * @return - */ - private double calculateAxialDrag(FlightConditions conditions, double cd) { - double aoa = MathUtil.clamp(conditions.getAOA(), 0, Math.PI); - double mul; - - // double sinaoa = conditions.getSinAOA(); - // return cd * (1 + Math.min(sinaoa, 0.25)); - - - if (aoa > Math.PI / 2) - aoa = Math.PI - aoa; - if (aoa < 17 * Math.PI / 180) - mul = PolyInterpolator.eval(aoa, axialDragPoly1); - else - mul = PolyInterpolator.eval(aoa, axialDragPoly2); - - if (conditions.getAOA() < Math.PI / 2) - return mul * cd; - else - return -mul * cd; - } - - - private void calculateDampingMoments(Configuration configuration, FlightConditions conditions, - AerodynamicForces total) { - - // Calculate pitch and yaw damping moments - double mul = getDampingMultiplier(configuration, conditions, - conditions.getPitchCenter().x); - double pitch = conditions.getPitchRate(); - double yaw = conditions.getYawRate(); - double vel = conditions.getVelocity(); - - vel = MathUtil.max(vel, 1); - - mul *= 3; // TODO: Higher damping yields much more realistic apogee turn - - total.setPitchDampingMoment(mul * MathUtil.sign(pitch) * pow2(pitch / vel)); - total.setYawDampingMoment(mul * MathUtil.sign(yaw) * pow2(yaw / vel)); - } - - // TODO: MEDIUM: Are the rotation etc. being added correctly? sin/cos theta? - - - private double getDampingMultiplier(Configuration configuration, FlightConditions conditions, - double cgx) { - if (cacheDiameter < 0) { - double area = 0; - cacheLength = 0; - cacheDiameter = 0; - - for (RocketComponent c : configuration) { - if (c instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent) c; - area += s.getComponentPlanformArea(); - cacheLength += s.getLength(); - } - } - if (cacheLength > 0) - cacheDiameter = area / cacheLength; - } - - double mul; - - // Body - mul = 0.275 * cacheDiameter / (conditions.getRefArea() * conditions.getRefLength()); - mul *= (MathUtil.pow4(cgx) + MathUtil.pow4(cacheLength - cgx)); - - // Fins - // TODO: LOW: This could be optimized a lot... - for (RocketComponent c : configuration) { - if (c instanceof FinSet) { - FinSet f = (FinSet) c; - mul += 0.6 * Math.min(f.getFinCount(), 4) * f.getFinArea() * - MathUtil.pow3(Math.abs(f.toAbsolute(new Coordinate( - ((FinSetCalc) calcMap.get(f)).getMidchordPos()))[0].x - - cgx)) / - (conditions.getRefArea() * conditions.getRefLength()); - } - } - - return mul; - } - - - - //////// The calculator map - - @Override - protected void voidAerodynamicCache() { - super.voidAerodynamicCache(); - - calcMap = null; - cacheDiameter = -1; - cacheLength = -1; - } - - - private void buildCalcMap(Configuration configuration) { - Iterator<RocketComponent> iterator; - - calcMap = new HashMap<RocketComponent, RocketComponentCalc>(); - - iterator = configuration.getRocket().iterator(); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - - if (!c.isAerodynamic()) - continue; - - calcMap.put(c, (RocketComponentCalc) Reflection.construct(BARROWMAN_PACKAGE, - c, BARROWMAN_SUFFIX, c)); - } - } - - - @Override - public int getModID() { - // Only cached data is stored, return constant mod ID - return 0; - } - - -} diff --git a/src/net/sf/openrocket/aerodynamics/FlightConditions.java b/src/net/sf/openrocket/aerodynamics/FlightConditions.java deleted file mode 100644 index 0b079e68..00000000 --- a/src/net/sf/openrocket/aerodynamics/FlightConditions.java +++ /dev/null @@ -1,467 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import java.util.ArrayList; -import java.util.EventListener; -import java.util.EventObject; -import java.util.List; - -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.StateChangeListener; -import net.sf.openrocket.util.UniqueID; - -/** - * A class defining the momentary flight conditions of a rocket, including - * the angle of attack, lateral wind angle, atmospheric conditions etc. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FlightConditions implements Cloneable, ChangeSource, Monitorable { - - private List<EventListener> listenerList = new ArrayList<EventListener>(); - private EventObject event = new EventObject(this); - - - /** Reference length used in calculations. */ - private double refLength = 1.0; - - /** Reference area used in calculations. */ - private double refArea = Math.PI * 0.25; - - - /** Angle of attack. */ - private double aoa = 0; - - /** Sine of the angle of attack. */ - private double sinAOA = 0; - - /** - * The fraction <code>sin(aoa) / aoa</code>. At an AOA of zero this value - * must be one. This value may be used in many cases to avoid checking for - * division by zero. - */ - private double sincAOA = 1.0; - - /** Lateral wind direction. */ - private double theta = 0; - - /** Current Mach speed. */ - private double mach = 0.3; - - /** - * Sqrt(1 - M^2) for M<1 - * Sqrt(M^2 - 1) for M>1 - */ - private double beta = MathUtil.safeSqrt(1 - mach * mach); - - - /** Current roll rate. */ - private double rollRate = 0; - - private double pitchRate = 0; - private double yawRate = 0; - - private Coordinate pitchCenter = Coordinate.NUL; - - - private AtmosphericConditions atmosphericConditions = new AtmosphericConditions(); - - - private int modID; - private int modIDadd = 0; - - - /** - * Sole constructor. The reference length is initialized to the reference length - * of the <code>Configuration</code>, and the reference area accordingly. - * If <code>config</code> is <code>null</code>, then the reference length is set - * to 1 meter. - * - * @param config the configuration of which the reference length is taken. - */ - public FlightConditions(Configuration config) { - if (config != null) - setRefLength(config.getReferenceLength()); - this.modID = UniqueID.next(); - } - - - /** - * Set the reference length from the given configuration. - * @param config the configuration from which to get the reference length. - */ - public void setReference(Configuration config) { - setRefLength(config.getReferenceLength()); - } - - - /** - * Set the reference length and area. - */ - public void setRefLength(double length) { - refLength = length; - - refArea = Math.PI * MathUtil.pow2(length / 2); - fireChangeEvent(); - } - - /** - * Return the reference length. - */ - public double getRefLength() { - return refLength; - } - - /** - * Set the reference area and length. - */ - public void setRefArea(double area) { - refArea = area; - refLength = MathUtil.safeSqrt(area / Math.PI) * 2; - fireChangeEvent(); - } - - /** - * Return the reference area. - */ - public double getRefArea() { - return refArea; - } - - - /** - * Sets the angle of attack. It calculates values also for the methods - * {@link #getSinAOA()} and {@link #getSincAOA()}. - * - * @param aoa the angle of attack. - */ - public void setAOA(double aoa) { - aoa = MathUtil.clamp(aoa, 0, Math.PI); - if (MathUtil.equals(this.aoa, aoa)) - return; - - this.aoa = aoa; - if (aoa < 0.001) { - this.sinAOA = aoa; - this.sincAOA = 1.0; - } else { - this.sinAOA = Math.sin(aoa); - this.sincAOA = sinAOA / aoa; - } - fireChangeEvent(); - } - - - /** - * Sets the angle of attack with the sine. The value <code>sinAOA</code> is assumed - * to be the sine of <code>aoa</code> for cases in which this value is known. - * The AOA must still be specified, as the sine is not unique in the range - * of 0..180 degrees. - * - * @param aoa the angle of attack in radians. - * @param sinAOA the sine of the angle of attack. - */ - public void setAOA(double aoa, double sinAOA) { - aoa = MathUtil.clamp(aoa, 0, Math.PI); - sinAOA = MathUtil.clamp(sinAOA, 0, 1); - if (MathUtil.equals(this.aoa, aoa)) - return; - - assert (Math.abs(Math.sin(aoa) - sinAOA) < 0.0001) : "Illegal sine: aoa=" + aoa + " sinAOA=" + sinAOA; - - this.aoa = aoa; - this.sinAOA = sinAOA; - if (aoa < 0.001) { - this.sincAOA = 1.0; - } else { - this.sincAOA = sinAOA / aoa; - } - fireChangeEvent(); - } - - - /** - * Return the angle of attack. - */ - public double getAOA() { - return aoa; - } - - /** - * Return the sine of the angle of attack. - */ - public double getSinAOA() { - return sinAOA; - } - - /** - * Return the sinc of the angle of attack (sin(AOA) / AOA). This method returns - * one if the angle of attack is zero. - */ - public double getSincAOA() { - return sincAOA; - } - - - /** - * Set the direction of the lateral airflow. - */ - public void setTheta(double theta) { - if (MathUtil.equals(this.theta, theta)) - return; - this.theta = theta; - fireChangeEvent(); - } - - /** - * Return the direction of the lateral airflow. - */ - public double getTheta() { - return theta; - } - - - /** - * Set the current Mach speed. This should be (but is not required to be) in - * reference to the speed of sound of the atmospheric conditions. - */ - public void setMach(double mach) { - mach = Math.max(mach, 0); - if (MathUtil.equals(this.mach, mach)) - return; - - this.mach = mach; - if (mach < 1) - this.beta = MathUtil.safeSqrt(1 - mach * mach); - else - this.beta = MathUtil.safeSqrt(mach * mach - 1); - fireChangeEvent(); - } - - /** - * Return the current Mach speed. - */ - public double getMach() { - return mach; - } - - /** - * Returns the current rocket velocity, calculated from the Mach number and the - * speed of sound. If either of these parameters are changed, the velocity changes - * as well. - * - * @return the velocity of the rocket. - */ - public double getVelocity() { - return mach * atmosphericConditions.getMachSpeed(); - } - - /** - * Sets the Mach speed according to the given velocity and the current speed of sound. - * - * @param velocity the current velocity. - */ - public void setVelocity(double velocity) { - setMach(velocity / atmosphericConditions.getMachSpeed()); - } - - - /** - * Return sqrt(abs(1 - Mach^2)). This is calculated in the setting call and is - * therefore fast. - */ - public double getBeta() { - return beta; - } - - - /** - * Return the current roll rate. - */ - public double getRollRate() { - return rollRate; - } - - - /** - * Set the current roll rate. - */ - public void setRollRate(double rate) { - if (MathUtil.equals(this.rollRate, rate)) - return; - - this.rollRate = rate; - fireChangeEvent(); - } - - - public double getPitchRate() { - return pitchRate; - } - - - public void setPitchRate(double pitchRate) { - if (MathUtil.equals(this.pitchRate, pitchRate)) - return; - this.pitchRate = pitchRate; - fireChangeEvent(); - } - - - public double getYawRate() { - return yawRate; - } - - - public void setYawRate(double yawRate) { - if (MathUtil.equals(this.yawRate, yawRate)) - return; - this.yawRate = yawRate; - fireChangeEvent(); - } - - - - - /** - * @return the pitchCenter - */ - public Coordinate getPitchCenter() { - return pitchCenter; - } - - - /** - * @param pitchCenter the pitchCenter to set - */ - public void setPitchCenter(Coordinate pitchCenter) { - if (this.pitchCenter.equals(pitchCenter)) - return; - this.pitchCenter = pitchCenter; - fireChangeEvent(); - } - - - /** - * Return the current atmospheric conditions. Note that this method returns a - * reference to the {@link AtmosphericConditions} object used by this object. - * Changes made to the object will modify the encapsulated object, but will NOT - * generate change events. - * - * @return the current atmospheric conditions. - */ - public AtmosphericConditions getAtmosphericConditions() { - return atmosphericConditions; - } - - /** - * Set the current atmospheric conditions. This method will fire a change event - * if a change occurs. - */ - public void setAtmosphericConditions(AtmosphericConditions cond) { - if (atmosphericConditions.equals(cond)) - return; - modIDadd += atmosphericConditions.getModID(); - atmosphericConditions = cond; - fireChangeEvent(); - } - - - /** - * Retrieve the modification count of this object. Each time it is modified - * the modification count is increased by one. - * - * @return the number of times this object has been modified since instantiation. - */ - @Override - public int getModID() { - return modID + modIDadd + this.atmosphericConditions.getModID(); - } - - - @Override - public String toString() { - return String.format("FlightConditions[" + - "aoa=%.2f\u00b0," + - "theta=%.2f\u00b0," + - "mach=%.3f," + - "rollRate=%.2f," + - "pitchRate=%.2f," + - "yawRate=%.2f," + - "refLength=%.3f," + - "pitchCenter=" + pitchCenter.toString() + "," + - "atmosphericConditions=" + atmosphericConditions.toString() + - "]", - aoa * 180 / Math.PI, theta * 180 / Math.PI, mach, rollRate, pitchRate, yawRate, refLength); - } - - - /** - * Return a copy of the flight conditions. The copy has no listeners. The - * atmospheric conditions is also cloned. - */ - @Override - public FlightConditions clone() { - try { - FlightConditions cond = (FlightConditions) super.clone(); - cond.listenerList = new ArrayList<EventListener>(); - cond.event = new EventObject(cond); - cond.atmosphericConditions = atmosphericConditions.clone(); - return cond; - } catch (CloneNotSupportedException e) { - throw new BugException("clone not supported!", e); - } - } - - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof FlightConditions)) - return false; - - FlightConditions other = (FlightConditions) obj; - - return (MathUtil.equals(this.refLength, other.refLength) && - MathUtil.equals(this.aoa, other.aoa) && - MathUtil.equals(this.theta, other.theta) && - MathUtil.equals(this.mach, other.mach) && - MathUtil.equals(this.rollRate, other.rollRate) && - MathUtil.equals(this.pitchRate, other.pitchRate) && - MathUtil.equals(this.yawRate, other.yawRate) && - this.pitchCenter.equals(other.pitchCenter) && this.atmosphericConditions.equals(other.atmosphericConditions)); - } - - @Override - public int hashCode() { - return (int) (1000 * (refLength + aoa + theta + mach + rollRate + pitchRate + yawRate)); - } - - - @Override - public void addChangeListener(EventListener listener) { - listenerList.add(0, listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - listenerList.remove(listener); - } - - protected void fireChangeEvent() { - modID = UniqueID.next(); - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] listeners = listenerList.toArray(new EventListener[0]); - for (EventListener l : listeners) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(event); - } - } - } -} diff --git a/src/net/sf/openrocket/aerodynamics/Warning.java b/src/net/sf/openrocket/aerodynamics/Warning.java deleted file mode 100644 index e44541c0..00000000 --- a/src/net/sf/openrocket/aerodynamics/Warning.java +++ /dev/null @@ -1,164 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public abstract class Warning { - - private static final Translator trans = Application.getTranslator(); - - /** - * Return a Warning with the specific text. - */ - public static Warning fromString(String text) { - return new Warning.Other(text); - } - - - /** - * Return <code>true</code> if the <code>other</code> warning should replace - * this warning. The method should return <code>true</code> if the other - * warning indicates a "worse" condition than the current warning. - * - * @param other the warning to compare to - * @return whether this warning should be replaced - */ - public abstract boolean replaceBy(Warning other); - - - /** - * Two <code>Warning</code>s are by default considered equal if they are of - * the same class. Therefore only one instance of a particular warning type - * is stored in a {@link WarningSet}. Subclasses may override this method for - * more specific functionality. - */ - @Override - public boolean equals(Object o) { - return (o.getClass() == this.getClass()); - } - - /** - * A <code>hashCode</code> method compatible with the <code>equals</code> method. - */ - @Override - public int hashCode() { - return this.getClass().hashCode(); - } - - - - - ///////////// Specific warning classes ///////////// - - - /** - * A <code>Warning</code> indicating a large angle of attack was encountered. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public static class LargeAOA extends Warning { - private double aoa; - - /** - * Sole constructor. The argument is the AOA that caused this warning. - * - * @param aoa the angle of attack that caused this warning - */ - public LargeAOA(double aoa) { - this.aoa = aoa; - } - - @Override - public String toString() { - if (Double.isNaN(aoa)) - //// Large angle of attack encountered. - return trans.get("Warning.LargeAOA.str1"); - //// Large angle of attack encountered ( - return (trans.get("Warning.LargeAOA.str2") + - UnitGroup.UNITS_ANGLE.getDefaultUnit().toString(aoa) + ")."); - } - - @Override - public boolean replaceBy(Warning other) { - if (!(other instanceof LargeAOA)) - return false; - - LargeAOA o = (LargeAOA)other; - if (Double.isNaN(this.aoa)) // If this has value NaN then replace - return true; - return (o.aoa > this.aoa); - } - } - - - - /** - * An unspecified warning type. This warning type holds a <code>String</code> - * describing it. Two warnings of this type are considered equal if the strings - * are identical. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public static class Other extends Warning { - private String description; - - public Other(String description) { - this.description = description; - } - - @Override - public String toString() { - return description; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Other)) - return false; - - Other o = (Other)other; - return (o.description.equals(this.description)); - } - - @Override - public int hashCode() { - return description.hashCode(); - } - - @Override - public boolean replaceBy(Warning other) { - return false; - } - } - - - /** A <code>Warning</code> that the body diameter is discontinuous. */ -////Discontinuity in rocket body diameter. - public static final Warning DISCONTINUITY = - new Other(trans.get("Warning.DISCONTINUITY")); - - /** A <code>Warning</code> that the fins are thick compared to the rocket body. */ -////Thick fins may not be modeled accurately. - public static final Warning THICK_FIN = - new Other(trans.get("Warning.THICK_FIN")); - - /** A <code>Warning</code> that the fins have jagged edges. */ -////Jagged-edged fin predictions may be inaccurate. - public static final Warning JAGGED_EDGED_FIN = - new Other(trans.get("Warning.JAGGED_EDGED_FIN")); - - /** A <code>Warning</code> that simulation listeners have affected the simulation */ -////Listeners modified the flight simulation - public static final Warning LISTENERS_AFFECTED = - new Other(trans.get("Warning.LISTENERS_AFFECTED")); - -////Recovery device opened while motor still burning. - public static final Warning RECOVERY_DEPLOYMENT_WHILE_BURNING = - new Other(trans.get("Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING")); - - - //// Invalid parameter encountered, ignoring. - public static final Warning FILE_INVALID_PARAMETER = - new Other(trans.get("Warning.FILE_INVALID_PARAMETER")); -} diff --git a/src/net/sf/openrocket/aerodynamics/WarningSet.java b/src/net/sf/openrocket/aerodynamics/WarningSet.java deleted file mode 100644 index c4e81c62..00000000 --- a/src/net/sf/openrocket/aerodynamics/WarningSet.java +++ /dev/null @@ -1,136 +0,0 @@ -package net.sf.openrocket.aerodynamics; - -import java.util.AbstractSet; -import java.util.Iterator; - -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.Mutable; - -/** - * A set that contains multiple <code>Warning</code>s. When adding a - * {@link Warning} to this set, the contents is checked for a warning of the - * same type. If one is found, then the warning left in the set is determined - * by the method {@link Warning#replaceBy(Warning)}. - * <p> - * A WarningSet can be made immutable by calling {@link #immute()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class WarningSet extends AbstractSet<Warning> implements Cloneable, Monitorable { - - private ArrayList<Warning> warnings = new ArrayList<Warning>(); - - private Mutable mutable = new Mutable(); - - private int modID = 0; - - /** - * Add a <code>Warning</code> to the set. If a warning of the same type - * exists in the set, the warning that is left in the set is defined by the - * method {@link Warning#replaceBy(Warning)}. - * - * @throws IllegalStateException if this warning set has been made immutable. - */ - @Override - public boolean add(Warning w) { - mutable.check(); - - modID++; - int index = warnings.indexOf(w); - - if (index < 0) { - warnings.add(w); - return false; - } - - Warning old = warnings.get(index); - if (old.replaceBy(w)) { - warnings.set(index, w); - } - - return true; - } - - /** - * Add a <code>Warning</code> with the specified text to the set. The Warning object - * is created using the {@link Warning#fromString(String)} method. If a warning of the - * same type exists in the set, the warning that is left in the set is defined by the - * method {@link Warning#replaceBy(Warning)}. - * - * @param s the warning text. - * @throws IllegalStateException if this warning set has been made immutable. - */ - public boolean add(String s) { - mutable.check(); - return add(Warning.fromString(s)); - } - - - @Override - public Iterator<Warning> iterator() { - final Iterator<Warning> iterator = warnings.iterator(); - return new Iterator<Warning>() { - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public Warning next() { - return iterator.next(); - } - - @Override - public void remove() { - mutable.check(); - iterator.remove(); - } - - }; - } - - @Override - public int size() { - return warnings.size(); - } - - - public void immute() { - mutable.immute(); - } - - - @Override - public WarningSet clone() { - try { - - WarningSet newSet = (WarningSet) super.clone(); - newSet.warnings = this.warnings.clone(); - newSet.mutable = this.mutable.clone(); - return newSet; - - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException occurred, report bug!", e); - } - } - - - @Override - public String toString() { - String s = ""; - - for (Warning w : warnings) { - if (s.length() > 0) - s = s + ","; - s += w.toString(); - } - return "WarningSet[" + s + "]"; - } - - @Override - public int getModID() { - return modID; - } -} diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java deleted file mode 100644 index bd8ee75e..00000000 --- a/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java +++ /dev/null @@ -1,776 +0,0 @@ -package net.sf.openrocket.aerodynamics.barrowman; - -import static java.lang.Math.pow; -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.util.Arrays; - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.LinearInterpolator; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.PolyInterpolator; - - -public class FinSetCalc extends RocketComponentCalc { - private static final double STALL_ANGLE = (20 * Math.PI / 180); - - /** Number of divisions in the fin chords. */ - protected static final int DIVISIONS = 48; - - - - protected double macLength = Double.NaN; // MAC length - protected double macLead = Double.NaN; // MAC leading edge position - protected double macSpan = Double.NaN; // MAC spanwise position - protected double finArea = Double.NaN; // Fin area - protected double ar = Double.NaN; // Fin aspect ratio - protected double span = Double.NaN; // Fin span - protected double cosGamma = Double.NaN; // Cosine of midchord sweep angle - protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle - protected double rollSum = Double.NaN; // Roll damping sum term - - protected int interferenceFinCount = -1; // No. of fins in interference - - protected double[] chordLead = new double[DIVISIONS]; - protected double[] chordTrail = new double[DIVISIONS]; - protected double[] chordLength = new double[DIVISIONS]; - - - protected final WarningSet geometryWarnings = new WarningSet(); - - private double[] poly = new double[6]; - - private final double thickness; - private final double bodyRadius; - private final int finCount; - private final double baseRotation; - private final double cantAngle; - private final FinSet.CrossSection crossSection; - - public FinSetCalc(RocketComponent component) { - super(component); - if (!(component instanceof FinSet)) { - throw new IllegalArgumentException("Illegal component type " + component); - } - - FinSet fin = (FinSet) component; - thickness = fin.getThickness(); - bodyRadius = fin.getBodyRadius(); - finCount = fin.getFinCount(); - baseRotation = fin.getBaseRotation(); - cantAngle = fin.getCantAngle(); - span = fin.getSpan(); - finArea = fin.getFinArea(); - crossSection = fin.getCrossSection(); - - calculateFinGeometry(fin); - calculatePoly(); - calculateInterferenceFinCount(fin); - } - - - /* - * Calculates the non-axial forces produced by the fins (normal and side forces, - * pitch, yaw and roll moments, CP position, CNa). - */ - @Override - public void calculateNonaxialForces(FlightConditions conditions, - AerodynamicForces forces, WarningSet warnings) { - - - if (span < 0.001) { - forces.setCm(0); - forces.setCN(0); - forces.setCNa(0); - forces.setCP(Coordinate.NUL); - forces.setCroll(0); - forces.setCrollDamp(0); - forces.setCrollForce(0); - forces.setCside(0); - forces.setCyaw(0); - return; - } - - - // Add warnings (radius/2 == diameter/4) - if (thickness > bodyRadius / 2) { - warnings.add(Warning.THICK_FIN); - } - warnings.addAll(geometryWarnings); - - - - //////// Calculate CNa. ///////// - - // One fin without interference (both sub- and supersonic): - double cna1 = calculateFinCNa1(conditions); - - - - // Multiple fins with fin-fin interference - double cna; - double theta = conditions.getTheta(); - double angle = baseRotation; - - - // Compute basic CNa without interference effects - if (finCount == 1 || finCount == 2) { - // Basic CNa from geometry - double mul = 0; - for (int i = 0; i < finCount; i++) { - mul += MathUtil.pow2(Math.sin(theta - angle)); - angle += 2 * Math.PI / finCount; - } - cna = cna1 * mul; - } else { - // Basic CNa assuming full efficiency - cna = cna1 * finCount / 2.0; - } - - - // Take into account fin-fin interference effects - switch (interferenceFinCount) { - case 1: - case 2: - case 3: - case 4: - // No interference effect - break; - - case 5: - cna *= 0.948; - break; - - case 6: - cna *= 0.913; - break; - - case 7: - cna *= 0.854; - break; - - case 8: - cna *= 0.81; - break; - - default: - // Assume 75% efficiency - cna *= 0.75; - warnings.add("Too many parallel fins"); - break; - } - - /* - * Used in 0.9.5 and earlier. Takes into account rotation angle for three - * and four fins, does not take into account interference from other fin sets. - * - switch (fins) { - case 1: - case 2: - // from geometry - double mul = 0; - for (int i=0; i < fins; i++) { - mul += MathUtil.pow2(Math.sin(theta - angle)); - angle += 2 * Math.PI / fins; - } - cna = cna1*mul; - break; - - case 3: - // multiplier 1.5, sinusoidal reduction of 15% - cna = cna1 * 1.5 * (1 - 0.15*pow2(Math.cos(1.5 * (theta-angle)))); - break; - - case 4: - // multiplier 2.0, sinusoidal reduction of 6% - cna = cna1 * 2.0 * (1 - 0.06*pow2(Math.sin(2 * (theta-angle)))); - break; - - case 5: - cna = 2.37 * cna1; - break; - - case 6: - cna = 2.74 * cna1; - break; - - case 7: - cna = 2.99 * cna1; - break; - - case 8: - cna = 3.24 * cna1; - break; - - default: - // Assume N/2 * 3/4 efficiency for more fins - cna = cna1 * fins * 3.0/8.0; - break; - } - */ - - // Body-fin interference effect - double r = bodyRadius; - double tau = r / (span + r); - if (Double.isNaN(tau) || Double.isInfinite(tau)) - tau = 0; - cna *= 1 + tau; // Classical Barrowman - // cna *= pow2(1 + tau); // Barrowman thesis (too optimistic??) - - - - // TODO: LOW: check for fin tip mach cone interference - // (Barrowman thesis pdf-page 40) - - // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25 - - - - // Calculate CP position - double x = macLead + calculateCPPos(conditions) * macLength; - - - - // Calculate roll forces, reduce forcing above stall angle - - // Without body-fin interference effect: - // forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() / - // conditions.getRefLength(); - // With body-fin interference effect: - forces.setCrollForce(finCount * (macSpan + r) * cna1 * (1 + tau) * cantAngle / conditions.getRefLength()); - - - - - if (conditions.getAOA() > STALL_ANGLE) { - // System.out.println("Fin stalling in roll"); - forces.setCrollForce(forces.getCrollForce() * MathUtil.clamp( - 1 - (conditions.getAOA() - STALL_ANGLE) / (STALL_ANGLE / 2), 0, 1)); - } - forces.setCrollDamp(calculateDampingMoment(conditions)); - forces.setCroll(forces.getCrollForce() - forces.getCrollDamp()); - - - - // System.out.printf(component.getName() + ": roll rate:%.3f force:%.3f damp:%.3f " + - // "total:%.3f\n", - // conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll); - - forces.setCNa(cna); - forces.setCN(cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE)); - forces.setCP(new Coordinate(x, 0, 0, cna)); - forces.setCm(forces.getCN() * x / conditions.getRefLength()); - - /* - * TODO: HIGH: Compute actual side force and yaw moment. - * This is not currently performed because it produces strange results for - * stable rockets that have two fins in the front part of the fuselage, - * where the rocket flies at an ever-increasing angle of attack. This may - * be due to incorrect computation of pitch/yaw damping moments. - */ - // if (fins == 1 || fins == 2) { - // forces.Cside = fins * cna1 * Math.cos(theta-angle) * Math.sin(theta-angle); - // forces.Cyaw = fins * forces.Cside * x / conditions.getRefLength(); - // } else { - // forces.Cside = 0; - // forces.Cyaw = 0; - // } - forces.setCside(0); - forces.setCyaw(0); - - - } - - - /** - * Returns the MAC length of the fin. This is required in the friction drag - * computation. - * - * @return the MAC length of the fin. - */ - public double getMACLength() { - return macLength; - } - - public double getMidchordPos() { - return macLead + 0.5 * macLength; - } - - - - /** - * Pre-calculates the fin geometry values. - */ - protected void calculateFinGeometry(FinSet component) { - - span = component.getSpan(); - finArea = component.getFinArea(); - ar = 2 * pow2(span) / finArea; - - Coordinate[] points = component.getFinPoints(); - - // Check for jagged edges - geometryWarnings.clear(); - boolean down = false; - for (int i = 1; i < points.length; i++) { - if ((points[i].y > points[i - 1].y + 0.001) && down) { - geometryWarnings.add(Warning.JAGGED_EDGED_FIN); - break; - } - if (points[i].y < points[i - 1].y - 0.001) { - down = true; - } - } - - - // Calculate the chord lead and trail positions and length - - Arrays.fill(chordLead, Double.POSITIVE_INFINITY); - Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY); - Arrays.fill(chordLength, 0); - - for (int point = 1; point < points.length; point++) { - double x1 = points[point - 1].x; - double y1 = points[point - 1].y; - double x2 = points[point].x; - double y2 = points[point].y; - - if (MathUtil.equals(y1, y2)) - continue; - - int i1 = (int) (y1 * 1.0001 / span * (DIVISIONS - 1)); - int i2 = (int) (y2 * 1.0001 / span * (DIVISIONS - 1)); - i1 = MathUtil.clamp(i1, 0, DIVISIONS - 1); - i2 = MathUtil.clamp(i2, 0, DIVISIONS - 1); - if (i1 > i2) { - int tmp = i2; - i2 = i1; - i1 = tmp; - } - - for (int i = i1; i <= i2; i++) { - // Intersection point (x,y) - double y = i * span / (DIVISIONS - 1); - double x = (y - y2) / (y1 - y2) * x1 + (y1 - y) / (y1 - y2) * x2; - if (x < chordLead[i]) - chordLead[i] = x; - if (x > chordTrail[i]) - chordTrail[i] = x; - - // TODO: LOW: If fin point exactly on chord line, might be counted twice: - if (y1 < y2) { - chordLength[i] -= x; - } else { - chordLength[i] += x; - } - } - } - - // Check and correct any inconsistencies - for (int i = 0; i < DIVISIONS; i++) { - if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) || - Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) { - chordLead[i] = 0; - chordTrail[i] = 0; - } - if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) { - chordLength[i] = 0; - } - if (chordLength[i] > chordTrail[i] - chordLead[i]) { - chordLength[i] = chordTrail[i] - chordLead[i]; - } - } - - - /* Calculate fin properties: - * - * macLength // MAC length - * macLead // MAC leading edge position - * macSpan // MAC spanwise position - * ar // Fin aspect ratio (already set) - * span // Fin span (already set) - */ - macLength = 0; - macLead = 0; - macSpan = 0; - cosGamma = 0; - cosGammaLead = 0; - rollSum = 0; - double area = 0; - double radius = component.getBodyRadius(); - - final double dy = span / (DIVISIONS - 1); - for (int i = 0; i < DIVISIONS; i++) { - double length = chordTrail[i] - chordLead[i]; - double y = i * dy; - - macLength += length * length; - macSpan += y * length; - macLead += chordLead[i] * length; - area += length; - rollSum += chordLength[i] * pow2(radius + y); - - if (i > 0) { - double dx = (chordTrail[i] + chordLead[i]) / 2 - (chordTrail[i - 1] + chordLead[i - 1]) / 2; - cosGamma += dy / MathUtil.hypot(dx, dy); - - dx = chordLead[i] - chordLead[i - 1]; - cosGammaLead += dy / MathUtil.hypot(dx, dy); - } - } - - macLength *= dy; - macSpan *= dy; - macLead *= dy; - area *= dy; - rollSum *= dy; - - macLength /= area; - macSpan /= area; - macLead /= area; - cosGamma /= (DIVISIONS - 1); - cosGammaLead /= (DIVISIONS - 1); - } - - - /////////////// CNa1 calculation //////////////// - - private static final double CNA_SUBSONIC = 0.9; - private static final double CNA_SUPERSONIC = 1.5; - private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC) - 1, 1.5); - private static final double GAMMA = 1.4; - private static final LinearInterpolator K1, K2, K3; - private static final PolyInterpolator cnaInterpolator = new PolyInterpolator( - new double[] { CNA_SUBSONIC, CNA_SUPERSONIC }, - new double[] { CNA_SUBSONIC, CNA_SUPERSONIC }, - new double[] { CNA_SUBSONIC } - ); - /* Pre-calculate the values for K1, K2 and K3 */ - static { - // Up to Mach 5 - int n = (int) ((5.0 - CNA_SUPERSONIC) * 10); - double[] x = new double[n]; - double[] k1 = new double[n]; - double[] k2 = new double[n]; - double[] k3 = new double[n]; - for (int i = 0; i < n; i++) { - double M = CNA_SUPERSONIC + i * 0.1; - double beta = MathUtil.safeSqrt(M * M - 1); - x[i] = M; - k1[i] = 2.0 / beta; - k2[i] = ((GAMMA + 1) * pow(M, 4) - 4 * pow2(beta)) / (4 * pow(beta, 4)); - k3[i] = ((GAMMA + 1) * pow(M, 8) + (2 * pow2(GAMMA) - 7 * GAMMA - 5) * pow(M, 6) + - 10 * (GAMMA + 1) * pow(M, 4) + 8) / (6 * pow(beta, 7)); - } - K1 = new LinearInterpolator(x, k1); - K2 = new LinearInterpolator(x, k2); - K3 = new LinearInterpolator(x, k3); - - // System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]); - // System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]); - // System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]); - } - - - protected double calculateFinCNa1(FlightConditions conditions) { - double mach = conditions.getMach(); - double ref = conditions.getRefArea(); - double alpha = MathUtil.min(conditions.getAOA(), - Math.PI - conditions.getAOA(), STALL_ANGLE); - - // Subsonic case - if (mach <= CNA_SUBSONIC) { - return 2 * Math.PI * pow2(span) / (1 + MathUtil.safeSqrt(1 + (1 - pow2(mach)) * - pow2(pow2(span) / (finArea * cosGamma)))) / ref; - } - - // Supersonic case - if (mach >= CNA_SUPERSONIC) { - return finArea * (K1.getValue(mach) + K2.getValue(mach) * alpha + - K3.getValue(mach) * pow2(alpha)) / ref; - } - - // Transonic case, interpolate - double subV, superV; - double subD, superD; - - double sq = MathUtil.safeSqrt(1 + (1 - pow2(CNA_SUBSONIC)) * pow2(span * span / (finArea * cosGamma))); - subV = 2 * Math.PI * pow2(span) / ref / (1 + sq); - subD = 2 * mach * Math.PI * pow(span, 6) / (pow2(finArea * cosGamma) * ref * - sq * pow2(1 + sq)); - - superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC) * alpha + - K3.getValue(CNA_SUPERSONIC) * pow2(alpha)) / ref; - superD = -finArea / ref * 2 * CNA_SUPERSONIC / CNA_SUPERSONIC_B; - - // System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD); - - return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0); - } - - - - - private double calculateDampingMoment(FlightConditions conditions) { - double rollRate = conditions.getRollRate(); - - if (Math.abs(rollRate) < 0.1) - return 0; - - double mach = conditions.getMach(); - double absRate = Math.abs(rollRate); - - - /* - * At low speeds and relatively large roll rates (i.e. near apogee) the - * fin tips rotate well above stall angle. In this case sum the chords - * separately. - */ - if (absRate * (bodyRadius + span) / conditions.getVelocity() > 15 * Math.PI / 180) { - double sum = 0; - for (int i = 0; i < DIVISIONS; i++) { - double dist = bodyRadius + span * i / DIVISIONS; - double aoa = Math.min(absRate * dist / conditions.getVelocity(), 15 * Math.PI / 180); - sum += chordLength[i] * dist * aoa; - } - sum = sum * (span / DIVISIONS) * 2 * Math.PI / conditions.getBeta() / - (conditions.getRefArea() * conditions.getRefLength()); - - // System.out.println("SPECIAL: " + - // (MathUtil.sign(rollRate) *component.getFinCount() * sum)); - return MathUtil.sign(rollRate) * finCount * sum; - } - - - - if (mach <= CNA_SUBSONIC) { - // System.out.println("BASIC: "+ - // (component.getFinCount() * 2*Math.PI * rollRate * rollSum / - // (conditions.getRefArea() * conditions.getRefLength() * - // conditions.getVelocity() * conditions.getBeta()))); - - return finCount * 2 * Math.PI * rollRate * rollSum / - (conditions.getRefArea() * conditions.getRefLength() * - conditions.getVelocity() * conditions.getBeta()); - } - if (mach >= CNA_SUPERSONIC) { - - double vel = conditions.getVelocity(); - double k1 = K1.getValue(mach); - double k2 = K2.getValue(mach); - double k3 = K3.getValue(mach); - - double sum = 0; - - for (int i = 0; i < DIVISIONS; i++) { - double y = i * span / (DIVISIONS - 1); - double angle = rollRate * (bodyRadius + y) / vel; - - sum += (k1 * angle + k2 * angle * angle + k3 * angle * angle * angle) - * chordLength[i] * (bodyRadius + y); - } - - return finCount * sum * span / (DIVISIONS - 1) / - (conditions.getRefArea() * conditions.getRefLength()); - } - - // Transonic, do linear interpolation - - FlightConditions cond = conditions.clone(); - cond.setMach(CNA_SUBSONIC - 0.01); - double subsonic = calculateDampingMoment(cond); - cond.setMach(CNA_SUPERSONIC + 0.01); - double supersonic = calculateDampingMoment(cond); - - return subsonic * (CNA_SUPERSONIC - mach) / (CNA_SUPERSONIC - CNA_SUBSONIC) + - supersonic * (mach - CNA_SUBSONIC) / (CNA_SUPERSONIC - CNA_SUBSONIC); - } - - - - - /** - * Return the relative position of the CP along the mean aerodynamic chord. - * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an - * empirical formula, between these two using an interpolation polynomial. - * - * @param cond Mach speed used - * @return CP position along the MAC - */ - private double calculateCPPos(FlightConditions cond) { - double m = cond.getMach(); - if (m <= 0.5) { - // At subsonic speeds CP at quarter chord - return 0.25; - } - if (m >= 2) { - // At supersonic speeds use empirical formula - double beta = cond.getBeta(); - return (ar * beta - 0.67) / (2 * ar * beta - 1); - } - - // In between use interpolation polynomial - double x = 1.0; - double val = 0; - - for (int i = 0; i < poly.length; i++) { - val += poly[i] * x; - x *= m; - } - - return val; - } - - /** - * Calculate CP position interpolation polynomial coefficients from the - * fin geometry. This is a fifth order polynomial that satisfies - * - * p(0.5)=0.25 - * p'(0.5)=0 - * p(2) = f(2) - * p'(2) = f'(2) - * p''(2) = 0 - * p'''(2) = 0 - * - * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1). - * - * The values were calculated analytically in Mathematica. The coefficients - * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ... - */ - private void calculatePoly() { - double denom = pow2(1 - 3.4641 * ar); // common denominator - - poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom; - poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom; - poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom; - poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom; - poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom; - poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom; - } - - - // @SuppressWarnings("null") - // public static void main(String arg[]) { - // Rocket rocket = TestRocket.makeRocket(); - // FinSet finset = null; - // - // Iterator<RocketComponent> iter = rocket.deepIterator(); - // while (iter.hasNext()) { - // RocketComponent c = iter.next(); - // if (c instanceof FinSet) { - // finset = (FinSet)c; - // break; - // } - // } - // - // ((TrapezoidFinSet)finset).setHeight(0.10); - // ((TrapezoidFinSet)finset).setRootChord(0.10); - // ((TrapezoidFinSet)finset).setTipChord(0.10); - // ((TrapezoidFinSet)finset).setSweep(0.0); - // - // - // FinSetCalc calc = new FinSetCalc(finset); - // - // calc.calculateFinGeometry(); - // FlightConditions cond = new FlightConditions(new Configuration(rocket)); - // for (double m=0; m < 3; m+=0.05) { - // cond.setMach(m); - // cond.setAOA(0.0*Math.PI/180); - // double cna = calc.calculateFinCNa1(cond); - // System.out.printf("%5.2f "+cna+"\n", m); - // } - // - // } - - - @Override - public double calculatePressureDragForce(FlightConditions conditions, - double stagnationCD, double baseCD, WarningSet warnings) { - - double mach = conditions.getMach(); - double drag = 0; - - // Pressure fore-drag - if (crossSection == FinSet.CrossSection.AIRFOIL || - crossSection == FinSet.CrossSection.ROUNDED) { - - // Round leading edge - if (mach < 0.9) { - drag = Math.pow(1 - pow2(mach), -0.417) - 1; - } else if (mach < 1) { - drag = 1 - 1.785 * (mach - 0.9); - } else { - drag = 1.214 - 0.502 / pow2(mach) + 0.1095 / pow2(pow2(mach)); - } - - } else if (crossSection == FinSet.CrossSection.SQUARE) { - drag = stagnationCD; - } else { - throw new UnsupportedOperationException("Unsupported fin profile: " + crossSection); - } - - // Slanted leading edge - drag *= pow2(cosGammaLead); - - // Trailing edge drag - if (crossSection == FinSet.CrossSection.SQUARE) { - drag += baseCD; - } else if (crossSection == FinSet.CrossSection.ROUNDED) { - drag += baseCD / 2; - } - // Airfoil assumed to have zero base drag - - - // Scale to correct reference area - drag *= finCount * span * thickness / conditions.getRefArea(); - - return drag; - } - - - private void calculateInterferenceFinCount(FinSet component) { - RocketComponent parent = component.getParent(); - if (parent == null) { - throw new IllegalStateException("fin set without parent component"); - } - - double lead = component.toRelative(Coordinate.NUL, parent)[0].x; - double trail = component.toRelative(new Coordinate(component.getLength()), - parent)[0].x; - - /* - * The counting fails if the fin root chord is very small, in that case assume - * no other fin interference than this fin set. - */ - if (trail - lead < 0.007) { - interferenceFinCount = finCount; - } else { - interferenceFinCount = 0; - for (RocketComponent c : parent.getChildren()) { - if (c instanceof FinSet) { - double finLead = c.toRelative(Coordinate.NUL, parent)[0].x; - double finTrail = c.toRelative(new Coordinate(c.getLength()), parent)[0].x; - - // Compute overlap of the fins - - if ((finLead < trail - 0.005) && (finTrail > lead + 0.005)) { - interferenceFinCount += ((FinSet) c).getFinCount(); - } - } - } - } - if (interferenceFinCount < component.getFinCount()) { - throw new BugException("Counted " + interferenceFinCount + " parallel fins, " + - "when component itself has " + component.getFinCount() + - ", fin points=" + Arrays.toString(component.getFinPoints())); - } - } - -} diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java deleted file mode 100644 index cd7fea01..00000000 --- a/src/net/sf/openrocket/aerodynamics/barrowman/LaunchLugCalc.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.sf.openrocket.aerodynamics.barrowman; - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.MathUtil; - -public class LaunchLugCalc extends RocketComponentCalc { - - private double CDmul; - private double refArea; - - public LaunchLugCalc(RocketComponent component) { - super(component); - - LaunchLug lug = (LaunchLug)component; - double ld = lug.getLength() / (2*lug.getOuterRadius()); - - CDmul = Math.max(1.3 - ld, 1); - refArea = Math.PI * MathUtil.pow2(lug.getOuterRadius()) - - Math.PI * MathUtil.pow2(lug.getInnerRadius()) * Math.max(1 - ld, 0); - } - - @Override - public void calculateNonaxialForces(FlightConditions conditions, - AerodynamicForces forces, WarningSet warnings) { - // Nothing to be done - } - - @Override - public double calculatePressureDragForce(FlightConditions conditions, - double stagnationCD, double baseCD, WarningSet warnings) { - - return CDmul*stagnationCD * refArea / conditions.getRefArea(); - } - -} diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java deleted file mode 100644 index 71f4ef0c..00000000 --- a/src/net/sf/openrocket/aerodynamics/barrowman/RocketComponentCalc.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.sf.openrocket.aerodynamics.barrowman; - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; - -public abstract class RocketComponentCalc { - - public RocketComponentCalc(RocketComponent component) { - - } - - /** - * Calculate the non-axial forces produced by the component (normal and side forces, - * pitch, yaw and roll moments and CP position). The values are stored in the - * <code>AerodynamicForces</code> object. Additionally the value of CNa is computed - * and stored if possible without large amount of extra calculation, otherwise - * NaN is stored. The CP coordinate is stored in local coordinates and moments are - * computed around the local origin. - * - * @param conditions the flight conditions. - * @param forces the object in which to store the values. - * @param warnings set in which to store possible warnings. - */ - public abstract void calculateNonaxialForces(FlightConditions conditions, - AerodynamicForces forces, WarningSet warnings); - - - /** - * Calculates the pressure drag of the component. This component does NOT include - * the effect of discontinuities in the rocket body. - * - * @param conditions the flight conditions. - * @param stagnationCD the current stagnation drag coefficient - * @param baseCD the current base drag coefficient - * @param warnings set in which to store possible warnings - * @return the pressure drag of the component - */ - public abstract double calculatePressureDragForce(FlightConditions conditions, - double stagnationCD, double baseCD, WarningSet warnings); -} diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java deleted file mode 100644 index 23178a33..00000000 --- a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java +++ /dev/null @@ -1,440 +0,0 @@ -package net.sf.openrocket.aerodynamics.barrowman; - -import static net.sf.openrocket.models.atmosphere.AtmosphericConditions.GAMMA; -import static net.sf.openrocket.util.MathUtil.pow2; -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.BarrowmanCalculator; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.LinearInterpolator; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.PolyInterpolator; - - - -/** - * Calculates the aerodynamic properties of a <code>SymmetricComponent</code>. - * <p> - * CP and CNa are calculated by the Barrowman method extended to account for body lift - * by the method presented by Galejs. Supersonic CNa and CP are assumed to be the - * same as the subsonic values. - * - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SymmetricComponentCalc extends RocketComponentCalc { - - public static final double BODY_LIFT_K = 1.1; - - private final double length; - private final double foreRadius, aftRadius; - private final double fineness; - private final Transition.Shape shape; - private final double param; - private final double frontalArea; - private final double fullVolume; - private final double planformArea, planformCenter; - private final double sinphi; - - public SymmetricComponentCalc(RocketComponent c) { - super(c); - if (!(c instanceof SymmetricComponent)) { - throw new IllegalArgumentException("Illegal component type " + c); - } - SymmetricComponent component = (SymmetricComponent) c; - - - length = component.getLength(); - foreRadius = component.getForeRadius(); - aftRadius = component.getAftRadius(); - - fineness = length / (2 * Math.abs(aftRadius - foreRadius)); - fullVolume = component.getFullVolume(); - planformArea = component.getComponentPlanformArea(); - planformCenter = component.getComponentPlanformCenter(); - - if (component instanceof BodyTube) { - shape = null; - param = 0; - frontalArea = 0; - sinphi = 0; - } else if (component instanceof Transition) { - shape = ((Transition) component).getType(); - param = ((Transition) component).getShapeParameter(); - frontalArea = Math.abs(Math.PI * (foreRadius * foreRadius - aftRadius * aftRadius)); - - double r = component.getRadius(0.99 * length); - sinphi = (aftRadius - r) / MathUtil.hypot(aftRadius - r, 0.01 * length); - } else { - throw new UnsupportedOperationException("Unknown component type " + - component.getComponentName()); - } - } - - - private boolean isTube = false; - private double cnaCache = Double.NaN; - private double cpCache = Double.NaN; - - - /** - * Calculates the non-axial forces produced by the fins (normal and side forces, - * pitch, yaw and roll moments, CP position, CNa). - * <p> - * This method uses the Barrowman method for CP and CNa calculation and the - * extension presented by Galejs for the effect of body lift. - * <p> - * The CP and CNa at supersonic speeds are assumed to be the same as those at - * subsonic speeds. - */ - @Override - public void calculateNonaxialForces(FlightConditions conditions, - AerodynamicForces forces, WarningSet warnings) { - - // Pre-calculate and store the results - if (Double.isNaN(cnaCache)) { - final double r0 = foreRadius; - final double r1 = aftRadius; - - if (MathUtil.equals(r0, r1)) { - isTube = true; - cnaCache = 0; - } else { - isTube = false; - - final double A0 = Math.PI * pow2(r0); - final double A1 = Math.PI * pow2(r1); - - cnaCache = 2 * (A1 - A0); - // System.out.println("cnaCache = " + cnaCache); - cpCache = (length * A1 - fullVolume) / (A1 - A0); - } - } - - Coordinate cp; - - // If fore == aft, only body lift is encountered - if (isTube) { - cp = getLiftCP(conditions, warnings); - } else { - cp = new Coordinate(cpCache, 0, 0, cnaCache * conditions.getSincAOA() / - conditions.getRefArea()).average(getLiftCP(conditions, warnings)); - } - - forces.setCP(cp); - forces.setCNa(cp.weight); - forces.setCN(forces.getCNa() * conditions.getAOA()); - forces.setCm(forces.getCN() * cp.x / conditions.getRefLength()); - forces.setCroll(0); - forces.setCrollDamp(0); - forces.setCrollForce(0); - forces.setCside(0); - forces.setCyaw(0); - - - // Add warning on supersonic flight - if (conditions.getMach() > 1.1) { - warnings.add("Body calculations may not be entirely accurate at supersonic speeds."); - } - - } - - - - /** - * Calculate the body lift effect according to Galejs. - */ - protected Coordinate getLiftCP(FlightConditions conditions, WarningSet warnings) { - - /* - * Without this extra multiplier the rocket may become unstable at apogee - * when turning around, and begin oscillating horizontally. During the flight - * of the rocket this has no effect. It is effective only when AOA > 45 deg - * and the velocity is less than 15 m/s. - * - * TODO: MEDIUM: This causes an anomaly to the flight results with the CP jumping at apogee - */ - double mul = 1; - if ((conditions.getMach() < 0.05) && (conditions.getAOA() > Math.PI / 4)) { - mul = pow2(conditions.getMach() / 0.05); - } - - return new Coordinate(planformCenter, 0, 0, mul * BODY_LIFT_K * planformArea / conditions.getRefArea() * - conditions.getSinAOA() * conditions.getSincAOA()); // sin(aoa)^2 / aoa - } - - - - private LinearInterpolator interpolator = null; - - @Override - public double calculatePressureDragForce(FlightConditions conditions, - double stagnationCD, double baseCD, WarningSet warnings) { - - // Check for simple cases first - if (MathUtil.equals(foreRadius, aftRadius)) - return 0; - - if (length < 0.001) { - if (foreRadius < aftRadius) { - return stagnationCD * frontalArea / conditions.getRefArea(); - } else { - return baseCD * frontalArea / conditions.getRefArea(); - } - } - - - // Boattail drag computed directly from base drag - if (aftRadius < foreRadius) { - if (fineness >= 3) - return 0; - double cd = baseCD * frontalArea / conditions.getRefArea(); - if (fineness <= 1) - return cd; - return cd * (3 - fineness) / 2; - } - - - // All nose cones and shoulders from pre-calculated and interpolating - if (interpolator == null) { - calculateNoseInterpolator(); - } - - return interpolator.getValue(conditions.getMach()) * frontalArea / conditions.getRefArea(); - } - - - - /* - * Experimental values of pressure drag for different nose cone shapes with a fineness - * ratio of 3. The data is taken from 'Collection of Zero-Lift Drag Data on Bodies - * of Revolution from Free-Flight Investigations', NASA TR-R-100, NTRS 19630004995, - * page 16. - * - * This data is extrapolated for other fineness ratios. - */ - - private static final LinearInterpolator ellipsoidInterpolator = new LinearInterpolator( - new double[] { 1.2, 1.25, 1.3, 1.4, 1.6, 2.0, 2.4 }, - new double[] { 0.110, 0.128, 0.140, 0.148, 0.152, 0.159, 0.162 /* constant */} - ); - private static final LinearInterpolator x14Interpolator = new LinearInterpolator( - new double[] { 1.2, 1.3, 1.4, 1.6, 1.8, 2.2, 2.6, 3.0, 3.6 }, - new double[] { 0.140, 0.156, 0.169, 0.192, 0.206, 0.227, 0.241, 0.249, 0.252 } - ); - private static final LinearInterpolator x12Interpolator = new LinearInterpolator( - new double[] { 0.925, 0.95, 1.0, 1.05, 1.1, 1.2, 1.3, 1.7, 2.0 }, - new double[] { 0, 0.014, 0.050, 0.060, 0.059, 0.081, 0.084, 0.085, 0.078 } - ); - private static final LinearInterpolator x34Interpolator = new LinearInterpolator( - new double[] { 0.8, 0.9, 1.0, 1.06, 1.2, 1.4, 1.6, 2.0, 2.8, 3.4 }, - new double[] { 0, 0.015, 0.078, 0.121, 0.110, 0.098, 0.090, 0.084, 0.078, 0.074 } - ); - private static final LinearInterpolator vonKarmanInterpolator = new LinearInterpolator( - new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.6, 2.0, 3.0 }, - new double[] { 0, 0.010, 0.027, 0.055, 0.070, 0.081, 0.095, 0.097, 0.091, 0.083 } - ); - private static final LinearInterpolator lvHaackInterpolator = new LinearInterpolator( - new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.6, 2.0 }, - new double[] { 0, 0.010, 0.024, 0.066, 0.084, 0.100, 0.114, 0.117, 0.113 } - ); - private static final LinearInterpolator parabolicInterpolator = new LinearInterpolator( - new double[] { 0.95, 0.975, 1.0, 1.05, 1.1, 1.2, 1.4, 1.7 }, - new double[] { 0, 0.016, 0.041, 0.092, 0.109, 0.119, 0.113, 0.108 } - ); - private static final LinearInterpolator parabolic12Interpolator = new LinearInterpolator( - new double[] { 0.8, 0.9, 0.95, 1.0, 1.05, 1.1, 1.3, 1.5, 1.8 }, - new double[] { 0, 0.016, 0.042, 0.100, 0.126, 0.125, 0.100, 0.090, 0.088 } - ); - private static final LinearInterpolator parabolic34Interpolator = new LinearInterpolator( - new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.7 }, - new double[] { 0, 0.023, 0.073, 0.098, 0.107, 0.106, 0.089, 0.082 } - ); - private static final LinearInterpolator bluntInterpolator = new LinearInterpolator(); - static { - for (double m = 0; m < 3; m += 0.05) - bluntInterpolator.addPoint(m, BarrowmanCalculator.calculateStagnationCD(m)); - } - - /** - * Calculate the LinearInterpolator 'interpolator'. After this call, if can be used - * to get the pressure drag coefficient at any Mach number. - * - * First, the transonic/supersonic region is computed. For conical and ogive shapes - * this is calculated directly. For other shapes, the values for fineness-ratio 3 - * transitions are taken from the experimental values stored above (for parameterized - * shapes the values are interpolated between the parameter values). These are then - * extrapolated to the current fineness ratio. - * - * Finally, if the first data points in the interpolator are not zero, the subsonic - * region is interpolated in the form Cd = a*M^b + Cd(M=0). - */ - @SuppressWarnings("null") - private void calculateNoseInterpolator() { - LinearInterpolator int1 = null, int2 = null; - double p = 0; - - interpolator = new LinearInterpolator(); - - - /* - * Take into account nose cone shape. Conical and ogive generate the interpolator - * directly. Others store a interpolator for fineness ratio 3 into int1, or - * for parameterized shapes store the bounding fineness ratio 3 interpolators into - * int1 and int2 and set 0 <= p <= 1 according to the bounds. - */ - switch (shape) { - case CONICAL: - interpolator = calculateOgiveNoseInterpolator(0, sinphi); // param==0 -> conical - break; - - case OGIVE: - interpolator = calculateOgiveNoseInterpolator(param, sinphi); - break; - - case ELLIPSOID: - int1 = ellipsoidInterpolator; - break; - - case POWER: - if (param <= 0.25) { - int1 = bluntInterpolator; - int2 = x14Interpolator; - p = param * 4; - } else if (param <= 0.5) { - int1 = x14Interpolator; - int2 = x12Interpolator; - p = (param - 0.25) * 4; - } else if (param <= 0.75) { - int1 = x12Interpolator; - int2 = x34Interpolator; - p = (param - 0.5) * 4; - } else { - int1 = x34Interpolator; - int2 = calculateOgiveNoseInterpolator(0, 1 / MathUtil.safeSqrt(1 + 4 * pow2(fineness))); - p = (param - 0.75) * 4; - } - break; - - case PARABOLIC: - if (param <= 0.5) { - int1 = calculateOgiveNoseInterpolator(0, 1 / MathUtil.safeSqrt(1 + 4 * pow2(fineness))); - int2 = parabolic12Interpolator; - p = param * 2; - } else if (param <= 0.75) { - int1 = parabolic12Interpolator; - int2 = parabolic34Interpolator; - p = (param - 0.5) * 4; - } else { - int1 = parabolic34Interpolator; - int2 = parabolicInterpolator; - p = (param - 0.75) * 4; - } - break; - - case HAACK: - int1 = vonKarmanInterpolator; - int2 = lvHaackInterpolator; - p = param * 3; - break; - - default: - throw new UnsupportedOperationException("Unknown transition shape: " + shape); - } - - if (p < 0 || p > 1.00001) { - throw new BugException("Inconsistent parameter value p=" + p + " shape=" + shape); - } - - - // Check for parameterized shape and interpolate if necessary - if (int2 != null) { - LinearInterpolator int3 = new LinearInterpolator(); - for (double m : int1.getXPoints()) { - int3.addPoint(m, p * int2.getValue(m) + (1 - p) * int1.getValue(m)); - } - for (double m : int2.getXPoints()) { - int3.addPoint(m, p * int2.getValue(m) + (1 - p) * int1.getValue(m)); - } - int1 = int3; - } - - // Extrapolate for fineness ratio if necessary - if (int1 != null) { - double log4 = Math.log(fineness + 1) / Math.log(4); - for (double m : int1.getXPoints()) { - double stag = bluntInterpolator.getValue(m); - interpolator.addPoint(m, stag * Math.pow(int1.getValue(m) / stag, log4)); - } - } - - - /* - * Now the transonic/supersonic region is ok. We still need to interpolate - * the subsonic region, if the values are non-zero. - */ - - double min = interpolator.getXPoints()[0]; - double minValue = interpolator.getValue(min); - if (minValue < 0.001) { - // No interpolation necessary - return; - } - - double cdMach0 = 0.8 * pow2(sinphi); - double minDeriv = (interpolator.getValue(min + 0.01) - minValue) / 0.01; - - // These should not occur, but might cause havoc for the interpolation - if ((cdMach0 >= minValue - 0.01) || (minDeriv <= 0.01)) { - return; - } - - // Cd = a*M^b + cdMach0 - double a = minValue - cdMach0; - double b = minDeriv / a; - - for (double m = 0; m < minValue; m += 0.05) { - interpolator.addPoint(m, a * Math.pow(m, b) + cdMach0); - } - } - - - private static final PolyInterpolator conicalPolyInterpolator = - new PolyInterpolator(new double[] { 1.0, 1.3 }, new double[] { 1.0, 1.3 }); - - private static LinearInterpolator calculateOgiveNoseInterpolator(double param, - double sinphi) { - LinearInterpolator interpolator = new LinearInterpolator(); - - // In the range M = 1 ... 1.3 use polynomial approximation - double cdMach1 = 2.1 * pow2(sinphi) + 0.6019 * sinphi; - - double[] poly = conicalPolyInterpolator.interpolator( - 1.0 * sinphi, cdMach1, - 4 / (GAMMA + 1) * (1 - 0.5 * cdMach1), -1.1341 * sinphi - ); - - // Shape parameter multiplier - double mul = 0.72 * pow2(param - 0.5) + 0.82; - - for (double m = 1; m < 1.3001; m += 0.02) { - interpolator.addPoint(m, mul * PolyInterpolator.eval(m, poly)); - } - - // Above M = 1.3 use direct formula - for (double m = 1.32; m < 4; m += 0.02) { - interpolator.addPoint(m, mul * (2.1 * pow2(sinphi) + 0.5 * sinphi / MathUtil.safeSqrt(m * m - 1))); - } - - return interpolator; - } - - - -} diff --git a/src/net/sf/openrocket/arch/SystemInfo.java b/src/net/sf/openrocket/arch/SystemInfo.java deleted file mode 100644 index 7b526f2c..00000000 --- a/src/net/sf/openrocket/arch/SystemInfo.java +++ /dev/null @@ -1,83 +0,0 @@ -package net.sf.openrocket.arch; - -import java.io.File; - -import net.sf.openrocket.util.BugException; - -public class SystemInfo { - - - /** - * Enumeration of supported operating systems. - * - * @see <a href="http://lopica.sourceforge.net/os.html">JNLP os and arch Value Collection</a> - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public enum Platform { - WINDOWS, - MAC_OS, - UNIX; - } - - - /** - * Return the current operating system. - * - * @return the operating system of the current system. - */ - public static Platform getPlatform() { - String os = System.getProperty("os.name").toLowerCase(); - - if (os.indexOf("win") >= 0) { - return Platform.WINDOWS; - } else if (os.indexOf("mac") >= 0) { - return Platform.MAC_OS; - } else { - /* - * Assume UNIX otherwise, e.g. "Linux", "Solaris", "AIX" etc. - */ - return Platform.UNIX; - } - } - - - - - /** - * Return the application data directory of this user. The location depends - * on the current platform. - * <p> - * The directory will not be created by this method. - * - * @return the application directory for OpenRocket - */ - public static File getUserApplicationDirectory() { - final String homeDir = System.getProperty("user.home"); - final File dir; - - switch (getPlatform()) { - case WINDOWS: - String appdata = System.getenv("APPDATA"); - if (appdata != null) { - dir = new File(appdata, "OpenRocket/"); - } else { - dir = new File(homeDir, "OpenRocket/"); - } - break; - - case MAC_OS: - dir = new File(homeDir, "Library/Application Support/OpenRocket/"); - break; - - case UNIX: - dir = new File(homeDir, ".openrocket/"); - break; - - default: - throw new BugException("Not implemented for platform " + getPlatform()); - } - - return dir; - } - -} diff --git a/src/net/sf/openrocket/communication/BugReporter.java b/src/net/sf/openrocket/communication/BugReporter.java deleted file mode 100644 index 7f081ba9..00000000 --- a/src/net/sf/openrocket/communication/BugReporter.java +++ /dev/null @@ -1,58 +0,0 @@ -package net.sf.openrocket.communication; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.net.HttpURLConnection; - -import net.sf.openrocket.util.BuildProperties; - -public class BugReporter extends Communicator { - - // Inhibit instantiation - private BugReporter() { - } - - - /** - * Send the provided report to the OpenRocket bug report URL. If the connection - * fails or the server does not respond with the correct response code, an - * exception is thrown. - * - * @param report the report to send. - * @throws IOException if an error occurs while connecting to the server or - * the server responds with a wrong response code. - */ - public static void sendBugReport(String report) throws IOException { - - HttpURLConnection connection = connectionSource.getConnection(BUG_REPORT_URL); - - connection.setConnectTimeout(CONNECTION_TIMEOUT); - connection.setInstanceFollowRedirects(true); - connection.setRequestMethod("POST"); - connection.setUseCaches(false); - connection.setRequestProperty("X-OpenRocket-Version", encode(BuildProperties.getVersion())); - - String post; - post = (VERSION_PARAM + "=" + encode(BuildProperties.getVersion()) - + "&" + BUG_REPORT_PARAM + "=" + encode(report)); - - OutputStreamWriter wr = null; - try { - // Send post information - connection.setDoOutput(true); - wr = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); - wr.write(post); - wr.flush(); - - if (connection.getResponseCode() != BUG_REPORT_RESPONSE_CODE) { - throw new IOException("Server responded with code " + - connection.getResponseCode() + ", expecting " + BUG_REPORT_RESPONSE_CODE); - } - } finally { - if (wr != null) - wr.close(); - connection.disconnect(); - } - } - -} diff --git a/src/net/sf/openrocket/communication/Communicator.java b/src/net/sf/openrocket/communication/Communicator.java deleted file mode 100644 index 06c666b7..00000000 --- a/src/net/sf/openrocket/communication/Communicator.java +++ /dev/null @@ -1,74 +0,0 @@ -package net.sf.openrocket.communication; - -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URLEncoder; - -import net.sf.openrocket.util.BugException; - -public abstract class Communicator { - - protected static final String BUG_REPORT_URL; - - protected static final String UPDATE_INFO_URL; - - static { - String url; - url = System.getProperty("openrocket.debug.bugurl"); - if (url == null) - url = "http://openrocket.sourceforge.net/actions/reportbug"; - BUG_REPORT_URL = url; - - url = System.getProperty("openrocket.debug.updateurl"); - if (url == null) - url = "http://openrocket.sourceforge.net/actions/updates"; - UPDATE_INFO_URL = url; - } - - - protected static final String VERSION_PARAM = "version"; - - - protected static final String BUG_REPORT_PARAM = "content"; - protected static final int BUG_REPORT_RESPONSE_CODE = HttpURLConnection.HTTP_ACCEPTED; - protected static final int CONNECTION_TIMEOUT = 10000; // in milliseconds - - protected static final int UPDATE_INFO_UPDATE_AVAILABLE = HttpURLConnection.HTTP_OK; - protected static final int UPDATE_INFO_NO_UPDATE_CODE = HttpURLConnection.HTTP_NO_CONTENT; - protected static final String UPDATE_INFO_CONTENT_TYPE = "text/plain"; - - // Limit the number of bytes that can be read from the server - protected static final int MAX_INPUT_BYTES = 20000; - - - protected static ConnectionSource connectionSource = new DefaultConnectionSource(); - - - /** - * Set the source of the network connections. This can be used for unit testing. - * By default the source is a DefaultConnectionSource. - * - * @param source the source of the connections. - */ - public static void setConnectionSource(ConnectionSource source) { - connectionSource = source; - } - - - /** - * URL-encode the specified string in UTF-8 encoding. - * - * @param str the string to encode (null ok) - * @return the encoded string or "null" - */ - public static String encode(String str) { - if (str == null) - return "null"; - try { - return URLEncoder.encode(str, "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new BugException("Unsupported encoding UTF-8", e); - } - } - -} diff --git a/src/net/sf/openrocket/communication/ConnectionSource.java b/src/net/sf/openrocket/communication/ConnectionSource.java deleted file mode 100644 index b5b0fdcc..00000000 --- a/src/net/sf/openrocket/communication/ConnectionSource.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.sf.openrocket.communication; - -import java.io.IOException; -import java.net.HttpURLConnection; - -/** - * A source for network connections. This interface exists to enable unit testing. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface ConnectionSource { - - /** - * Return a connection to the specified url. - * @param url the URL to connect to. - * @return the corresponding HttpURLConnection - * @throws IOException if an IOException occurs - */ - public HttpURLConnection getConnection(String url) throws IOException; - -} diff --git a/src/net/sf/openrocket/communication/DefaultConnectionSource.java b/src/net/sf/openrocket/communication/DefaultConnectionSource.java deleted file mode 100644 index 501c37ac..00000000 --- a/src/net/sf/openrocket/communication/DefaultConnectionSource.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.sf.openrocket.communication; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; - -/** - * Default implementation of ConnectionSource, which simply opens a new - * HttpURLConnection from a URL object. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DefaultConnectionSource implements ConnectionSource { - - @Override - public HttpURLConnection getConnection(String urlString) throws IOException { - URL url = new URL(urlString); - return (HttpURLConnection) url.openConnection(); - } - -} diff --git a/src/net/sf/openrocket/communication/UpdateInfo.java b/src/net/sf/openrocket/communication/UpdateInfo.java deleted file mode 100644 index 78458b81..00000000 --- a/src/net/sf/openrocket/communication/UpdateInfo.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.sf.openrocket.communication; - -import java.util.List; - -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.ComparablePair; - -public class UpdateInfo { - - private final String latestVersion; - - private final ArrayList<ComparablePair<Integer, String>> updates; - - - public UpdateInfo() { - this.latestVersion = BuildProperties.getVersion(); - this.updates = new ArrayList<ComparablePair<Integer, String>>(); - } - - public UpdateInfo(String version, List<ComparablePair<Integer, String>> updates) { - this.latestVersion = version; - this.updates = new ArrayList<ComparablePair<Integer, String>>(updates); - } - - - - /** - * Get the latest OpenRocket version. If it is the current version, then the value - * of {@link BuildProperties#getVersion()} is returned. - * - * @return the latest OpenRocket version. - */ - public String getLatestVersion() { - return latestVersion; - } - - - /** - * Return a list of the new features/updates that are available. The list has a - * priority for each update and a message text. The returned list may be modified. - * - * @return a modifiable list of the updates. - */ - public List<ComparablePair<Integer, String>> getUpdates() { - return updates.clone(); - } - - @Override - public String toString() { - return "UpdateInfo[version=" + latestVersion + "; updates=" + updates.toString() + "]"; - } - -} diff --git a/src/net/sf/openrocket/communication/UpdateInfoRetriever.java b/src/net/sf/openrocket/communication/UpdateInfoRetriever.java deleted file mode 100644 index 3122f085..00000000 --- a/src/net/sf/openrocket/communication/UpdateInfoRetriever.java +++ /dev/null @@ -1,240 +0,0 @@ -package net.sf.openrocket.communication; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.HttpURLConnection; -import java.util.ArrayList; -import java.util.Locale; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.ComparablePair; -import net.sf.openrocket.util.LimitedInputStream; - -public class UpdateInfoRetriever { - private static final LogHelper log = Application.getLogger(); - - private UpdateInfoFetcher fetcher = null; - - - /** - * Start an asynchronous task that will fetch information about the latest - * OpenRocket version. This will overwrite any previous fetching operation. - * This call will return immediately. - */ - public void start() { - fetcher = new UpdateInfoFetcher(); - fetcher.setDaemon(true); - fetcher.start(); - } - - - /** - * Check whether the update info fetching is still in progress. - * - * @return <code>true</code> if the communication is still in progress. - */ - public boolean isRunning() { - if (fetcher == null) { - throw new IllegalStateException("startFetchUpdateInfo() has not been called"); - } - return fetcher.isAlive(); - } - - - /** - * Retrieve the result of the background update info fetcher. This method returns - * the result of the previous call to {@link #start()}. It must be - * called before calling this method. - * <p> - * This method will return <code>null</code> if the info fetcher is still running or - * if it encountered a problem in communicating with the server. The difference can - * be checked using {@link #isRunning()}. - * - * @return the update result, or <code>null</code> if the fetching is still in progress - * or an error occurred while communicating with the server. - * @throws IllegalStateException if {@link #start()} has not been called. - */ - public UpdateInfo getUpdateInfo() { - if (fetcher == null) { - throw new IllegalStateException("start() has not been called"); - } - return fetcher.info; - } - - - - /** - * Parse the data received from the server. - * - * @param r the Reader from which to read. - * @return an UpdateInfo construct, or <code>null</code> if the data was invalid. - * @throws IOException if an I/O exception occurs. - */ - /* package-private */ - static UpdateInfo parseUpdateInput(Reader r) throws IOException { - BufferedReader reader; - if (r instanceof BufferedReader) { - reader = (BufferedReader) r; - } else { - reader = new BufferedReader(r); - } - - - String version = null; - ArrayList<ComparablePair<Integer, String>> updates = - new ArrayList<ComparablePair<Integer, String>>(); - - String str = reader.readLine(); - while (str != null) { - if (str.matches("^Version: *[0-9]+\\.[0-9]+\\.[0-9]+[a-zA-Z0-9.-]* *$")) { - version = str.substring(8).trim(); - } else if (str.matches("^[0-9]+:\\p{Print}+$")) { - int index = str.indexOf(':'); - int value = Integer.parseInt(str.substring(0, index)); - String desc = str.substring(index + 1).trim(); - if (!desc.equals("")) { - updates.add(new ComparablePair<Integer, String>(value, desc)); - } - } - // Ignore anything else - str = reader.readLine(); - } - - if (version != null) { - return new UpdateInfo(version, updates); - } else { - return null; - } - } - - - - /** - * An asynchronous task that fetches and parses the update info. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - private class UpdateInfoFetcher extends Thread { - - private volatile UpdateInfo info = null; - - @Override - public void run() { - try { - doConnection(); - } catch (IOException e) { - log.info("Fetching update failed: " + e); - return; - } - } - - - private void doConnection() throws IOException { - String url = Communicator.UPDATE_INFO_URL + "?" + Communicator.VERSION_PARAM + "=" - + Communicator.encode(BuildProperties.getVersion()); - - HttpURLConnection connection = Communicator.connectionSource.getConnection(url); - - connection.setConnectTimeout(Communicator.CONNECTION_TIMEOUT); - connection.setInstanceFollowRedirects(true); - connection.setRequestMethod("GET"); - connection.setUseCaches(false); - connection.setDoInput(true); - connection.setRequestProperty("X-OpenRocket-Version", - Communicator.encode(BuildProperties.getVersion() + " " + BuildProperties.getBuildSource())); - connection.setRequestProperty("X-OpenRocket-ID", - Communicator.encode(Application.getPreferences().getUniqueID())); - connection.setRequestProperty("X-OpenRocket-OS", - Communicator.encode(System.getProperty("os.name") + " " + - System.getProperty("os.arch"))); - connection.setRequestProperty("X-OpenRocket-Java", - Communicator.encode(System.getProperty("java.vendor") + " " + - System.getProperty("java.version"))); - connection.setRequestProperty("X-OpenRocket-Country", - Communicator.encode(System.getProperty("user.country") + " " + - System.getProperty("user.timezone"))); - connection.setRequestProperty("X-OpenRocket-Locale", - Communicator.encode(Locale.getDefault().toString())); - connection.setRequestProperty("X-OpenRocket-CPUs", "" + Runtime.getRuntime().availableProcessors()); - - InputStream is = null; - try { - connection.connect(); - - log.debug("Update response code: " + connection.getResponseCode()); - - if (connection.getResponseCode() == Communicator.UPDATE_INFO_NO_UPDATE_CODE) { - // No updates are available - log.info("No updates available"); - info = new UpdateInfo(); - return; - } - - if (connection.getResponseCode() != Communicator.UPDATE_INFO_UPDATE_AVAILABLE) { - // Error communicating with server - log.warn("Unknown server response code: " + connection.getResponseCode()); - return; - } - - String contentType = connection.getContentType(); - if (contentType == null || - contentType.toLowerCase().indexOf(Communicator.UPDATE_INFO_CONTENT_TYPE) < 0) { - // Unknown response type - log.warn("Unknown Content-type received:" + contentType); - return; - } - - // Update is available, parse input - is = connection.getInputStream(); - is = new LimitedInputStream(is, Communicator.MAX_INPUT_BYTES); - String encoding = connection.getContentEncoding(); - if (encoding == null || encoding.equals("")) - encoding = "UTF-8"; - BufferedReader reader = new BufferedReader(new InputStreamReader(is, encoding)); - - String version = null; - ArrayList<ComparablePair<Integer, String>> updates = - new ArrayList<ComparablePair<Integer, String>>(); - - String line = reader.readLine(); - while (line != null) { - - if (line.matches("^Version:[a-zA-Z0-9._ -]{1,30}$")) { - version = line.substring(8).trim(); - } else if (line.matches("^[0-9]{1,9}:\\P{Cntrl}{1,300}$")) { - String[] split = line.split(":", 2); - int n = Integer.parseInt(split[0]); - updates.add(new ComparablePair<Integer, String>(n, split[1].trim())); - } - // Ignore line otherwise - line = reader.readLine(); - } - - // Check version input - if (version == null || version.length() == 0 || - version.equalsIgnoreCase(BuildProperties.getVersion())) { - // Invalid response - log.warn("Invalid version received, ignoring."); - return; - } - - - info = new UpdateInfo(version, updates); - log.info("Found update: " + info); - } finally { - try { - if (is != null) - is.close(); - connection.disconnect(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } -} diff --git a/src/net/sf/openrocket/database/Database.java b/src/net/sf/openrocket/database/Database.java deleted file mode 100644 index 37636304..00000000 --- a/src/net/sf/openrocket/database/Database.java +++ /dev/null @@ -1,243 +0,0 @@ -package net.sf.openrocket.database; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.io.InputStream; -import java.util.AbstractSet; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import net.sf.openrocket.file.Loader; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.JarUtil; - - - -/** - * A database set. This class functions as a <code>Set</code> that contains items - * of a specific type. Additionally, the items can be accessed via an index number. - * The elements are always kept in their natural order. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Database<T extends Comparable<T>> extends AbstractSet<T> { - private static final LogHelper log = Application.getLogger(); - - private final List<T> list = new ArrayList<T>(); - private final ArrayList<DatabaseListener<T>> listeners = - new ArrayList<DatabaseListener<T>>(); - private final Loader<T> loader; - - - public Database() { - loader = null; - } - - public Database(Loader<T> loader) { - this.loader = loader; - } - - - - @Override - public Iterator<T> iterator() { - return new DBIterator(); - } - - @Override - public int size() { - return list.size(); - } - - @Override - public boolean add(T element) { - int index; - - index = Collections.binarySearch(list, element); - if (index >= 0) { - // List might contain the element - if (list.contains(element)) { - return false; - } - } else { - index = -(index + 1); - } - list.add(index, element); - fireAddEvent(element); - return true; - } - - - /** - * Get the element with the specified index. - * @param index the index to retrieve. - * @return the element at the index. - */ - public T get(int index) { - return list.get(index); - } - - /** - * Return the index of the given <code>Motor</code>, or -1 if not in the database. - * - * @param m the motor - * @return the index of the motor - */ - public int indexOf(T m) { - return list.indexOf(m); - } - - - public void addDatabaseListener(DatabaseListener<T> listener) { - listeners.add(listener); - } - - public void removeChangeListener(DatabaseListener<T> listener) { - listeners.remove(listener); - } - - - - @SuppressWarnings("unchecked") - protected void fireAddEvent(T element) { - Object[] array = listeners.toArray(); - for (Object l : array) { - ((DatabaseListener<T>) l).elementAdded(element, this); - } - } - - @SuppressWarnings("unchecked") - protected void fireRemoveEvent(T element) { - Object[] array = listeners.toArray(); - for (Object l : array) { - ((DatabaseListener<T>) l).elementRemoved(element, this); - } - } - - - - //////// Directory loading - - - - /** - * Load all files in a directory to the motor database. Only files with file - * names matching the given pattern (as matched by <code>String.matches(String)</code>) - * are processed. - * - * @param dir the directory to read. - * @param pattern the pattern to match the file names to. - * @throws IOException if an IO error occurs when reading the JAR archive - * (errors reading individual files are printed to stderr). - */ - public void loadDirectory(File dir, final String pattern) throws IOException { - if (loader == null) { - throw new IllegalStateException("no file loader set"); - } - - File[] files = dir.listFiles(new FilenameFilter() { - @Override - public boolean accept(File directory, String name) { - return name.matches(pattern); - } - }); - if (files == null) { - throw new IOException("not a directory: " + dir); - } - for (File file : files) { - try { - this.addAll(loader.load(new FileInputStream(file), file.getName())); - } catch (IOException e) { - log.warn("Error loading file " + file + ": " + e.getMessage(), e); - } - } - } - - - /** - * Read all files in a directory contained in the JAR file that this class belongs to. - * Only files whose names match the given pattern (as matched by - * <code>String.matches(String)</code>) will be read. - * - * @param dir the directory within the JAR archive to read. - * @param pattern the pattern to match the file names to. - * @throws IOException if an IO error occurs when reading the JAR archive - * (errors reading individual files are printed to stderr). - */ - public void loadJarDirectory(String dir, String pattern) throws IOException { - - // Process directory and extension - if (!dir.endsWith("/")) { - dir += "/"; - } - - // Find and open the jar file this class is contained in - File file = JarUtil.getCurrentJarFile(); - JarFile jarFile = new JarFile(file); - - try { - - // Loop through JAR entries searching for files to load - Enumeration<JarEntry> entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - String name = entry.getName(); - if (name.startsWith(dir) && name.matches(pattern)) { - try { - InputStream stream = jarFile.getInputStream(entry); - this.addAll(loader.load(stream, name)); - } catch (IOException e) { - log.warn("Error loading file " + file + ": " + e.getMessage(), e); - } - } - } - - } finally { - jarFile.close(); - } - } - - - - public void load(File file) throws IOException { - if (loader == null) { - throw new IllegalStateException("no file loader set"); - } - this.addAll(loader.load(new FileInputStream(file), file.getName())); - } - - - - /** - * Iterator class implementation that fires changes if remove() is called. - */ - private class DBIterator implements Iterator<T> { - private Iterator<T> iterator = list.iterator(); - private T current = null; - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public T next() { - current = iterator.next(); - return current; - } - - @Override - public void remove() { - iterator.remove(); - fireRemoveEvent(current); - } - } -} diff --git a/src/net/sf/openrocket/database/DatabaseListener.java b/src/net/sf/openrocket/database/DatabaseListener.java deleted file mode 100644 index a24b2dde..00000000 --- a/src/net/sf/openrocket/database/DatabaseListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.sf.openrocket.database; - -public interface DatabaseListener<T extends Comparable<T>> { - - public void elementAdded(T element, Database<T> source); - - public void elementRemoved(T element, Database<T> source); - -} diff --git a/src/net/sf/openrocket/database/Databases.java b/src/net/sf/openrocket/database/Databases.java deleted file mode 100644 index d8a132b0..00000000 --- a/src/net/sf/openrocket/database/Databases.java +++ /dev/null @@ -1,202 +0,0 @@ -package net.sf.openrocket.database; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.material.MaterialStorage; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; - - -/** - * A class that contains single instances of {@link Database} for specific purposes. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Databases { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - /* Static implementations of specific databases: */ - - /** - * A database of bulk materials (with bulk densities). - */ - public static final Database<Material> BULK_MATERIAL = new Database<Material>(); - /** - * A database of surface materials (with surface densities). - */ - public static final Database<Material> SURFACE_MATERIAL = new Database<Material>(); - /** - * A database of linear material (with length densities). - */ - public static final Database<Material> LINE_MATERIAL = new Database<Material>(); - - - - static { - - // Add default materials - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Acrylic"), 1190, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Balsa"), 170, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Basswood"), 500, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Birch"), 670, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Cardboard"), 680, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Carbonfiber"), 1780, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Cork"), 240, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.DepronXPS"), 40, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Fiberglass"), 1850, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Kraftphenolic"), 950, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Maple"), 755, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Paperoffice"), 820, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Pine"), 530, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Plywoodbirch"), 630, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.PolycarbonateLexan"), 1200, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Polystyrene"), 1050, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.PVC"), 1390, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Spruce"), 450, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.StyrofoamgenericEPS"), 20, false)); - // BULK_MATERIAL.add(new Material.Bulk("Styrofoam (Blue foam, XPS)", 32, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.StyrofoamBluefoamXPS"), 32, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.Quantumtubing"), 1050, false)); - BULK_MATERIAL.add(new Material.Bulk(trans.get("Databases.materials.BlueTube"), 1300, false)); - - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Ripstopnylon"), 0.067, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Mylar"), 0.021, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Polyethylenethin"), 0.015, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Polyethyleneheavy"), 0.040, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Silk"), 0.060, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Paperoffice"), 0.080, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Cellophane"), 0.018, false)); - SURFACE_MATERIAL.add(new Material.Surface(trans.get("Databases.materials.Crepepaper"), 0.025, false)); - - //// Thread (heavy-duty) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Threadheavy-duty"), 0.0003, false)); - //// Elastic cord (round 2mm, 1/16 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordround2mm"), 0.0018, false)); - //// Elastic cord (flat 6mm, 1/4 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat6mm"), 0.0043, false)); - //// Elastic cord (flat 12mm, 1/2 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat12mm"), 0.008, false)); - //// Elastic cord (flat 19mm, 3/4 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat19mm"), 0.0012, false)); - //// Elastic cord (flat 25mm, 1 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Elasticcordflat25mm"), 0.0016, false)); - //// Braided nylon (2 mm, 1/16 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Braidednylon2mm"), 0.001, false)); - //// Braided nylon (3 mm, 1/8 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Braidednylon3mm"), 0.0035, false)); - //// Tubular nylon (11 mm, 7/16 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Tubularnylon11mm"), 0.013, false)); - //// Tubular nylon (14 mm, 9/16 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Tubularnylon14mm"), 0.016, false)); - //// Tubular nylon (25 mm, 1 in) - LINE_MATERIAL.add(new Material.Line(trans.get("Databases.materials.Tubularnylon25mm"), 0.029, false)); - - - // Add user-defined materials - for (Material m : Application.getPreferences().getUserMaterials()) { - switch (m.getType()) { - case LINE: - LINE_MATERIAL.add(m); - break; - - case SURFACE: - SURFACE_MATERIAL.add(m); - break; - - case BULK: - BULK_MATERIAL.add(m); - break; - - default: - log.warn("ERROR: Unknown material type " + m); - } - } - - // Add database storage listener - MaterialStorage listener = new MaterialStorage(); - LINE_MATERIAL.addDatabaseListener(listener); - SURFACE_MATERIAL.addDatabaseListener(listener); - BULK_MATERIAL.addDatabaseListener(listener); - } - - - /* - * Used just for ensuring initialization of the class. - */ - public static void fakeMethod() { - - } - - - /** - * Find a material from the database with the specified type and name. Returns - * <code>null</code> if the specified material could not be found. - * - * @param type the material type. - * @param name the material name in the database. - * @return the material, or <code>null</code> if not found. - */ - public static Material findMaterial(Material.Type type, String name) { - Database<Material> db; - switch (type) { - case BULK: - db = BULK_MATERIAL; - break; - case SURFACE: - db = SURFACE_MATERIAL; - break; - case LINE: - db = LINE_MATERIAL; - break; - default: - throw new IllegalArgumentException("Illegal material type: " + type); - } - - for (Material m : db) { - if (m.getName().equalsIgnoreCase(name)) { - return m; - } - } - return null; - } - - - /** - * Find a material from the database or return a new material if the specified - * material with the specified density is not found. - * - * @param type the material type. - * @param name the material name. - * @param density the density of the material. - * @param userDefined whether a newly created material should be user-defined. - * @return the material object from the database or a new material. - */ - public static Material findMaterial(Material.Type type, String name, double density, - boolean userDefined) { - Database<Material> db; - switch (type) { - case BULK: - db = BULK_MATERIAL; - break; - case SURFACE: - db = SURFACE_MATERIAL; - break; - case LINE: - db = LINE_MATERIAL; - break; - default: - throw new IllegalArgumentException("Illegal material type: " + type); - } - - for (Material m : db) { - if (m.getName().equalsIgnoreCase(name) && MathUtil.equals(m.getDensity(), density)) { - return m; - } - } - return Material.newMaterial(type, name, density, userDefined); - } - - -} diff --git a/src/net/sf/openrocket/database/MotorDatabase.java b/src/net/sf/openrocket/database/MotorDatabase.java deleted file mode 100644 index fdde5fac..00000000 --- a/src/net/sf/openrocket/database/MotorDatabase.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.sf.openrocket.database; - -import java.util.List; - -import net.sf.openrocket.motor.Motor; - -public interface MotorDatabase { - - /** - * Return all motors in the database matching a search criteria. Any search criteria that - * is null or NaN is ignored. - * - * @param type the motor type, or null. - * @param manufacturer the manufacturer, or null. - * @param designation the designation, or null. - * @param diameter the diameter, or NaN. - * @param length the length, or NaN. - * @return a list of all the matching motors. - */ - public List<? extends Motor> findMotors(Motor.Type type, - String manufacturer, String designation, double diameter, - double length); - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/database/ThrustCurveMotorSet.java b/src/net/sf/openrocket/database/ThrustCurveMotorSet.java deleted file mode 100644 index 99c422e3..00000000 --- a/src/net/sf/openrocket/database/ThrustCurveMotorSet.java +++ /dev/null @@ -1,305 +0,0 @@ -package net.sf.openrocket.database; - -import java.text.Collator; -import java.util.Collections; -import java.util.Comparator; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.sf.openrocket.motor.DesignationComparator; -import net.sf.openrocket.motor.Manufacturer; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.Motor.Type; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.MathUtil; - -public class ThrustCurveMotorSet implements Comparable<ThrustCurveMotorSet> { - - // Comparators: - private static final Collator COLLATOR = Collator.getInstance(Locale.US); - static { - COLLATOR.setStrength(Collator.PRIMARY); - } - private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator(); - private static final ThrustCurveMotorComparator comparator = new ThrustCurveMotorComparator(); - - - - private final ArrayList<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); - private final Map<ThrustCurveMotor, String> digestMap = - new IdentityHashMap<ThrustCurveMotor, String>(); - - private final List<Double> delays = new ArrayList<Double>(); - - private Manufacturer manufacturer = null; - private String designation = null; - private String simplifiedDesignation = null; - private double diameter = -1; - private double length = -1; - private Motor.Type type = Motor.Type.UNKNOWN; - - - - public void addMotor(ThrustCurveMotor motor) { - - // Check for first insertion - if (motors.isEmpty()) { - manufacturer = motor.getManufacturer(); - designation = motor.getDesignation(); - simplifiedDesignation = simplifyDesignation(designation); - diameter = motor.getDiameter(); - length = motor.getLength(); - } - - // Verify that the motor can be added - if (!matches(motor)) { - throw new IllegalArgumentException("Motor does not match the set:" + - " manufacturer=" + manufacturer + - " designation=" + designation + - " diameter=" + diameter + - " length=" + length + - " set_size=" + motors.size() + - " motor=" + motor); - } - - // Update the type if now known - if (type == Motor.Type.UNKNOWN) { - type = motor.getMotorType(); - // Add "Plugged" option if hybrid - if (type == Motor.Type.HYBRID) { - if (!delays.contains(Motor.PLUGGED)) { - delays.add(Motor.PLUGGED); - } - } - } - - // Change the simplified designation if necessary - if (!designation.equalsIgnoreCase(motor.getDesignation().trim())) { - designation = simplifiedDesignation; - } - - // Add the standard delays - for (double d : motor.getStandardDelays()) { - d = Math.rint(d); - if (!delays.contains(d)) { - delays.add(d); - } - } - Collections.sort(delays); - - - // Check whether to add as new motor or overwrite existing - final String digest = MotorDigest.digestMotor(motor); - for (int index = 0; index < motors.size(); index++) { - Motor m = motors.get(index); - - if (digest.equals(digestMap.get(m)) && - motor.getDesignation().equals(m.getDesignation())) { - - // Match found, check which one to keep (or both) based on comment - String newCmt = motor.getDescription().replaceAll("\\s+", " ").trim(); - String oldCmt = m.getDescription().replaceAll("\\s+", " ").trim(); - - if (newCmt.length() == 0 || newCmt.equals(oldCmt)) { - // Do not replace and do not add - return; - } else if (oldCmt.length() == 0) { - // Replace existing motor - motors.set(index, motor); - digestMap.put(motor, digest); - return; - } - // else continue search and add both - - } - } - - // Motor not present, add it - motors.add(motor); - digestMap.put(motor, digest); - Collections.sort(motors, comparator); - - } - - - public boolean matches(ThrustCurveMotor m) { - if (motors.isEmpty()) - return true; - - if (manufacturer != m.getManufacturer()) - return false; - - if (!MathUtil.equals(diameter, m.getDiameter())) - return false; - - if (!MathUtil.equals(length, m.getLength())) - return false; - - if ((type != Type.UNKNOWN) && (m.getMotorType() != Type.UNKNOWN) && - (type != m.getMotorType())) { - return false; - } - - if (!simplifiedDesignation.equals(simplifyDesignation(m.getDesignation()))) - return false; - - return true; - } - - - public List<ThrustCurveMotor> getMotors() { - return motors.clone(); - } - - - public int getMotorCount() { - return motors.size(); - } - - - /** - * Return the standard delays applicable to this motor type. This is a union of - * all the delays of the motors included in this set. - * @return the delays - */ - public List<Double> getDelays() { - return Collections.unmodifiableList(delays); - } - - - /** - * Return the manufacturer of this motor type. - * @return the manufacturer - */ - public Manufacturer getManufacturer() { - return manufacturer; - } - - - /** - * Return the designation of this motor type. This is either the exact or simplified - * designation, depending on what motors have been added. - * @return the designation - */ - public String getDesignation() { - return designation; - } - - - /** - * Return the diameter of this motor type. - * @return the diameter - */ - public double getDiameter() { - return diameter; - } - - - /** - * Return the length of this motor type. - * @return the length - */ - public double getLength() { - return length; - } - - - /** - * Return the type of this motor type. If any of the added motors has a type different - * from UNKNOWN, then that type will be returned. - * @return the type - */ - public Motor.Type getType() { - return type; - } - - - - - @Override - public String toString() { - return "ThrustCurveMotorSet[" + manufacturer + " " + designation + - ", type=" + type + ", count=" + motors.size() + "]"; - } - - - - - private static final Pattern SIMPLIFY_PATTERN = Pattern.compile("^[0-9]*[ -]*([A-Z][0-9]+).*"); - - /** - * Simplify a motor designation, if possible. This attempts to reduce the designation - * into a simple letter + number notation for the impulse class and average thrust. - * - * @param str the designation to simplify - * @return the simplified designation, or the string itself if the format was not detected - */ - public static String simplifyDesignation(String str) { - str = str.trim(); - Matcher m = SIMPLIFY_PATTERN.matcher(str); - if (m.matches()) { - return m.group(1); - } else { - return str; - } - } - - - /** - * Comparator for deciding in which order to display matching motors. - */ - private static class ThrustCurveMotorComparator implements Comparator<ThrustCurveMotor> { - - @Override - public int compare(ThrustCurveMotor o1, ThrustCurveMotor o2) { - // 1. Designation - if (!o1.getDesignation().equals(o2.getDesignation())) { - return o1.getDesignation().compareTo(o2.getDesignation()); - } - - // 2. Number of data points (more is better) - if (o1.getTimePoints().length != o2.getTimePoints().length) { - return o2.getTimePoints().length - o1.getTimePoints().length; - } - - // 3. Comment length (longer is better) - return o2.getDescription().length() - o1.getDescription().length(); - } - - } - - - @Override - public int compareTo(ThrustCurveMotorSet other) { - - int value; - - // 1. Manufacturer - value = COLLATOR.compare(this.manufacturer.getDisplayName(), - other.manufacturer.getDisplayName()); - if (value != 0) - return value; - - // 2. Designation - value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation); - if (value != 0) - return value; - - // 3. Diameter - value = (int) ((this.diameter - other.diameter) * 1000000); - if (value != 0) - return value; - - // 4. Length - value = (int) ((this.length - other.length) * 1000000); - return value; - - } - -} diff --git a/src/net/sf/openrocket/database/ThrustCurveMotorSetDatabase.java b/src/net/sf/openrocket/database/ThrustCurveMotorSetDatabase.java deleted file mode 100644 index 1aeb6cd4..00000000 --- a/src/net/sf/openrocket/database/ThrustCurveMotorSetDatabase.java +++ /dev/null @@ -1,213 +0,0 @@ -package net.sf.openrocket.database; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; - -/** - * A database containing ThrustCurveMotorSet objects and allowing adding a motor - * to the database. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class ThrustCurveMotorSetDatabase implements MotorDatabase { - - private static final LogHelper logger = Application.getLogger(); - - protected List<ThrustCurveMotorSet> motorSets; - - private volatile boolean startedLoading = false; - private volatile boolean endedLoading = false; - private final boolean asynchronous; - - /** Set to true the first time {@link #blockUntilLoaded()} is called. */ - protected volatile boolean inUse = false; - - /** - * Sole constructor. - * - * @param asynchronous whether to load motors asynchronously in a background thread. - */ - public ThrustCurveMotorSetDatabase(boolean asynchronous) { - this.asynchronous = asynchronous; - } - - - /* (non-Javadoc) - * @see net.sf.openrocket.database.ThrustCurveMotorSetDatabaseI#getMotorSets() - */ - public List<ThrustCurveMotorSet> getMotorSets() { - blockUntilLoaded(); - return motorSets; - } - - - - /* (non-Javadoc) - * @see net.sf.openrocket.database.ThrustCurveMotorSetDatabaseI#findMotors(net.sf.openrocket.motor.Motor.Type, java.lang.String, java.lang.String, double, double) - */ - @Override - public List<ThrustCurveMotor> findMotors(Motor.Type type, String manufacturer, String designation, - double diameter, double length) { - blockUntilLoaded(); - ArrayList<ThrustCurveMotor> results = new ArrayList<ThrustCurveMotor>(); - - for (ThrustCurveMotorSet set : motorSets) { - for (ThrustCurveMotor m : set.getMotors()) { - boolean match = true; - if (type != null && type != set.getType()) - match = false; - else if (manufacturer != null && !m.getManufacturer().matches(manufacturer)) - match = false; - else if (designation != null && !designation.equalsIgnoreCase(m.getDesignation())) - match = false; - else if (!Double.isNaN(diameter) && (Math.abs(diameter - m.getDiameter()) > 0.0015)) - match = false; - else if (!Double.isNaN(length) && (Math.abs(length - m.getLength()) > 0.0015)) - match = false; - - if (match) - results.add(m); - } - } - - return results; - } - - - /** - * Add a motor to the database. If a matching ThrustCurveMototSet is found, - * the motor is added to that set, otherwise a new set is created and added to the - * database. - * - * @param motor the motor to add - */ - protected void addMotor(ThrustCurveMotor motor) { - // Iterate from last to first, as this is most likely to hit early when loading files - for (int i = motorSets.size() - 1; i >= 0; i--) { - ThrustCurveMotorSet set = motorSets.get(i); - if (set.matches(motor)) { - set.addMotor(motor); - return; - } - } - - ThrustCurveMotorSet newSet = new ThrustCurveMotorSet(); - newSet.addMotor(motor); - motorSets.add(newSet); - } - - - - - - /** - * Start loading the motors. If asynchronous - * - * @throws IllegalStateException if this method has already been called. - */ - public void startLoading() { - if (startedLoading) { - throw new IllegalStateException("Already called startLoading"); - } - startedLoading = true; - if (asynchronous) { - new LoadingThread().start(); - } else { - performMotorLoading(); - } - } - - - /** - * Return whether loading the database has ended. - * - * @return whether background loading has ended. - */ - public boolean isLoaded() { - return endedLoading; - } - - - /** - * Mark that this database is in use or a place is waiting for the database to - * become loaded. This can be used in conjunction with {@link #isLoaded()} to load - * the database without blocking. - */ - public void setInUse() { - inUse = true; - } - - - /** - * Block the current thread until loading of the motors has been completed. - * - * @throws IllegalStateException if startLoading() has not been called. - */ - public void blockUntilLoaded() { - inUse = true; - if (!startedLoading) { - throw new IllegalStateException("startLoading() has not been called"); - } - if (!endedLoading) { - synchronized (this) { - while (!endedLoading) { - try { - this.wait(); - } catch (InterruptedException e) { - logger.warn("InterruptedException occurred, ignoring", e); - } - } - } - } - } - - - /** - * Used for loading the motor database. This method will be called in a background - * thread to load the motors asynchronously. This method should call - * {@link #addMotor(ThrustCurveMotor)} to add the motors to the database. - */ - protected abstract void loadMotors(); - - - - /** - * Creates the motor list, calls {@link #loadMotors()}, sorts the list and marks - * the motors as loaded. This method is called either synchronously or from the - * background thread. - */ - private void performMotorLoading() { - motorSets = new ArrayList<ThrustCurveMotorSet>(); - try { - loadMotors(); - } catch (Exception e) { - logger.error("Loading motors failed", e); - } - Collections.sort(motorSets); - motorSets = Collections.unmodifiableList(motorSets); - synchronized (ThrustCurveMotorSetDatabase.this) { - endedLoading = true; - ThrustCurveMotorSetDatabase.this.notifyAll(); - } - } - - - /** - * Background thread for loading the motors. This creates the motor list, - * calls loadMotors(), sorts the database, makes it unmodifiable, and finally - * marks the database as loaded and notifies any blocked threads. - */ - private class LoadingThread extends Thread { - @Override - public void run() { - performMotorLoading(); - } - } - -} diff --git a/src/net/sf/openrocket/document/OpenRocketDocument.java b/src/net/sf/openrocket/document/OpenRocketDocument.java deleted file mode 100644 index cafc98f3..00000000 --- a/src/net/sf/openrocket/document/OpenRocketDocument.java +++ /dev/null @@ -1,531 +0,0 @@ -package net.sf.openrocket.document; - -import java.io.File; -import java.util.LinkedList; -import java.util.List; - -import net.sf.openrocket.document.events.DocumentChangeEvent; -import net.sf.openrocket.document.events.DocumentChangeListener; -import net.sf.openrocket.document.events.SimulationChangeEvent; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayList; - -/** - * Class describing an entire OpenRocket document, including a rocket and - * simulations. The document contains: - * <p> - * - the rocket definition - * - a default Configuration - * - Simulation instances - * - the stored file and file save information - * - undo/redo information - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OpenRocketDocument implements ComponentChangeListener { - private static final LogHelper log = Application.getLogger(); - - /** - * The minimum number of undo levels that are stored. - */ - public static final int UNDO_LEVELS = 50; - /** - * The margin of the undo levels. After the number of undo levels exceeds - * UNDO_LEVELS by this amount the undo is purged to that length. - */ - public static final int UNDO_MARGIN = 10; - - public static final String SIMULATION_NAME_PREFIX = "Simulation "; - - /** Whether an undo error has already been reported to the user */ - private static boolean undoErrorReported = false; - - private final Rocket rocket; - private final Configuration configuration; - - private final ArrayList<Simulation> simulations = new ArrayList<Simulation>(); - - - /* - * The undo/redo variables and mechanism are documented in doc/undo-redo-flow.* - */ - - /** - * The undo history of the rocket. Whenever a new undo position is created while the - * rocket is in "dirty" state, the rocket is copied here. - */ - private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>(); - private LinkedList<String> undoDescription = new LinkedList<String>(); - - /** - * The position in the undoHistory we are currently at. If modifications have been - * made to the rocket, the rocket is in "dirty" state and this points to the previous - * "clean" state. - */ - private int undoPosition = -1; // Illegal position, init in constructor - - /** - * The description of the next action that modifies this rocket. - */ - private String nextDescription = null; - private String storedDescription = null; - - - private ArrayList<UndoRedoListener> undoRedoListeners = new ArrayList<UndoRedoListener>(2); - - private File file = null; - private int savedID = -1; - - private final StorageOptions storageOptions = new StorageOptions(); - - - private final List<DocumentChangeListener> listeners = - new ArrayList<DocumentChangeListener>(); - - public OpenRocketDocument(Rocket rocket) { - this(rocket.getDefaultConfiguration()); - } - - - private OpenRocketDocument(Configuration configuration) { - this.configuration = configuration; - this.rocket = configuration.getRocket(); - - clearUndo(); - - rocket.addComponentChangeListener(this); - } - - - - - public Rocket getRocket() { - return rocket; - } - - - public Configuration getDefaultConfiguration() { - return configuration; - } - - - public File getFile() { - return file; - } - - public void setFile(File file) { - this.file = file; - } - - - public boolean isSaved() { - return rocket.getModID() == savedID; - } - - public void setSaved(boolean saved) { - if (saved == false) - this.savedID = -1; - else - this.savedID = rocket.getModID(); - } - - /** - * Retrieve the default storage options for this document. - * - * @return the storage options. - */ - public StorageOptions getDefaultStorageOptions() { - return storageOptions; - } - - - - - - public List<Simulation> getSimulations() { - return simulations.clone(); - } - - public int getSimulationCount() { - return simulations.size(); - } - - public Simulation getSimulation(int n) { - return simulations.get(n); - } - - public int getSimulationIndex(Simulation simulation) { - return simulations.indexOf(simulation); - } - - public void addSimulation(Simulation simulation) { - simulations.add(simulation); - fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); - } - - public void addSimulation(Simulation simulation, int n) { - simulations.add(n, simulation); - fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); - } - - public void removeSimulation(Simulation simulation) { - simulations.remove(simulation); - fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); - } - - public Simulation removeSimulation(int n) { - Simulation simulation = simulations.remove(n); - fireDocumentChangeEvent(new SimulationChangeEvent(simulation)); - return simulation; - } - - /** - * Return a unique name suitable for the next simulation. The name begins - * with {@link #SIMULATION_NAME_PREFIX} and has a unique number larger than any - * previous simulation. - * - * @return the new name. - */ - public String getNextSimulationName() { - // Generate unique name for the simulation - int maxValue = 0; - for (Simulation s : simulations) { - String name = s.getName(); - if (name.startsWith(SIMULATION_NAME_PREFIX)) { - try { - maxValue = Math.max(maxValue, - Integer.parseInt(name.substring(SIMULATION_NAME_PREFIX.length()))); - } catch (NumberFormatException ignore) { - } - } - } - return SIMULATION_NAME_PREFIX + (maxValue + 1); - } - - - /** - * Adds an undo point at this position. This method should be called *before* any - * action that is to be undoable. All actions after the call will be undone by a - * single "Undo" action. - * <p> - * The description should be a short, descriptive string of the actions that will - * follow. This is shown to the user e.g. in the Edit-menu, for example - * "Undo (Modify body tube)". If the actions are not known (in general should not - * be the case!) description may be null. - * <p> - * If this method is called successively without any change events occurring between the - * calls, only the last call will have any effect. - * - * @param description A short description of the following actions. - */ - public void addUndoPosition(String description) { - - if (storedDescription != null) { - logUndoError("addUndoPosition called while storedDescription=" + storedDescription + - " description=" + description); - } - - // Check whether modifications have been done since last call - if (isCleanState()) { - // No modifications - log.info("Adding undo position '" + description + "' to " + this + ", document was in clean state"); - nextDescription = description; - return; - } - - log.info("Adding undo position '" + description + "' to " + this + ", document is in unclean state"); - - /* - * Modifications have been made to the rocket. We should be at the end of the - * undo history, but check for consistency and try to recover. - */ - if (undoPosition != undoHistory.size() - 1) { - logUndoError("undo position inconsistency"); - } - while (undoPosition < undoHistory.size() - 1) { - undoHistory.removeLast(); - undoDescription.removeLast(); - } - - - // Add the current state to the undo history - undoHistory.add(rocket.copyWithOriginalID()); - undoDescription.add(null); - nextDescription = description; - undoPosition++; - - - // Maintain maximum undo size - if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN && undoPosition > UNDO_MARGIN) { - for (int i = 0; i < UNDO_MARGIN; i++) { - undoHistory.removeFirst(); - undoDescription.removeFirst(); - undoPosition--; - } - } - } - - - /** - * Start a time-limited undoable operation. After the operation {@link #stopUndo()} - * must be called, which will restore the previous undo description into effect. - * Only one level of start-stop undo descriptions is supported, i.e. start-stop - * undo cannot be nested, and no other undo operations may be called between - * the start and stop calls. - * - * @param description Description of the following undoable operations. - */ - public void startUndo(String description) { - if (storedDescription != null) { - logUndoError("startUndo called while storedDescription=" + storedDescription + - " description=" + description); - } - log.info("Starting time-limited undoable operation '" + description + "' for " + this); - String store = nextDescription; - addUndoPosition(description); - storedDescription = store; - } - - /** - * End the previous time-limited undoable operation. This must be called after - * {@link #startUndo(String)} has been called before any other undo operations are - * performed. - */ - public void stopUndo() { - log.info("Ending time-limited undoable operation for " + this + " nextDescription=" + - nextDescription + " storedDescription=" + storedDescription); - String stored = storedDescription; - storedDescription = null; - addUndoPosition(stored); - } - - - /** - * Clear the undo history. - */ - public void clearUndo() { - log.info("Clearing undo history of " + this); - undoHistory.clear(); - undoDescription.clear(); - - undoHistory.add(rocket.copyWithOriginalID()); - undoDescription.add(null); - undoPosition = 0; - - fireUndoRedoChangeEvent(); - } - - - @Override - public void componentChanged(ComponentChangeEvent e) { - - if (!e.isUndoChange()) { - if (undoPosition < undoHistory.size() - 1) { - log.info("Rocket changed while in undo history, removing redo information for " + this + - " undoPosition=" + undoPosition + " undoHistory.size=" + undoHistory.size() + - " isClean=" + isCleanState()); - } - // Remove any redo information if available - while (undoPosition < undoHistory.size() - 1) { - undoHistory.removeLast(); - undoDescription.removeLast(); - } - - // Set the latest description - undoDescription.set(undoPosition, nextDescription); - } - - fireUndoRedoChangeEvent(); - } - - - /** - * Return whether undo action is available. - * @return <code>true</code> if undo can be performed - */ - public boolean isUndoAvailable() { - if (undoPosition > 0) - return true; - - return !isCleanState(); - } - - /** - * Return the description of what action would be undone if undo is called. - * @return the description what would be undone, or <code>null</code> if description unavailable. - */ - public String getUndoDescription() { - if (!isUndoAvailable()) - return null; - - if (isCleanState()) { - return undoDescription.get(undoPosition - 1); - } else { - return undoDescription.get(undoPosition); - } - } - - - /** - * Return whether redo action is available. - * @return <code>true</code> if redo can be performed - */ - public boolean isRedoAvailable() { - return undoPosition < undoHistory.size() - 1; - } - - /** - * Return the description of what action would be redone if redo is called. - * @return the description of what would be redone, or <code>null</code> if description unavailable. - */ - public String getRedoDescription() { - if (!isRedoAvailable()) - return null; - - return undoDescription.get(undoPosition); - } - - - /** - * Perform undo operation on the rocket. - */ - public void undo() { - log.info("Performing undo for " + this + " undoPosition=" + undoPosition + - " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState()); - if (!isUndoAvailable()) { - logUndoError("Undo not available"); - fireUndoRedoChangeEvent(); - return; - } - if (storedDescription != null) { - logUndoError("undo() called with storedDescription=" + storedDescription); - } - - // Update history position - - if (isCleanState()) { - // We are in a clean state, simply move backwards in history - undoPosition--; - } else { - if (undoPosition != undoHistory.size() - 1) { - logUndoError("undo position inconsistency"); - } - // Modifications have been made, save the state and restore previous state - undoHistory.add(rocket.copyWithOriginalID()); - undoDescription.add(null); - } - - rocket.checkComponentStructure(); - undoHistory.get(undoPosition).checkComponentStructure(); - undoHistory.get(undoPosition).copyWithOriginalID().checkComponentStructure(); - rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID()); - rocket.checkComponentStructure(); - } - - - /** - * Perform redo operation on the rocket. - */ - public void redo() { - log.info("Performing redo for " + this + " undoPosition=" + undoPosition + - " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState()); - if (!isRedoAvailable()) { - logUndoError("Redo not available"); - fireUndoRedoChangeEvent(); - return; - } - if (storedDescription != null) { - logUndoError("redo() called with storedDescription=" + storedDescription); - } - - undoPosition++; - - rocket.loadFrom(undoHistory.get(undoPosition).copyWithOriginalID()); - } - - - private boolean isCleanState() { - return rocket.getModID() == undoHistory.get(undoPosition).getModID(); - } - - - /** - * Log a non-fatal undo/redo error or inconsistency. Reports it to the user the first - * time it occurs, but not on subsequent times. Logs automatically the undo system state. - */ - private void logUndoError(String error) { - log.error(1, error + ": this=" + this + " undoPosition=" + undoPosition + - " undoHistory.size=" + undoHistory.size() + " isClean=" + isCleanState() + - " nextDescription=" + nextDescription + " storedDescription=" + storedDescription, - new TraceException()); - - if (!undoErrorReported) { - undoErrorReported = true; - Application.getExceptionHandler().handleErrorCondition("Undo/Redo error: " + error); - } - } - - - - /** - * Return a copy of this document. The rocket is copied with original ID's, the default - * motor configuration ID is maintained and the simulations are copied to the new rocket. - * No undo/redo information or file storage information is maintained. - * - * @return a copy of this document. - */ - public OpenRocketDocument copy() { - Rocket rocketCopy = rocket.copyWithOriginalID(); - OpenRocketDocument documentCopy = new OpenRocketDocument(rocketCopy); - documentCopy.getDefaultConfiguration().setMotorConfigurationID(configuration.getMotorConfigurationID()); - for (Simulation s : simulations) { - documentCopy.addSimulation(s.duplicateSimulation(rocketCopy)); - } - return documentCopy; - } - - - - /////// Listeners - - public void addUndoRedoListener( UndoRedoListener listener ) { - undoRedoListeners.add(listener); - } - - public void removeUndoRedoListener( UndoRedoListener listener ) { - undoRedoListeners.remove(listener); - } - - private void fireUndoRedoChangeEvent() { - UndoRedoListener[] array = undoRedoListeners.toArray(new UndoRedoListener[0]); - for (UndoRedoListener l : array) { - l.setAllValues(); - } - - } - - public void addDocumentChangeListener(DocumentChangeListener listener) { - listeners.add(listener); - } - - public void removeDocumentChangeListener(DocumentChangeListener listener) { - listeners.remove(listener); - } - - protected void fireDocumentChangeEvent(DocumentChangeEvent event) { - DocumentChangeListener[] array = listeners.toArray(new DocumentChangeListener[0]); - for (DocumentChangeListener l : array) { - l.documentChanged(event); - } - } - - - - -} diff --git a/src/net/sf/openrocket/document/Simulation.java b/src/net/sf/openrocket/document/Simulation.java deleted file mode 100644 index 74d3ab28..00000000 --- a/src/net/sf/openrocket/document/Simulation.java +++ /dev/null @@ -1,469 +0,0 @@ -package net.sf.openrocket.document; - -import java.util.EventListener; -import java.util.EventObject; -import java.util.List; - -import net.sf.openrocket.aerodynamics.AerodynamicCalculator; -import net.sf.openrocket.aerodynamics.BarrowmanCalculator; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.simulation.BasicEventSimulationEngine; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.RK4SimulationStepper; -import net.sf.openrocket.simulation.SimulationConditions; -import net.sf.openrocket.simulation.SimulationEngine; -import net.sf.openrocket.simulation.SimulationOptions; -import net.sf.openrocket.simulation.SimulationStepper; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.exception.SimulationListenerException; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.SafetyMutex; -import net.sf.openrocket.util.StateChangeListener; - -/** - * A class defining a simulation, its conditions and simulated data. - * <p> - * This class is not thread-safe and enforces single-threaded access with a - * SafetyMutex. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Simulation implements ChangeSource, Cloneable { - private static final LogHelper log = Application.getLogger(); - - public static enum Status { - /** Up-to-date */ - UPTODATE, - - /** Loaded from file, status probably up-to-date */ - LOADED, - - /** Data outdated */ - OUTDATED, - - /** Imported external data */ - EXTERNAL, - - /** Not yet simulated */ - NOT_SIMULATED - } - - private SafetyMutex mutex = SafetyMutex.newInstance(); - - private final Rocket rocket; - - private String name = ""; - - private Status status = Status.NOT_SIMULATED; - - /** The conditions to use */ - // TODO: HIGH: Change to use actual conditions class?? - private SimulationOptions options; - - private ArrayList<String> simulationListeners = new ArrayList<String>(); - - private final Class<? extends SimulationEngine> simulationEngineClass = BasicEventSimulationEngine.class; - private Class<? extends SimulationStepper> simulationStepperClass = RK4SimulationStepper.class; - private Class<? extends AerodynamicCalculator> aerodynamicCalculatorClass = BarrowmanCalculator.class; - private Class<? extends MassCalculator> massCalculatorClass = BasicMassCalculator.class; - - - - /** Listeners for this object */ - private List<EventListener> listeners = new ArrayList<EventListener>(); - - - /** The conditions actually used in the previous simulation, or null */ - private SimulationOptions simulatedConditions = null; - private String simulatedMotors = null; - private FlightData simulatedData = null; - private int simulatedRocketID = -1; - - - /** - * Create a new simulation for the rocket. The initial motor configuration is - * taken from the default rocket configuration. - * - * @param rocket the rocket associated with the simulation. - */ - public Simulation(Rocket rocket) { - this.rocket = rocket; - this.status = Status.NOT_SIMULATED; - - options = new SimulationOptions(rocket); - options.setMotorConfigurationID( - rocket.getDefaultConfiguration().getMotorConfigurationID()); - options.addChangeListener(new ConditionListener()); - } - - - public Simulation(Rocket rocket, Status status, String name, SimulationOptions options, - List<String> listeners, FlightData data) { - - if (rocket == null) - throw new IllegalArgumentException("rocket cannot be null"); - if (status == null) - throw new IllegalArgumentException("status cannot be null"); - if (name == null) - throw new IllegalArgumentException("name cannot be null"); - if (options == null) - throw new IllegalArgumentException("options cannot be null"); - - this.rocket = rocket; - - if (status == Status.UPTODATE) { - this.status = Status.LOADED; - } else if (data == null) { - this.status = Status.NOT_SIMULATED; - } else { - this.status = status; - } - - this.name = name; - - this.options = options; - options.addChangeListener(new ConditionListener()); - - if (listeners != null) { - this.simulationListeners.addAll(listeners); - } - - - if (data != null && this.status != Status.NOT_SIMULATED) { - simulatedData = data; - if (this.status == Status.LOADED) { - simulatedConditions = options.clone(); - simulatedRocketID = rocket.getModID(); - } - } - - } - - - /** - * Return the rocket associated with this simulation. - * - * @return the rocket. - */ - public Rocket getRocket() { - mutex.verify(); - return rocket; - } - - - /** - * Return a newly created Configuration for this simulation. The configuration - * has the motor ID set and all stages active. - * - * @return a newly created Configuration of the launch conditions. - */ - public Configuration getConfiguration() { - mutex.verify(); - Configuration c = new Configuration(rocket); - c.setMotorConfigurationID(options.getMotorConfigurationID()); - c.setAllStages(); - return c; - } - - /** - * Returns the simulation options attached to this simulation. The options - * may be modified freely, and the status of the simulation will change to reflect - * the changes. - * - * @return the simulation conditions. - */ - public SimulationOptions getOptions() { - mutex.verify(); - return options; - } - - - /** - * Get the list of simulation listeners. The returned list is the one used by - * this object; changes to it will reflect changes in the simulation. - * - * @return the actual list of simulation listeners. - */ - public List<String> getSimulationListeners() { - mutex.verify(); - return simulationListeners; - } - - - /** - * Return the user-defined name of the simulation. - * - * @return the name for the simulation. - */ - public String getName() { - mutex.verify(); - return name; - } - - /** - * Set the user-defined name of the simulation. Setting the name to - * null yields an empty name. - * - * @param name the name of the simulation. - */ - public void setName(String name) { - mutex.lock("setName"); - try { - if (this.name.equals(name)) - return; - - if (name == null) - this.name = ""; - else - this.name = name; - - fireChangeEvent(); - } finally { - mutex.unlock("setName"); - } - } - - - /** - * Returns the status of this simulation. This method examines whether the - * simulation has been outdated and returns {@link Status#OUTDATED} accordingly. - * - * @return the status - * @see Status - */ - public Status getStatus() { - mutex.verify(); - - if (status == Status.UPTODATE || status == Status.LOADED) { - if (rocket.getFunctionalModID() != simulatedRocketID || - !options.equals(simulatedConditions)) - return Status.OUTDATED; - } - - return status; - } - - - - /** - * Simulate the flight. - * - * @param additionalListeners additional simulation listeners (those defined by the simulation are used in any case) - * @throws SimulationException if a problem occurs during simulation - */ - public void simulate(SimulationListener... additionalListeners) - throws SimulationException { - mutex.lock("simulate"); - try { - - if (this.status == Status.EXTERNAL) { - throw new SimulationException("Cannot simulate imported simulation."); - } - - SimulationEngine simulator; - - try { - simulator = simulationEngineClass.newInstance(); - } catch (InstantiationException e) { - throw new IllegalStateException("Cannot instantiate simulator.", e); - } catch (IllegalAccessException e) { - throw new IllegalStateException("Cannot access simulator instance?! BUG!", e); - } - - SimulationConditions simulationConditions = options.toSimulationConditions(); - for (SimulationListener l : additionalListeners) { - simulationConditions.getSimulationListenerList().add(l); - } - - for (String className : simulationListeners) { - SimulationListener l = null; - try { - Class<?> c = Class.forName(className); - l = (SimulationListener) c.newInstance(); - } catch (Exception e) { - throw new SimulationListenerException("Could not instantiate listener of " + - "class: " + className, e); - } - simulationConditions.getSimulationListenerList().add(l); - } - - long t1, t2; - log.debug("Simulation: calling simulator"); - t1 = System.currentTimeMillis(); - simulatedData = simulator.simulate(simulationConditions); - t2 = System.currentTimeMillis(); - log.debug("Simulation: returning from simulator, simulation took " + (t2 - t1) + "ms"); - - // Set simulated info after simulation, will not be set in case of exception - simulatedConditions = options.clone(); - simulatedMotors = getConfiguration().getMotorConfigurationDescription(); - simulatedRocketID = rocket.getFunctionalModID(); - - status = Status.UPTODATE; - fireChangeEvent(); - } finally { - mutex.unlock("simulate"); - } - } - - - /** - * Return the conditions used in the previous simulation, or <code>null</code> - * if this simulation has not been run. - * - * @return the conditions used in the previous simulation, or <code>null</code>. - */ - public SimulationOptions getSimulatedConditions() { - mutex.verify(); - return simulatedConditions; - } - - /** - * Return the warnings generated in the previous simulation, or - * <code>null</code> if this simulation has not been run. This is the same - * warning set as contained in the <code>FlightData</code> object. - * - * @return the warnings during the previous simulation, or <code>null</code>. - * @see FlightData#getWarningSet() - */ - public WarningSet getSimulatedWarnings() { - mutex.verify(); - if (simulatedData == null) - return null; - return simulatedData.getWarningSet(); - } - - - /** - * Return a string describing the motor configuration of the previous simulation, - * or <code>null</code> if this simulation has not been run. - * - * @return a description of the motor configuration of the previous simulation, or - * <code>null</code>. - * @see Rocket#getMotorConfigurationNameOrDescription(String) - */ - public String getSimulatedMotorDescription() { - mutex.verify(); - return simulatedMotors; - } - - /** - * Return the flight data of the previous simulation, or <code>null</code> if - * this simulation has not been run. - * - * @return the flight data of the previous simulation, or <code>null</code>. - */ - public FlightData getSimulatedData() { - mutex.verify(); - return simulatedData; - } - - - - /** - * Returns a copy of this simulation suitable for cut/copy/paste operations. - * The rocket refers to the same instance as the original simulation. - * This excludes any simulated data. - * - * @return a copy of this simulation and its conditions. - */ - public Simulation copy() { - mutex.lock("copy"); - try { - - Simulation copy = (Simulation) super.clone(); - - copy.mutex = SafetyMutex.newInstance(); - copy.status = Status.NOT_SIMULATED; - copy.options = this.options.clone(); - copy.simulationListeners = this.simulationListeners.clone(); - copy.listeners = new ArrayList<EventListener>(); - copy.simulatedConditions = null; - copy.simulatedMotors = null; - copy.simulatedData = null; - copy.simulatedRocketID = -1; - - return copy; - - } catch (CloneNotSupportedException e) { - throw new BugException("Clone not supported, BUG", e); - } finally { - mutex.unlock("copy"); - } - } - - - /** - * Create a duplicate of this simulation with the specified rocket. The new - * simulation is in non-simulated state. - * - * @param newRocket the rocket for the new simulation. - * @return a new simulation with the same conditions and properties. - */ - public Simulation duplicateSimulation(Rocket newRocket) { - mutex.lock("duplicateSimulation"); - try { - Simulation copy = new Simulation(newRocket); - - copy.name = this.name; - copy.options.copyFrom(this.options); - copy.simulationListeners = this.simulationListeners.clone(); - copy.simulationStepperClass = this.simulationStepperClass; - copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass; - - return copy; - } finally { - mutex.unlock("duplicateSimulation"); - } - } - - - - @Override - public void addChangeListener(EventListener listener) { - mutex.verify(); - listeners.add(listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - mutex.verify(); - listeners.remove(listener); - } - - protected void fireChangeEvent() { - EventObject e = new EventObject(this); - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] ls = listeners.toArray(new EventListener[0]); - for (EventListener l : ls) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(e); - } - } - } - - - - - private class ConditionListener implements StateChangeListener { - - private Status oldStatus = null; - - @Override - public void stateChanged(EventObject e) { - if (getStatus() != oldStatus) { - oldStatus = getStatus(); - fireChangeEvent(); - } - } - } -} diff --git a/src/net/sf/openrocket/document/StorageOptions.java b/src/net/sf/openrocket/document/StorageOptions.java deleted file mode 100644 index 7e1186c1..00000000 --- a/src/net/sf/openrocket/document/StorageOptions.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.sf.openrocket.document; - -import net.sf.openrocket.util.BugException; - -public class StorageOptions implements Cloneable { - - public static final double SIMULATION_DATA_NONE = Double.POSITIVE_INFINITY; - public static final double SIMULATION_DATA_ALL = 0; - - private boolean compressionEnabled = true; - - private double simulationTimeSkip = SIMULATION_DATA_NONE; - - private boolean explicitlySet = false; - - - public boolean isCompressionEnabled() { - return compressionEnabled; - } - - public void setCompressionEnabled(boolean compression) { - this.compressionEnabled = compression; - } - - public double getSimulationTimeSkip() { - return simulationTimeSkip; - } - - public void setSimulationTimeSkip(double simulationTimeSkip) { - this.simulationTimeSkip = simulationTimeSkip; - } - - - - public boolean isExplicitlySet() { - return explicitlySet; - } - - public void setExplicitlySet(boolean explicitlySet) { - this.explicitlySet = explicitlySet; - } - - - - @Override - public StorageOptions clone() { - try { - return (StorageOptions)super.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException?!?", e); - } - } -} diff --git a/src/net/sf/openrocket/document/UndoRedoListener.java b/src/net/sf/openrocket/document/UndoRedoListener.java deleted file mode 100644 index 4f7900ea..00000000 --- a/src/net/sf/openrocket/document/UndoRedoListener.java +++ /dev/null @@ -1,8 +0,0 @@ -package net.sf.openrocket.document; - -import java.util.EventListener; - -public interface UndoRedoListener extends EventListener { - - public void setAllValues(); -} diff --git a/src/net/sf/openrocket/document/events/DocumentChangeEvent.java b/src/net/sf/openrocket/document/events/DocumentChangeEvent.java deleted file mode 100644 index cb5662f2..00000000 --- a/src/net/sf/openrocket/document/events/DocumentChangeEvent.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.sf.openrocket.document.events; - -import java.util.EventObject; - -public class DocumentChangeEvent extends EventObject { - - public DocumentChangeEvent(Object source) { - super(source); - } - -} diff --git a/src/net/sf/openrocket/document/events/DocumentChangeListener.java b/src/net/sf/openrocket/document/events/DocumentChangeListener.java deleted file mode 100644 index 01d3dbd4..00000000 --- a/src/net/sf/openrocket/document/events/DocumentChangeListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.sf.openrocket.document.events; - -public interface DocumentChangeListener { - - public void documentChanged(DocumentChangeEvent event); - -} diff --git a/src/net/sf/openrocket/document/events/SimulationChangeEvent.java b/src/net/sf/openrocket/document/events/SimulationChangeEvent.java deleted file mode 100644 index 56ed6d1a..00000000 --- a/src/net/sf/openrocket/document/events/SimulationChangeEvent.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.sf.openrocket.document.events; - - -public class SimulationChangeEvent extends DocumentChangeEvent { - - public SimulationChangeEvent(Object source) { - super(source); - } - -} diff --git a/src/net/sf/openrocket/file/CSVExport.java b/src/net/sf/openrocket/file/CSVExport.java deleted file mode 100644 index f2e8d6fc..00000000 --- a/src/net/sf/openrocket/file/CSVExport.java +++ /dev/null @@ -1,213 +0,0 @@ -package net.sf.openrocket.file; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.util.TextUtil; - -public class CSVExport { - - /** - * Exports the specified flight data branch into a CSV file. - * - * @param stream the stream to write to. - * @param simulation the simulation being exported. - * @param branch the branch to export. - * @param fields the fields to export (in appropriate order). - * @param units the units of the fields. - * @param fieldSeparator the field separator string. - * @param commentStarter the comment starting character(s). - * @param simulationComments whether to output general simulation comments. - * @param fieldComments whether to output field comments. - * @param eventComments whether to output comments for the flight events. - * @throws IOException if an I/O exception occurs. - */ - public static void exportCSV(OutputStream stream, Simulation simulation, - FlightDataBranch branch, FlightDataType[] fields, Unit[] units, - String fieldSeparator, String commentStarter, boolean simulationComments, - boolean fieldComments, boolean eventComments) throws IOException { - - if (fields.length != units.length) { - throw new IllegalArgumentException("fields and units lengths must be equal " + - "(" + fields.length + " vs " + units.length + ")"); - } - - - PrintWriter writer = null; - try { - - writer = new PrintWriter(stream); - - // Write the initial comments - if (simulationComments) { - writeSimulationComments(writer, simulation, branch, fields, commentStarter); - } - - if (simulationComments && fieldComments) { - writer.println(commentStarter); - } - - if (fieldComments) { - writer.print(commentStarter + " "); - for (int i = 0; i < fields.length; i++) { - writer.print(fields[i].getName() + " (" + units[i].getUnit() + ")"); - if (i < fields.length - 1) { - writer.print(fieldSeparator); - } - } - writer.println(); - } - - writeData(writer, branch, fields, units, fieldSeparator, - eventComments, commentStarter); - - - } finally { - if (writer != null) { - try { - writer.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } - - private static void writeData(PrintWriter writer, FlightDataBranch branch, - FlightDataType[] fields, Unit[] units, String fieldSeparator, boolean eventComments, - String commentStarter) { - - // Number of data points - int n = branch.getLength(); - - // Flight events in occurrance order - List<FlightEvent> events = branch.getEvents(); - Collections.sort(events); - int eventPosition = 0; - - // List of field values - List<List<Double>> fieldValues = new ArrayList<List<Double>>(); - for (FlightDataType t : fields) { - fieldValues.add(branch.get(t)); - } - - // Time variable - List<Double> time = branch.get(FlightDataType.TYPE_TIME); - if (eventComments && time == null) { - // If time information is not available, print events at beginning of file - for (FlightEvent e : events) { - printEvent(writer, e, commentStarter); - } - eventPosition = events.size(); - } - - - // Loop over all data points - for (int pos = 0; pos < n; pos++) { - - // Check for events to store - if (eventComments && time != null) { - double t = time.get(pos); - - while ((eventPosition < events.size()) && - (events.get(eventPosition).getTime() <= t)) { - printEvent(writer, events.get(eventPosition), commentStarter); - eventPosition++; - } - } - - // Store CSV line - for (int i = 0; i < fields.length; i++) { - double value = fieldValues.get(i).get(pos); - writer.print(TextUtil.doubleToString(units[i].toUnit(value))); - if (i < fields.length - 1) { - writer.print(fieldSeparator); - } - } - writer.println(); - - } - - // Store any remaining events - if (eventComments && time != null) { - while (eventPosition < events.size()) { - printEvent(writer, events.get(eventPosition), commentStarter); - eventPosition++; - } - } - - } - - - private static void printEvent(PrintWriter writer, FlightEvent e, - String commentStarter) { - writer.println(commentStarter + " Event " + e.getType().name() + - " occurred at t=" + TextUtil.doubleToString(e.getTime()) + " seconds"); - } - - private static void writeSimulationComments(PrintWriter writer, - Simulation simulation, FlightDataBranch branch, FlightDataType[] fields, - String commentStarter) { - - String line; - - line = simulation.getName(); - - FlightData data = simulation.getSimulatedData(); - - switch (simulation.getStatus()) { - case UPTODATE: - line += " (Up to date)"; - break; - - case LOADED: - line += " (Data loaded from a file)"; - break; - - case OUTDATED: - line += " (Data is out of date)"; - break; - - case EXTERNAL: - line += " (Imported data)"; - break; - - case NOT_SIMULATED: - line += " (Not simulated yet)"; - break; - } - - writer.println(commentStarter + " " + line); - - - writer.println(commentStarter + " " + branch.getLength() + " data points written for " - + fields.length + " variables."); - - - if (data == null) { - writer.println(commentStarter + " No simulation data available."); - return; - } - WarningSet warnings = data.getWarningSet(); - - if (!warnings.isEmpty()) { - writer.println(commentStarter + " Simulation warnings:"); - for (Warning w : warnings) { - writer.println(commentStarter + " " + w.toString()); - } - } - } - -} diff --git a/src/net/sf/openrocket/file/GeneralRocketLoader.java b/src/net/sf/openrocket/file/GeneralRocketLoader.java deleted file mode 100644 index b6117f78..00000000 --- a/src/net/sf/openrocket/file/GeneralRocketLoader.java +++ /dev/null @@ -1,91 +0,0 @@ -package net.sf.openrocket.file; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.file.openrocket.OpenRocketLoader; - -import java.io.BufferedInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.zip.GZIPInputStream; - - -/** - * A rocket loader that auto-detects the document type and uses the appropriate - * loading. Supports loading of GZIPed files as well with transparent - * uncompression. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class GeneralRocketLoader extends RocketLoader { - - private static final int READ_BYTES = 300; - - private static final byte[] GZIP_SIGNATURE = { 31, -117 }; // 0x1f, 0x8b - private static final byte[] OPENROCKET_SIGNATURE = - "<openrocket".getBytes(Charset.forName("US-ASCII")); - private static final byte[] ROCKSIM_SIGNATURE = - "<RockSimDoc".getBytes(Charset.forName("US-ASCII")); - - private final OpenRocketLoader openRocketLoader = new OpenRocketLoader(); - - private final net.sf.openrocket.file.rocksim.importt.RocksimLoader rocksimLoader = new net.sf.openrocket.file.rocksim.importt.RocksimLoader(); - - @Override - protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, - RocketLoadException { - - // Check for mark() support - if (!source.markSupported()) { - source = new BufferedInputStream(source); - } - - // Read using mark() - byte[] buffer = new byte[READ_BYTES]; - int count; - source.mark(READ_BYTES + 10); - count = source.read(buffer); - source.reset(); - - if (count < 10) { - throw new RocketLoadException("Unsupported or corrupt file."); - } - - // Detect the appropriate loader - - // Check for GZIP - if (buffer[0] == GZIP_SIGNATURE[0] && buffer[1] == GZIP_SIGNATURE[1]) { - OpenRocketDocument doc = loadFromStream(new GZIPInputStream(source)); - doc.getDefaultStorageOptions().setCompressionEnabled(true); - return doc; - } - - // Check for OpenRocket - int match = 0; - for (int i=0; i < count; i++) { - if (buffer[i] == OPENROCKET_SIGNATURE[match]) { - match++; - if (match == OPENROCKET_SIGNATURE.length) { - return loadUsing(source, openRocketLoader); - } - } else { - match = 0; - } - } - - byte[] typeIdentifier = Arrays.copyOf(buffer, ROCKSIM_SIGNATURE.length); - if (Arrays.equals(ROCKSIM_SIGNATURE, typeIdentifier)) { - return loadUsing(source, rocksimLoader); - } - throw new RocketLoadException("Unsupported or corrupt file."); - } - - private OpenRocketDocument loadUsing(InputStream source, RocketLoader loader) - throws RocketLoadException { - warnings.clear(); - OpenRocketDocument doc = loader.load(source); - warnings.addAll(loader.getWarnings()); - return doc; - } -} diff --git a/src/net/sf/openrocket/file/Loader.java b/src/net/sf/openrocket/file/Loader.java deleted file mode 100644 index 005d4d66..00000000 --- a/src/net/sf/openrocket/file/Loader.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.sf.openrocket.file; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collection; - -public interface Loader<T> { - - public Collection<T> load(InputStream stream, String filename) throws IOException; - -} diff --git a/src/net/sf/openrocket/file/RocketLoadException.java b/src/net/sf/openrocket/file/RocketLoadException.java deleted file mode 100644 index 2fdd177b..00000000 --- a/src/net/sf/openrocket/file/RocketLoadException.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.file; - -public class RocketLoadException extends Exception { - - public RocketLoadException() { - } - - public RocketLoadException(String message) { - super(message); - } - - public RocketLoadException(Throwable cause) { - super(cause); - } - - public RocketLoadException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/file/RocketLoader.java b/src/net/sf/openrocket/file/RocketLoader.java deleted file mode 100644 index 4fc8dada..00000000 --- a/src/net/sf/openrocket/file/RocketLoader.java +++ /dev/null @@ -1,74 +0,0 @@ -package net.sf.openrocket.file; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.document.OpenRocketDocument; - - -public abstract class RocketLoader { - protected final WarningSet warnings = new WarningSet(); - - - /** - * Loads a rocket from the specified File object. - */ - public final OpenRocketDocument load(File source) throws RocketLoadException { - warnings.clear(); - InputStream stream = null; - - try { - - stream = new BufferedInputStream(new FileInputStream(source)); - return load(stream); - - } catch (FileNotFoundException e) { - throw new RocketLoadException("File not found: " + source); - } finally { - if (stream != null) { - try { - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - - /** - * Loads a rocket from the specified InputStream. - */ - public final OpenRocketDocument load(InputStream source) throws RocketLoadException { - warnings.clear(); - - try { - return loadFromStream(source); - } catch (RocketLoadException e) { - throw e; - } catch (IOException e) { - throw new RocketLoadException("I/O error: " + e.getMessage(), e); - } - } - - - - /** - * This method is called by the default implementations of {@link #load(File)} - * and {@link #load(InputStream)} to load the rocket. - * - * @throws RocketLoadException if an error occurs during loading. - */ - protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException, - RocketLoadException; - - - - public final WarningSet getWarnings() { - return warnings; - } -} diff --git a/src/net/sf/openrocket/file/RocketSaver.java b/src/net/sf/openrocket/file/RocketSaver.java deleted file mode 100644 index 5c18bb8d..00000000 --- a/src/net/sf/openrocket/file/RocketSaver.java +++ /dev/null @@ -1,102 +0,0 @@ -package net.sf.openrocket.file; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.StorageOptions; - - -public abstract class RocketSaver { - - /** - * Save the document to the specified file using the default storage options. - * - * @param dest the destination file. - * @param document the document to save. - * @throws IOException in case of an I/O error. - */ - public final void save(File dest, OpenRocketDocument document) throws IOException { - save(dest, document, document.getDefaultStorageOptions()); - } - - - /** - * Save the document to the specified file using the given storage options. - * - * @param dest the destination file. - * @param document the document to save. - * @param options the storage options. - * @throws IOException in case of an I/O error. - */ - public void save(File dest, OpenRocketDocument document, StorageOptions options) - throws IOException { - OutputStream s = new BufferedOutputStream(new FileOutputStream(dest)); - try { - save(s, document, options); - } finally { - s.close(); - } - } - - - /** - * Save the document to the specified output stream using the default storage options. - * - * @param dest the destination stream. - * @param doc the document to save. - * @throws IOException in case of an I/O error. - */ - public final void save(OutputStream dest, OpenRocketDocument doc) throws IOException { - save(dest, doc, doc.getDefaultStorageOptions()); - } - - - /** - * Save the document to the specified output stream using the given storage options. - * - * @param dest the destination stream. - * @param doc the document to save. - * @param options the storage options. - * @throws IOException in case of an I/O error. - */ - public abstract void save(OutputStream dest, OpenRocketDocument doc, - StorageOptions options) throws IOException; - - - - /** - * Provide an estimate of the file size when saving the document with the - * specified options. This is used as an indication to the user and when estimating - * file save progress. - * - * @param doc the document. - * @param options the save options, compression must be taken into account. - * @return the estimated number of bytes the storage would take. - */ - public abstract long estimateFileSize(OpenRocketDocument doc, StorageOptions options); - - - - - public static String escapeXML(String s) { - - s = s.replace("&", "&"); - s = s.replace("<", "<"); - s = s.replace(">", ">"); - s = s.replace("\"","""); - s = s.replace("'", "'"); - - for (int i=0; i < s.length(); i++) { - char n = s.charAt(i); - if (((n < 32) && (n != 9) && (n != 10) && (n != 13)) || (n == 127)) { - s = s.substring(0,i) + "&#" + ((int)n) + ";" + s.substring(i+1); - } - } - - return s; - } -} diff --git a/src/net/sf/openrocket/file/UnknownFileTypeException.java b/src/net/sf/openrocket/file/UnknownFileTypeException.java deleted file mode 100644 index bcea2bbb..00000000 --- a/src/net/sf/openrocket/file/UnknownFileTypeException.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.file; - -import java.io.IOException; - -/** - * An exception marking that a file type was not supported. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class UnknownFileTypeException extends IOException { - - public UnknownFileTypeException() { - } - - public UnknownFileTypeException(String message) { - super(message); - } - - public UnknownFileTypeException(Throwable cause) { - super(cause); - } - - public UnknownFileTypeException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/file/configuration/XmlContainerElement.java b/src/net/sf/openrocket/file/configuration/XmlContainerElement.java deleted file mode 100644 index c227f6d0..00000000 --- a/src/net/sf/openrocket/file/configuration/XmlContainerElement.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.sf.openrocket.file.configuration; - -import java.util.ArrayList; -import java.util.List; - -public class XmlContainerElement extends XmlElement { - - private ArrayList<XmlElement> subelements = new ArrayList<XmlElement>(); - - public XmlContainerElement(String name) { - super(name); - } - - - public void addElement(XmlElement element) { - subelements.add(element); - } - - @SuppressWarnings("unchecked") - public List<XmlElement> getElements() { - return (List<XmlElement>) subelements.clone(); - } - -} diff --git a/src/net/sf/openrocket/file/configuration/XmlContentElement.java b/src/net/sf/openrocket/file/configuration/XmlContentElement.java deleted file mode 100644 index bf469271..00000000 --- a/src/net/sf/openrocket/file/configuration/XmlContentElement.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.sf.openrocket.file.configuration; - -/** - * A simple XML element that contains textual content. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class XmlContentElement extends XmlElement { - - private String content = ""; - - public XmlContentElement(String name) { - super(name); - } - - - public String getContent() { - return content; - } - - public void setContent(String content) { - if (content == null) { - throw new IllegalArgumentException("XML content cannot be null"); - } - this.content = content; - } - -} diff --git a/src/net/sf/openrocket/file/configuration/XmlElement.java b/src/net/sf/openrocket/file/configuration/XmlElement.java deleted file mode 100644 index ee094340..00000000 --- a/src/net/sf/openrocket/file/configuration/XmlElement.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.sf.openrocket.file.configuration; - -import java.util.HashMap; -import java.util.Map; - -/** - * A base simple XML element. A simple XML element can contain either other XML elements - * (XmlContainerElement) or textual content (XmlContentElement), but not both. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class XmlElement { - - private final String name; - private final HashMap<String, String> attributes = new HashMap<String, String>(); - - - - public XmlElement(String name) { - this.name = name; - } - - - public String getName() { - return name; - } - - public void setAttribute(String key, String value) { - attributes.put(key, value); - } - - public void removeAttribute(String key) { - attributes.remove(key); - } - - public String getAttribute(String key) { - return attributes.get(key); - } - - @SuppressWarnings("unchecked") - public Map<String, String> getAttributes() { - return (Map<String, String>) attributes.clone(); - } - -} diff --git a/src/net/sf/openrocket/file/iterator/DirectoryIterator.java b/src/net/sf/openrocket/file/iterator/DirectoryIterator.java deleted file mode 100644 index c934482c..00000000 --- a/src/net/sf/openrocket/file/iterator/DirectoryIterator.java +++ /dev/null @@ -1,187 +0,0 @@ -package net.sf.openrocket.file.iterator; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.JarUtil; -import net.sf.openrocket.util.Pair; - -/** - * A DirectoryIterator that scans for files within a directory in the file system - * matching a FileFilter. The scan is optionally recursive. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DirectoryIterator extends FileIterator { - - private static final LogHelper logger = Application.getLogger(); - - private final FileFilter filter; - private final File[] files; - private final boolean recursive; - private int position = 0; - private DirectoryIterator subIterator = null; - - /** - * Sole constructor. - * - * @param directory the directory to read. - * @param filter the filter for selecting files. - * @throws IOException if the directory cannot be read. - */ - public DirectoryIterator(File directory, FileFilter filter, boolean recursive) - throws IOException { - - this.filter = filter; - this.recursive = recursive; - - this.files = directory.listFiles(new DirSelectionFileFilter(filter, recursive)); - if (this.files == null) { - throw new IOException("not a directory or IOException occurred when listing files " + - "from " + directory); - } - } - - - - - - @Override - protected Pair<String, InputStream> findNext() { - - // Check if we're recursing - if (subIterator != null) { - if (subIterator.hasNext()) { - return subIterator.next(); - } else { - subIterator.close(); - subIterator = null; - } - } - - // Scan through file entries - while (position < files.length) { - File file = files[position]; - position++; - - try { - if (recursive && file.isDirectory()) { - subIterator = new DirectoryIterator(file, filter, recursive); - if (subIterator.hasNext()) { - return subIterator.next(); - } else { - subIterator.close(); - subIterator = null; - continue; - } - } - - InputStream is = new BufferedInputStream(new FileInputStream(file)); - return new Pair<String, InputStream>(file.getName(), is); - } catch (IOException e) { - logger.warn("Error opening file/directory " + file, e); - } - } - return null; - } - - - - /** - * Return a DirectoryIterator for a directory that can be located either - * within the containing JAR file, in the classpath or in the current directory - * (searched in this order). The first place that contains matching files - * will be iterated through. - * - * @param directory the directory to search for. - * @param filter the filter for matching files in the directory. - * @return a DirectoryIterator for iterating through the files in the - * directory, or <code>null</code> if no directory containing - * matching files can be found. - */ - public static FileIterator findDirectory(String directory, FileFilter filter) { - FileIterator iterator = null; - - // Try to load from containing JAR file - File jarFile = JarUtil.getCurrentJarFile(); - if (jarFile != null) { - try { - iterator = new ZipDirectoryIterator(jarFile, directory, filter); - if (iterator.hasNext()) { - return iterator; - } - iterator.close(); - } catch (IOException e) { - logger.error("Error opening containing JAR file " + jarFile, e); - } - } - - - // Try to find directory as a system resource - URL url = ClassLoader.getSystemResource(directory); - if (url != null) { - try { - File dir = JarUtil.urlToFile(url); - iterator = new DirectoryIterator(dir, filter, true); - if (iterator.hasNext()) { - return iterator; - } - iterator.close(); - } catch (Exception e1) { - logger.error("Error opening directory from URL " + url); - } - } - - - // Try to open directory as such - try { - iterator = new DirectoryIterator(new File(directory), filter, true); - if (iterator.hasNext()) { - return iterator; - } - iterator.close(); - } catch (IOException e) { - logger.error("Error opening directory " + directory); - } - - return null; - } - - - - /** - * A FileFilter wrapper that accepts or discards directories. - */ - private class DirSelectionFileFilter implements FileFilter { - - private final boolean acceptDirs; - private final FileFilter parentFilter; - - - public DirSelectionFileFilter(FileFilter filter, boolean acceptDirs) { - this.acceptDirs = acceptDirs; - this.parentFilter = filter; - } - - - @Override - public boolean accept(File pathname) { - if (pathname.getName().startsWith(".")) { - return false; - } - if (pathname.isDirectory()) { - return acceptDirs; - } - return parentFilter.accept(pathname); - } - - } - -} diff --git a/src/net/sf/openrocket/file/iterator/FileIterator.java b/src/net/sf/openrocket/file/iterator/FileIterator.java deleted file mode 100644 index 14d220a9..00000000 --- a/src/net/sf/openrocket/file/iterator/FileIterator.java +++ /dev/null @@ -1,94 +0,0 @@ -package net.sf.openrocket.file.iterator; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Iterator; -import java.util.NoSuchElementException; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Pair; - -/** - * An abstract class for iterating over files fulfilling some condition. The files are - * returned as pairs of open InputStreams and file names. Conditions can be for example - * files in a directory matching a specific FileFilter. - * <p> - * Concrete implementations must implement the method {@link #findNext()} and possibly - * {@link #close()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class FileIterator implements Iterator<Pair<String, InputStream>> { - private static final LogHelper logger = Application.getLogger(); - - private Pair<String, InputStream> next = null; - private int fileCount = 0; - - @Override - public boolean hasNext() { - if (next != null) - return true; - - next = findNext(); - return (next != null); - } - - - @Override - public Pair<String, InputStream> next() { - if (next == null) { - next = findNext(); - } - if (next == null) { - throw new NoSuchElementException("No more files"); - } - - Pair<String, InputStream> n = next; - next = null; - fileCount++; - return n; - } - - - @Override - public void remove() { - throw new UnsupportedOperationException("remove() not supported"); - } - - - - /** - * Closes the resources related to this iterator. This method should be - * overridden if the iterator needs to close any resources of its own, but - * must call this method as well. - */ - public void close() { - if (next != null) { - try { - next.getV().close(); - } catch (IOException e) { - logger.error("Error closing file " + next.getU()); - } - next = null; - } - } - - - /** - * Return the number of files that have so far been returned by this iterator. - * - * @return the number of files that this iterator has returned so far. - */ - public int getFileCount() { - return fileCount; - } - - /** - * Return the next pair of file name and InputStream. - * - * @return a pair with the file name and input stream reading the file. - */ - protected abstract Pair<String, InputStream> findNext(); - -} diff --git a/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java b/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java deleted file mode 100644 index 3d3e6df5..00000000 --- a/src/net/sf/openrocket/file/iterator/ZipDirectoryIterator.java +++ /dev/null @@ -1,105 +0,0 @@ -package net.sf.openrocket.file.iterator; - -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.io.InputStream; -import java.util.Enumeration; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Pair; - -/** - * A DirectoryIterator that reads files from the specified directory of a - * ZIP (or JAR) file. - * - * TODO: MEDIUM: This is always a recursive search. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ZipDirectoryIterator extends FileIterator { - - private static final LogHelper logger = Application.getLogger(); - - private final File zipFileName; - private final String directory; - private final FileFilter filter; - - private ZipFile zipFile; - private Enumeration<? extends ZipEntry> entries; - - - /** - * Sole constructor. - * - * @param zipFileName the ZIP file to read. - * @param directory the directory within the ZIP file to read, relative to the - * base (an empty string corresponds to the root directory) - * @param filter the filter for accepted files. - * @throws IOException if the ZIP file could not be read. - */ - public ZipDirectoryIterator(File zipFileName, String directory, FileFilter filter) - throws IOException { - - // Process directory and extension - if (!directory.endsWith("/")) { - directory += "/"; - } - - this.zipFileName = zipFileName; - this.directory = directory; - this.filter = filter; - - - // Loop through ZIP entries searching for files to load - this.zipFile = new ZipFile(zipFileName); - entries = zipFile.entries(); - - } - - - @Override - public void close() { - super.close(); - if (zipFile != null) { - try { - zipFile.close(); - } catch (IOException e) { - logger.error("Closing ZIP file failed", e); - } - zipFile = null; - entries = null; - } - } - - - @Override - protected Pair<String, InputStream> findNext() { - if (entries == null) { - return null; - } - - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String name = entry.getName(); - File file = new File(name); - if (name.startsWith(directory) && filter.accept(file)) { - try { - InputStream is = zipFile.getInputStream(entry); - return new Pair<String, InputStream>(name, is); - } catch (IOException e) { - logger.error("IOException when reading ZIP file " + zipFileName, e); - } - } - } - - // No more elements exist - close(); - return null; - } - - -} diff --git a/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java b/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java deleted file mode 100644 index e9051f7a..00000000 --- a/src/net/sf/openrocket/file/motor/AbstractMotorLoader.java +++ /dev/null @@ -1,200 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.util.MathUtil; - -public abstract class AbstractMotorLoader implements MotorLoader { - - - /** - * {@inheritDoc} - * <p> - * This method delegates the reading to the loaded from the Reader using the charset - * returned by {@link #getDefaultCharset()}. - */ - public List<Motor> load(InputStream stream, String filename) throws IOException { - return load(new InputStreamReader(stream, getDefaultCharset()), filename); - } - - - /** - * Load motors from the specified <code>Reader</code>. - * - * @param reader the source of the motor definitions. - * @param filename the file name of the file, may be <code>null</code> if not - * applicable. - * @return a list of motors contained in the file. - * @throws IOException if an I/O exception occurs of the file format is invalid. - */ - protected abstract List<Motor> load(Reader reader, String filename) throws IOException; - - - - /** - * Return the default charset to use when loading rocket files of this type. - * <p> - * If the method {@link #load(InputStream, String)} is overridden as well, this - * method may return <code>null</code>. - * - * @return the charset to use when loading the rocket file. - */ - protected abstract Charset getDefaultCharset(); - - - - - ////////// Helper methods ////////// - - - /** - * Calculate the mass of a motor at distinct points in time based on the - * initial total mass, propellant weight and thrust. - * <p> - * This calculation assumes that the velocity of the exhaust remains constant - * during the burning. This derives from the mass-flow and thrust relation - * <pre>F = m' * v</pre> - * - * @param time list of time points - * @param thrust thrust at the discrete times - * @param total total weight of the motor - * @param prop propellant amount consumed during burning - * @return a list of the mass at the specified time points - */ - protected static List<Double> calculateMass(List<Double> time, List<Double> thrust, - double total, double prop) { - List<Double> mass = new ArrayList<Double>(); - List<Double> deltam = new ArrayList<Double>(); - - double t0, f0; - double totalMassChange = 0; - double scale; - - // First calculate mass change between points - t0 = time.get(0); - f0 = thrust.get(0); - for (int i = 1; i < time.size(); i++) { - double t1 = time.get(i); - double f1 = thrust.get(i); - - double dm = 0.5 * (f0 + f1) * (t1 - t0); - deltam.add(dm); - totalMassChange += dm; - t0 = t1; - f0 = f1; - } - - // Scale mass change and calculate mass - mass.add(total); - scale = prop / totalMassChange; - for (double dm : deltam) { - total -= dm * scale; - mass.add(total); - } - - return mass; - } - - - /** - * Helper method to remove a delay (or plugged) from the end of a motor designation, - * if present. - * - * @param designation the motor designation. - * @return the designation with a possible delay removed. - */ - protected static String removeDelay(String designation) { - if (designation.matches(".*-([0-9]+|[pP])$")) { - designation = designation.substring(0, designation.lastIndexOf('-')); - } - return designation; - } - - - - /** - * Helper method to tokenize a string using whitespace as the delimiter. - */ - protected static String[] split(String str) { - return split(str, "\\s+"); - } - - - /** - * Helper method to tokenize a string using the given delimiter. - */ - protected static String[] split(String str, String delim) { - String[] pieces = str.split(delim); - if (pieces.length == 0 || !pieces[0].equals("")) - return pieces; - return Arrays.copyOfRange(pieces, 1, pieces.length); - } - - - /** - * Sort the primary list and other lists in that order. - * - * @param primary the list to order. - * @param lists lists to order in the same permutation. - */ - protected static void sortLists(List<Double> primary, List<?>... lists) { - - // TODO: LOW: Very idiotic sort algorithm, but should be fast enough - // since the time should be sorted already - - int index; - - do { - for (index = 0; index < primary.size() - 1; index++) { - if (primary.get(index + 1) < primary.get(index)) { - Collections.swap(primary, index, index + 1); - for (List<?> l : lists) { - Collections.swap(l, index, index + 1); - } - break; - } - } - } while (index < primary.size() - 1); - } - - - - @SuppressWarnings("unchecked") - protected static void finalizeThrustCurve(List<Double> time, List<Double> thrust, - List... lists) { - - if (time.size() == 0) - return; - - // Start - if (!MathUtil.equals(time.get(0), 0) || !MathUtil.equals(thrust.get(0), 0)) { - time.add(0, 0.0); - thrust.add(0, 0.0); - for (List l : lists) { - Object o = l.get(0); - l.add(0, o); - } - } - - // End - int n = time.size() - 1; - if (!MathUtil.equals(thrust.get(n), 0)) { - time.add(time.get(n)); - thrust.add(0.0); - for (List l : lists) { - Object o = l.get(n); - l.add(o); - } - } - } - -} diff --git a/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java b/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java deleted file mode 100644 index 937c6790..00000000 --- a/src/net/sf/openrocket/file/motor/GeneralMotorLoader.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import net.sf.openrocket.file.UnknownFileTypeException; -import net.sf.openrocket.motor.Motor; - -/** - * A motor loader class that detects the file type based on the file name extension. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class GeneralMotorLoader implements MotorLoader { - - private final MotorLoader RASP_LOADER = new RASPMotorLoader(); - private final MotorLoader ROCKSIM_LOADER = new RockSimMotorLoader(); - private final MotorLoader ZIP_LOADER; - - - public GeneralMotorLoader() { - // Must use this loader in order to avoid recursive instantiation - ZIP_LOADER = new ZipFileMotorLoader(this); - } - - - - /** - * {@inheritDoc} - * - * @throws UnknownFileTypeException if the file format is not supported - */ - @Override - public List<Motor> load(InputStream stream, String filename) throws IOException { - return selectLoader(filename).load(stream, filename); - } - - - - /** - * Return an array containing the supported file extensions. - * - * @return an array of the supported file extensions. - */ - public String[] getSupportedExtensions() { - return new String[] { "rse", "eng", "zip" }; - } - - - /** - * Return the appropriate motor loader based on the file name. - * - * @param filename the file name (may be <code>null</code>). - * @return the appropriate motor loader to use for the file. - * @throws UnknownFileTypeException if the file type cannot be detected from the file name. - */ - private MotorLoader selectLoader(String filename) throws IOException { - if (filename == null) { - throw new UnknownFileTypeException("Unknown file type, filename=null"); - } - - String ext = ""; - int point = filename.lastIndexOf('.'); - - if (point > 0) - ext = filename.substring(point + 1); - - if (ext.equalsIgnoreCase("eng")) { - return RASP_LOADER; - } else if (ext.equalsIgnoreCase("rse")) { - return ROCKSIM_LOADER; - } else if (ext.equalsIgnoreCase("zip")) { - return ZIP_LOADER; - } - - throw new UnknownFileTypeException("Unknown file type, filename=" + filename); - } - -} diff --git a/src/net/sf/openrocket/file/motor/MotorLoader.java b/src/net/sf/openrocket/file/motor/MotorLoader.java deleted file mode 100644 index 73e14039..00000000 --- a/src/net/sf/openrocket/file/motor/MotorLoader.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import net.sf.openrocket.file.Loader; -import net.sf.openrocket.motor.Motor; - - -public interface MotorLoader extends Loader<Motor> { - - /** - * Load motors from the specified <code>InputStream</code>. - * - * @param stream the source of the motor definitions. - * @param filename the file name of the file, may be <code>null</code> if not - * applicable. - * @return a list of motors contained in the file. - * @throws IOException if an I/O exception occurs of the file format is invalid. - */ - public List<Motor> load(InputStream stream, String filename) throws IOException; - -} diff --git a/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java b/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java deleted file mode 100644 index 049738a4..00000000 --- a/src/net/sf/openrocket/file/motor/MotorLoaderHelper.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.file.iterator.DirectoryIterator; -import net.sf.openrocket.file.iterator.FileIterator; -import net.sf.openrocket.gui.util.SimpleFileFilter; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Pair; - -public final class MotorLoaderHelper { - - private static final LogHelper log = Application.getLogger(); - - private MotorLoaderHelper() { - // Prevent construction - } - - /** - * Load a file or directory of thrust curves. Directories are loaded - * recursively. Any errors during loading are logged, but otherwise ignored. - * - * @param target the file or directory to load. - * @return a list of all motors in the file/directory. - */ - public static List<Motor> load(File target) { - GeneralMotorLoader loader = new GeneralMotorLoader(); - - if (target.isDirectory()) { - - try { - return load(new DirectoryIterator(target, new SimpleFileFilter("", loader.getSupportedExtensions()), true)); - } catch (IOException e) { - log.warn("Could not read directory " + target, e); - return Collections.emptyList(); - } - - } else { - - InputStream is = null; - try { - is = new FileInputStream(target); - return loader.load(new BufferedInputStream(is), target.getName()); - } catch (IOException e) { - log.warn("Could not load file " + target, e); - return Collections.emptyList(); - } finally { - if (is != null) { - try { - is.close(); - } catch (IOException e) { - log.error("Could not close file " + target, e); - } - } - } - - } - } - - - /** - * Load motors from files iterated over by a FileIterator. Any errors during - * loading are logged, but otherwise ignored. - * <p> - * The iterator is closed at the end of the operation. - * - * @param iterator the FileIterator that iterates of the files to load. - * @return a list of all motors loaded. - */ - public static List<Motor> load(FileIterator iterator) { - GeneralMotorLoader loader = new GeneralMotorLoader(); - List<Motor> list = new ArrayList<Motor>(); - - while (iterator.hasNext()) { - final Pair<String, InputStream> input = iterator.next(); - log.debug("Loading motors from file " + input.getU()); - try { - List<Motor> motors = loader.load(input.getV(), input.getU()); - if (motors.size() == 0) { - log.warn("No motors found in file " + input.getU()); - } - for (Motor m : motors) { - list.add((ThrustCurveMotor) m); - } - } catch (IOException e) { - log.warn("IOException when loading motor file " + input.getU(), e); - } finally { - try { - input.getV().close(); - } catch (IOException e) { - log.error("IOException when closing InputStream", e); - } - } - } - iterator.close(); - - return list; - } - -} diff --git a/src/net/sf/openrocket/file/motor/RASPMotorLoader.java b/src/net/sf/openrocket/file/motor/RASPMotorLoader.java deleted file mode 100644 index ae653e66..00000000 --- a/src/net/sf/openrocket/file/motor/RASPMotorLoader.java +++ /dev/null @@ -1,216 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.Reader; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.motor.Manufacturer; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.MotorDigest.DataType; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.util.Coordinate; - -public class RASPMotorLoader extends AbstractMotorLoader { - - public static final String CHARSET_NAME = "ISO-8859-1"; - - public static final Charset CHARSET = Charset.forName(CHARSET_NAME); - - - - - @Override - protected Charset getDefaultCharset() { - return CHARSET; - } - - - /** - * Load a <code>Motor</code> from a RASP file specified by the <code>Reader</code>. - * The <code>Reader</code> is responsible for using the correct charset. - * <p> - * The CG is assumed to be located at the center of the motor casing and the mass - * is calculated from the thrust curve by assuming a constant exhaust velocity. - * - * @param reader the source of the file. - * @return a list of the {@link Motor} objects defined in the file. - * @throws IOException if an I/O error occurs or if the file format is illegal. - */ - @Override - public List<Motor> load(Reader reader, String filename) throws IOException { - List<Motor> motors = new ArrayList<Motor>(); - BufferedReader in = new BufferedReader(reader); - - String manufacturer = ""; - String designation = ""; - String comment = ""; - - double length = 0; - double diameter = 0; - ArrayList<Double> delays = null; - - List<Double> time = new ArrayList<Double>(); - List<Double> thrust = new ArrayList<Double>(); - - double propW = 0; - double totalW = 0; - - try { - String line; - String[] pieces, buf; - - line = in.readLine(); - main: while (line != null) { // Until EOF - - manufacturer = ""; - designation = ""; - comment = ""; - length = 0; - diameter = 0; - delays = new ArrayList<Double>(); - propW = 0; - totalW = 0; - time.clear(); - thrust.clear(); - - // Read comment - while (line.length() == 0 || line.charAt(0) == ';') { - if (line.length() > 0) { - comment += line.substring(1).trim() + "\n"; - } - line = in.readLine(); - if (line == null) - break main; - } - comment = comment.trim(); - - // Parse header line, example: - // F32 24 124 5-10-15-P .0377 .0695 RV - // desig diam len delays prop.w tot.w manufacturer - pieces = split(line); - if (pieces.length != 7) { - throw new IOException("Illegal file format."); - } - - designation = pieces[0]; - diameter = Double.parseDouble(pieces[1]) / 1000.0; - length = Double.parseDouble(pieces[2]) / 1000.0; - - if (pieces[3].equalsIgnoreCase("None")) { - - } else { - buf = split(pieces[3], "[-,]+"); - for (int i = 0; i < buf.length; i++) { - if (buf[i].equalsIgnoreCase("P") || - buf[i].equalsIgnoreCase("plugged")) { - delays.add(Motor.PLUGGED); - } else { - // Many RASP files have "100" as an only delay - double d = Double.parseDouble(buf[i]); - if (d < 99) - delays.add(d); - } - } - Collections.sort(delays); - } - - propW = Double.parseDouble(pieces[4]); - totalW = Double.parseDouble(pieces[5]); - manufacturer = pieces[6]; - - if (propW > totalW) { - throw new IOException("Propellant weight exceeds total weight in " + - "RASP file " + filename); - } - - // Read the data - for (line = in.readLine(); (line != null) && (line.length() == 0 || line.charAt(0) != ';'); line = in.readLine()) { - - buf = split(line); - if (buf.length == 0) { - continue; - } else if (buf.length == 2) { - - time.add(Double.parseDouble(buf[0])); - thrust.add(Double.parseDouble(buf[1])); - - } else { - throw new IOException("Illegal file format."); - } - } - - // Comment of EOF encountered, marks the start of the next motor - if (time.size() < 2) { - throw new IOException("Illegal file format, too short thrust-curve."); - } - double[] delayArray = new double[delays.size()]; - for (int i = 0; i < delays.size(); i++) { - delayArray[i] = delays.get(i); - } - motors.add(createRASPMotor(manufacturer, designation, comment, - length, diameter, delayArray, propW, totalW, time, thrust)); - } - - } catch (NumberFormatException e) { - - throw new IOException("Illegal file format."); - - } - - return motors; - } - - - /** - * Create a motor from RASP file data. - * @throws IOException if the data is illegal for a thrust curve - */ - private static Motor createRASPMotor(String manufacturer, String designation, - String comment, double length, double diameter, double[] delays, - double propW, double totalW, List<Double> time, List<Double> thrust) - throws IOException { - - // Add zero time/thrust if necessary - sortLists(time, thrust); - finalizeThrustCurve(time, thrust); - List<Double> mass = calculateMass(time, thrust, totalW, propW); - - double[] timeArray = new double[time.size()]; - double[] thrustArray = new double[time.size()]; - Coordinate[] cgArray = new Coordinate[time.size()]; - for (int i = 0; i < time.size(); i++) { - timeArray[i] = time.get(i); - thrustArray[i] = thrust.get(i); - cgArray[i] = new Coordinate(length / 2, 0, 0, mass.get(i)); - } - - designation = removeDelay(designation); - - // Create the motor digest from data available in RASP files - MotorDigest motorDigest = new MotorDigest(); - motorDigest.update(DataType.TIME_ARRAY, timeArray); - motorDigest.update(DataType.MASS_SPECIFIC, totalW, totalW - propW); - motorDigest.update(DataType.FORCE_PER_TIME, thrustArray); - // TODO: HIGH: Motor digest? - // final String digest = motorDigest.getDigest(); - - - try { - - Manufacturer m = Manufacturer.getManufacturer(manufacturer); - return new ThrustCurveMotor(m, designation, comment, m.getMotorType(), - delays, diameter, length, timeArray, thrustArray, cgArray); - - } catch (IllegalArgumentException e) { - - // Bad data read from file. - throw new IOException("Illegal file format.", e); - - } - } -} diff --git a/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java b/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java deleted file mode 100644 index d2bbcd26..00000000 --- a/src/net/sf/openrocket/file/motor/RockSimMotorLoader.java +++ /dev/null @@ -1,481 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.IOException; -import java.io.Reader; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.NullElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.file.simplesax.SimpleSAX; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Manufacturer; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.MotorDigest.DataType; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; - -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -public class RockSimMotorLoader extends AbstractMotorLoader { - - private static final LogHelper log = Application.getLogger(); - - public static final String CHARSET_NAME = "UTF-8"; - - public static final Charset CHARSET = Charset.forName(CHARSET_NAME); - - - /** Any delay longer than this will be interpreted as a plugged motor. */ - private static final int DELAY_LIMIT = 90; - - - - @Override - protected Charset getDefaultCharset() { - return CHARSET; - } - - - - /** - * Load a <code>Motor</code> from a RockSim motor definition file specified by the - * <code>Reader</code>. The <code>Reader</code> is responsible for using the correct - * charset. - * <p> - * If automatic CG/mass calculation is used, then the CG is assumed to be located at - * the center of the motor casing and the mass is calculated from the thrust curve - * by assuming a constant exhaust velocity. - * - * @param reader the source of the file. - * @return a list of the {@link Motor} objects defined in the file. - * @throws IOException if an I/O error occurs or if the file format is invalid. - */ - @Override - public List<Motor> load(Reader reader, String filename) throws IOException { - InputSource source = new InputSource(reader); - RSEHandler handler = new RSEHandler(); - WarningSet warnings = new WarningSet(); - - try { - SimpleSAX.readXML(source, handler, warnings); - return handler.getMotors(); - } catch (SAXException e) { - throw new IOException(e.getMessage(), e); - } - } - - - - /** - * Initial handler for the RockSim engine files. - */ - private static class RSEHandler extends ElementHandler { - private final List<Motor> motors = new ArrayList<Motor>(); - - private RSEMotorHandler motorHandler; - - public List<Motor> getMotors() { - return motors; - } - - @Override - public ElementHandler openElement(String element, - HashMap<String, String> attributes, WarningSet warnings) throws SAXException { - - if (element.equals("engine-database") || - element.equals("engine-list")) { - // Ignore <engine-database> and <engine-list> elements - return this; - } - - if (element.equals("version")) { - // Ignore <version> elements completely - return null; - } - - if (element.equals("engine")) { - motorHandler = new RSEMotorHandler(attributes); - return motorHandler; - } - - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - if (element.equals("engine")) { - Motor motor = motorHandler.getMotor(); - motors.add(motor); - } - } - } - - - /** - * Handler for a RockSim engine file <motor> element. - */ - private static class RSEMotorHandler extends ElementHandler { - - private final String manufacturer; - private final String designation; - private final double[] delays; - private final double diameter; - private final double length; - private final double initMass; - private final double propMass; - private final Motor.Type type; - private boolean calculateMass; - private boolean calculateCG; - - private String description = ""; - - private List<Double> time; - private List<Double> force; - private List<Double> mass; - private List<Double> cg; - - private RSEMotorDataHandler dataHandler = null; - - - public RSEMotorHandler(HashMap<String, String> attributes) throws SAXException { - String str; - - // Manufacturer - str = attributes.get("mfg"); - if (str == null) - throw new SAXException("Manufacturer missing"); - manufacturer = str; - - // Designation - str = attributes.get("code"); - if (str == null) - throw new SAXException("Designation missing"); - designation = removeDelay(str); - - // Delays - ArrayList<Double> delayList = new ArrayList<Double>(); - str = attributes.get("delays"); - if (str != null) { - String[] split = str.split(","); - for (String delay : split) { - try { - - double d = Double.parseDouble(delay); - if (d >= DELAY_LIMIT) - d = Motor.PLUGGED; - delayList.add(d); - - } catch (NumberFormatException e) { - if (str.equalsIgnoreCase("P") || str.equalsIgnoreCase("plugged")) { - delayList.add(Motor.PLUGGED); - } - } - } - } - delays = new double[delayList.size()]; - for (int i = 0; i < delayList.size(); i++) { - delays[i] = delayList.get(i); - } - - // Diameter - str = attributes.get("dia"); - if (str == null) - throw new SAXException("Diameter missing"); - try { - diameter = Double.parseDouble(str) / 1000.0; - } catch (NumberFormatException e) { - throw new SAXException("Invalid diameter " + str); - } - - // Length - str = attributes.get("len"); - if (str == null) - throw new SAXException("Length missing"); - try { - length = Double.parseDouble(str) / 1000.0; - } catch (NumberFormatException e) { - throw new SAXException("Invalid length " + str); - } - - // Initial mass - str = attributes.get("initWt"); - if (str == null) - throw new SAXException("Initial mass missing"); - try { - initMass = Double.parseDouble(str) / 1000.0; - } catch (NumberFormatException e) { - throw new SAXException("Invalid initial mass " + str); - } - - // Propellant mass - str = attributes.get("propWt"); - if (str == null) - throw new SAXException("Propellant mass missing"); - try { - propMass = Double.parseDouble(str) / 1000.0; - } catch (NumberFormatException e) { - throw new SAXException("Invalid propellant mass " + str); - } - - if (propMass > initMass) { - throw new SAXException("Propellant weight exceeds total weight in " + - "RockSim engine format"); - } - - // Motor type - str = attributes.get("Type"); - if (str != null && str.equalsIgnoreCase("single-use")) { - type = Motor.Type.SINGLE; - } else if (str != null && str.equalsIgnoreCase("hybrid")) { - type = Motor.Type.HYBRID; - } else if (str != null && str.equalsIgnoreCase("reloadable")) { - type = Motor.Type.RELOAD; - } else { - type = Motor.Type.UNKNOWN; - } - - // Calculate mass - str = attributes.get("auto-calc-mass"); - if ("0".equals(str) || "false".equalsIgnoreCase(str)) { - calculateMass = false; - } else { - calculateMass = true; - } - - // Calculate CG - str = attributes.get("auto-calc-cg"); - if ("0".equals(str) || "false".equalsIgnoreCase(str)) { - calculateCG = false; - } else { - calculateCG = true; - } - } - - @Override - public ElementHandler openElement(String element, - HashMap<String, String> attributes, WarningSet warnings) throws SAXException { - - if (element.equals("comments")) { - return PlainTextHandler.INSTANCE; - } - - if (element.equals("data")) { - if (dataHandler != null) { - throw new SAXException("Multiple data elements encountered in motor " + - "definition"); - } - dataHandler = new RSEMotorDataHandler(); - return dataHandler; - } - - warnings.add("Unknown element '" + element + "' encountered, ignoring."); - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - if (element.equals("comments")) { - if (description.length() > 0) { - description = description + "\n\n" + content.trim(); - } else { - description = content.trim(); - } - return; - } - - if (element.equals("data")) { - time = dataHandler.getTime(); - force = dataHandler.getForce(); - mass = dataHandler.getMass(); - cg = dataHandler.getCG(); - - sortLists(time, force, mass, cg); - - for (double d : mass) { - if (Double.isNaN(d)) { - calculateMass = true; - break; - } - } - for (double d : cg) { - if (Double.isNaN(d)) { - calculateCG = true; - break; - } - } - return; - } - } - - public Motor getMotor() throws SAXException { - if (time == null || time.size() == 0) - throw new SAXException("Illegal motor data"); - - - finalizeThrustCurve(time, force, mass, cg); - final int n = time.size(); - - if (hasIllegalValue(mass)) - calculateMass = true; - if (hasIllegalValue(cg)) - calculateCG = true; - - if (calculateMass) { - mass = calculateMass(time, force, initMass, propMass); - } - if (calculateCG) { - for (int i = 0; i < n; i++) { - cg.set(i, length / 2); - } - } - - double[] timeArray = toArray(time); - double[] thrustArray = toArray(force); - Coordinate[] cgArray = new Coordinate[n]; - for (int i = 0; i < n; i++) { - cgArray[i] = new Coordinate(cg.get(i), 0, 0, mass.get(i)); - } - - - // Create the motor digest from all data available in the file - MotorDigest motorDigest = new MotorDigest(); - motorDigest.update(DataType.TIME_ARRAY, timeArray); - if (!calculateMass) { - motorDigest.update(DataType.MASS_PER_TIME, toArray(mass)); - } else { - motorDigest.update(DataType.MASS_SPECIFIC, initMass, initMass - propMass); - } - if (!calculateCG) { - motorDigest.update(DataType.CG_PER_TIME, toArray(cg)); - } - motorDigest.update(DataType.FORCE_PER_TIME, thrustArray); - // TODO: HIGH: Motor digest? - // final String digest = motorDigest.getDigest(); - - - try { - Manufacturer m = Manufacturer.getManufacturer(manufacturer); - Motor.Type t = type; - if (t == Motor.Type.UNKNOWN) { - t = m.getMotorType(); - } else { - if (m.getMotorType() != Motor.Type.UNKNOWN && m.getMotorType() != t) { - log.warn("Loaded motor type inconsistent with manufacturer," + - " loaded type=" + t + " manufacturer=" + m + - " manufacturer type=" + m.getMotorType() + - " designation=" + designation); - } - } - - return new ThrustCurveMotor(m, designation, description, t, - delays, diameter, length, timeArray, thrustArray, cgArray); - } catch (IllegalArgumentException e) { - throw new SAXException("Illegal motor data", e); - } - } - } - - - /** - * Handler for the <data> element in a RockSim engine file motor definition. - */ - private static class RSEMotorDataHandler extends ElementHandler { - - private final List<Double> time = new ArrayList<Double>(); - private final List<Double> force = new ArrayList<Double>(); - private final List<Double> mass = new ArrayList<Double>(); - private final List<Double> cg = new ArrayList<Double>(); - - - public List<Double> getTime() { - return time; - } - - public List<Double> getForce() { - return force; - } - - public List<Double> getMass() { - return mass; - } - - public List<Double> getCG() { - return cg; - } - - - @Override - public ElementHandler openElement(String element, - HashMap<String, String> attributes, WarningSet warnings) { - - if (element.equals("eng-data")) { - return NullElementHandler.INSTANCE; - } - - warnings.add("Unknown element '" + element + "' encountered, ignoring."); - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - double t = parseDouble(attributes.get("t")); - double f = parseDouble(attributes.get("f")); - double m = parseDouble(attributes.get("m")) / 1000.0; - double g = parseDouble(attributes.get("cg")) / 1000.0; - - if (Double.isNaN(t) || Double.isNaN(f)) { - throw new SAXException("Illegal motor data point encountered"); - } - - time.add(t); - force.add(f); - mass.add(m); - cg.add(g); - } - - - private double parseDouble(String str) { - if (str == null) - return Double.NaN; - try { - return Double.parseDouble(str); - } catch (NumberFormatException e) { - return Double.NaN; - } - } - } - - - - private static boolean hasIllegalValue(List<Double> list) { - for (Double d : list) { - if (d == null || d.isNaN() || d.isInfinite()) { - return true; - } - } - return false; - } - - private static double[] toArray(List<Double> list) { - final int n = list.size(); - double[] array = new double[n]; - for (int i = 0; i < n; i++) { - array[i] = list.get(i); - } - return array; - } -} diff --git a/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java b/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java deleted file mode 100644 index 804226f0..00000000 --- a/src/net/sf/openrocket/file/motor/ZipFileMotorLoader.java +++ /dev/null @@ -1,85 +0,0 @@ -package net.sf.openrocket.file.motor; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import net.sf.openrocket.file.UnknownFileTypeException; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.UncloseableInputStream; - -/** - * A motor loader that loads motors from a ZIP file. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ZipFileMotorLoader implements MotorLoader { - private static final LogHelper log = Application.getLogger(); - - private final MotorLoader loader; - - - /** - * Construct a ZipFileMotorLoader that loads files using a - * {@link GeneralMotorLoader}. - */ - public ZipFileMotorLoader() { - this(new GeneralMotorLoader()); - } - - /** - * Constructs a ZipFileMotorLoader that loads files using the provided motor loader. - * - * @param loader the motor loader to use when loading. - */ - public ZipFileMotorLoader(MotorLoader loader) { - this.loader = loader; - } - - - @Override - public List<Motor> load(InputStream stream, String filename) throws IOException { - List<Motor> motors = new ArrayList<Motor>(); - - ZipInputStream is = new ZipInputStream(stream); - - // SAX seems to close the input stream, prevent it - InputStream uncloseable = new UncloseableInputStream(is); - - while (true) { - ZipEntry entry = is.getNextEntry(); - if (entry == null) - break; - - if (entry.isDirectory()) - continue; - - // Get the file name of the entry - String name = entry.getName(); - int index = name.lastIndexOf('/'); - if (index < 0) { - index = name.lastIndexOf('\\'); - } - if (index >= 0) { - name = name.substring(index + 1); - } - - try { - List<Motor> m = loader.load(uncloseable, entry.getName()); - motors.addAll(m); - log.info("Loaded " + m.size() + " motors from ZIP entry " + entry.getName()); - } catch (UnknownFileTypeException e) { - log.info("Could not read ZIP entry " + entry.getName() + ": " + e.getMessage()); - } - - } - - return motors; - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java b/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java deleted file mode 100644 index c4fc7d73..00000000 --- a/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java +++ /dev/null @@ -1,2074 +0,0 @@ -package net.sf.openrocket.file.openrocket; - -import java.io.IOException; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.database.Databases; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.document.Simulation.Status; -import net.sf.openrocket.document.StorageOptions; -import net.sf.openrocket.file.RocketLoadException; -import net.sf.openrocket.file.RocketLoader; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.file.simplesax.SimpleSAX; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.rocketcomponent.BodyComponent; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.ClusterConfiguration; -import net.sf.openrocket.rocketcomponent.Clusterable; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FinSet.TabRelativePosition; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.InternalComponent; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RadiusRingComponent; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.ReferenceType; -import net.sf.openrocket.rocketcomponent.RingComponent; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent.Position; -import net.sf.openrocket.rocketcomponent.ShockCord; -import net.sf.openrocket.rocketcomponent.Stage; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.StructuralComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.FlightEvent.Type; -import net.sf.openrocket.simulation.SimulationOptions; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Color; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.GeodeticComputationStrategy; -import net.sf.openrocket.util.LineStyle; -import net.sf.openrocket.util.Reflection; - -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - - -/** - * Class that loads a rocket definition from an OpenRocket rocket file. - * <p> - * This class uses SAX to read the XML file format. The - * {@link #loadFromStream(InputStream)} method simply sets the system up and - * starts the parsing, while the actual logic is in the private inner class - * <code>OpenRocketHandler</code>. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OpenRocketLoader extends RocketLoader { - private static final LogHelper log = Application.getLogger(); - - - @Override - public OpenRocketDocument loadFromStream(InputStream source) throws RocketLoadException, - IOException { - log.info("Loading .ork file"); - - InputSource xmlSource = new InputSource(source); - OpenRocketHandler handler = new OpenRocketHandler(); - - - try { - SimpleSAX.readXML(xmlSource, handler, warnings); - } catch (SAXException e) { - log.warn("Malformed XML in input"); - throw new RocketLoadException("Malformed XML in input.", e); - } - - - OpenRocketDocument doc = handler.getDocument(); - doc.getDefaultConfiguration().setAllStages(); - - // Deduce suitable time skip - double timeSkip = StorageOptions.SIMULATION_DATA_NONE; - for (Simulation s : doc.getSimulations()) { - if (s.getStatus() == Simulation.Status.EXTERNAL || - s.getStatus() == Simulation.Status.NOT_SIMULATED) - continue; - if (s.getSimulatedData() == null) - continue; - if (s.getSimulatedData().getBranchCount() == 0) - continue; - FlightDataBranch branch = s.getSimulatedData().getBranch(0); - if (branch == null) - continue; - List<Double> list = branch.get(FlightDataType.TYPE_TIME); - if (list == null) - continue; - - double previousTime = Double.NaN; - for (double time : list) { - if (time - previousTime < timeSkip) - timeSkip = time - previousTime; - previousTime = time; - } - } - // Round value - timeSkip = Math.rint(timeSkip * 100) / 100; - - doc.getDefaultStorageOptions().setSimulationTimeSkip(timeSkip); - doc.getDefaultStorageOptions().setCompressionEnabled(false); // Set by caller if compressed - doc.getDefaultStorageOptions().setExplicitlySet(false); - - doc.clearUndo(); - log.info("Loading done"); - return doc; - } - -} - - - -class DocumentConfig { - - /* Remember to update OpenRocketSaver as well! */ - public static final String[] SUPPORTED_VERSIONS = { "0.9", "1.0", "1.1", "1.2", "1.3" }; - - - //////// Component constructors - static final HashMap<String, Constructor<? extends RocketComponent>> constructors = new HashMap<String, Constructor<? extends RocketComponent>>(); - static { - try { - // External components - constructors.put("bodytube", BodyTube.class.getConstructor(new Class<?>[0])); - constructors.put("transition", Transition.class.getConstructor(new Class<?>[0])); - constructors.put("nosecone", NoseCone.class.getConstructor(new Class<?>[0])); - constructors.put("trapezoidfinset", TrapezoidFinSet.class.getConstructor(new Class<?>[0])); - constructors.put("ellipticalfinset", EllipticalFinSet.class.getConstructor(new Class<?>[0])); - constructors.put("freeformfinset", FreeformFinSet.class.getConstructor(new Class<?>[0])); - constructors.put("launchlug", LaunchLug.class.getConstructor(new Class<?>[0])); - - // Internal components - constructors.put("engineblock", EngineBlock.class.getConstructor(new Class<?>[0])); - constructors.put("innertube", InnerTube.class.getConstructor(new Class<?>[0])); - constructors.put("tubecoupler", TubeCoupler.class.getConstructor(new Class<?>[0])); - constructors.put("bulkhead", Bulkhead.class.getConstructor(new Class<?>[0])); - constructors.put("centeringring", CenteringRing.class.getConstructor(new Class<?>[0])); - - constructors.put("masscomponent", MassComponent.class.getConstructor(new Class<?>[0])); - constructors.put("shockcord", ShockCord.class.getConstructor(new Class<?>[0])); - constructors.put("parachute", Parachute.class.getConstructor(new Class<?>[0])); - constructors.put("streamer", Streamer.class.getConstructor(new Class<?>[0])); - - // Other - constructors.put("stage", Stage.class.getConstructor(new Class<?>[0])); - - } catch (NoSuchMethodException e) { - throw new BugException( - "Error in constructing the 'constructors' HashMap."); - } - } - - - //////// Parameter setters - /* - * The keys are of the form Class:param, where Class is the class name and param - * the element name. Setters are searched for in descending class order. - * A setter of null means setting the parameter is not allowed. - */ - static final HashMap<String, Setter> setters = new HashMap<String, Setter>(); - static { - // RocketComponent - setters.put("RocketComponent:name", new StringSetter( - Reflection.findMethod(RocketComponent.class, "setName", String.class))); - setters.put("RocketComponent:color", new ColorSetter( - Reflection.findMethod(RocketComponent.class, "setColor", Color.class))); - setters.put("RocketComponent:linestyle", new EnumSetter<LineStyle>( - Reflection.findMethod(RocketComponent.class, "setLineStyle", LineStyle.class), - LineStyle.class)); - setters.put("RocketComponent:position", new PositionSetter()); - setters.put("RocketComponent:overridemass", new OverrideSetter( - Reflection.findMethod(RocketComponent.class, "setOverrideMass", double.class), - Reflection.findMethod(RocketComponent.class, "setMassOverridden", boolean.class))); - setters.put("RocketComponent:overridecg", new OverrideSetter( - Reflection.findMethod(RocketComponent.class, "setOverrideCGX", double.class), - Reflection.findMethod(RocketComponent.class, "setCGOverridden", boolean.class))); - setters.put("RocketComponent:overridesubcomponents", new BooleanSetter( - Reflection.findMethod(RocketComponent.class, "setOverrideSubcomponents", boolean.class))); - setters.put("RocketComponent:comment", new StringSetter( - Reflection.findMethod(RocketComponent.class, "setComment", String.class))); - - // ExternalComponent - setters.put("ExternalComponent:finish", new EnumSetter<Finish>( - Reflection.findMethod(ExternalComponent.class, "setFinish", Finish.class), - Finish.class)); - setters.put("ExternalComponent:material", new MaterialSetter( - Reflection.findMethod(ExternalComponent.class, "setMaterial", Material.class), - Material.Type.BULK)); - - // BodyComponent - setters.put("BodyComponent:length", new DoubleSetter( - Reflection.findMethod(BodyComponent.class, "setLength", double.class))); - - // SymmetricComponent - setters.put("SymmetricComponent:thickness", new DoubleSetter( - Reflection.findMethod(SymmetricComponent.class, "setThickness", double.class), - "filled", - Reflection.findMethod(SymmetricComponent.class, "setFilled", boolean.class))); - - // BodyTube - setters.put("BodyTube:radius", new DoubleSetter( - Reflection.findMethod(BodyTube.class, "setOuterRadius", double.class), - "auto", - Reflection.findMethod(BodyTube.class, "setOuterRadiusAutomatic", boolean.class))); - - // Transition - setters.put("Transition:shape", new EnumSetter<Transition.Shape>( - Reflection.findMethod(Transition.class, "setType", Transition.Shape.class), - Transition.Shape.class)); - setters.put("Transition:shapeclipped", new BooleanSetter( - Reflection.findMethod(Transition.class, "setClipped", boolean.class))); - setters.put("Transition:shapeparameter", new DoubleSetter( - Reflection.findMethod(Transition.class, "setShapeParameter", double.class))); - - setters.put("Transition:foreradius", new DoubleSetter( - Reflection.findMethod(Transition.class, "setForeRadius", double.class), - "auto", - Reflection.findMethod(Transition.class, "setForeRadiusAutomatic", boolean.class))); - setters.put("Transition:aftradius", new DoubleSetter( - Reflection.findMethod(Transition.class, "setAftRadius", double.class), - "auto", - Reflection.findMethod(Transition.class, "setAftRadiusAutomatic", boolean.class))); - - setters.put("Transition:foreshoulderradius", new DoubleSetter( - Reflection.findMethod(Transition.class, "setForeShoulderRadius", double.class))); - setters.put("Transition:foreshoulderlength", new DoubleSetter( - Reflection.findMethod(Transition.class, "setForeShoulderLength", double.class))); - setters.put("Transition:foreshoulderthickness", new DoubleSetter( - Reflection.findMethod(Transition.class, "setForeShoulderThickness", double.class))); - setters.put("Transition:foreshouldercapped", new BooleanSetter( - Reflection.findMethod(Transition.class, "setForeShoulderCapped", boolean.class))); - - setters.put("Transition:aftshoulderradius", new DoubleSetter( - Reflection.findMethod(Transition.class, "setAftShoulderRadius", double.class))); - setters.put("Transition:aftshoulderlength", new DoubleSetter( - Reflection.findMethod(Transition.class, "setAftShoulderLength", double.class))); - setters.put("Transition:aftshoulderthickness", new DoubleSetter( - Reflection.findMethod(Transition.class, "setAftShoulderThickness", double.class))); - setters.put("Transition:aftshouldercapped", new BooleanSetter( - Reflection.findMethod(Transition.class, "setAftShoulderCapped", boolean.class))); - - // NoseCone - disable disallowed elements - setters.put("NoseCone:foreradius", null); - setters.put("NoseCone:foreshoulderradius", null); - setters.put("NoseCone:foreshoulderlength", null); - setters.put("NoseCone:foreshoulderthickness", null); - setters.put("NoseCone:foreshouldercapped", null); - - // FinSet - setters.put("FinSet:fincount", new IntSetter( - Reflection.findMethod(FinSet.class, "setFinCount", int.class))); - setters.put("FinSet:rotation", new DoubleSetter( - Reflection.findMethod(FinSet.class, "setBaseRotation", double.class), Math.PI / 180.0)); - setters.put("FinSet:thickness", new DoubleSetter( - Reflection.findMethod(FinSet.class, "setThickness", double.class))); - setters.put("FinSet:crosssection", new EnumSetter<FinSet.CrossSection>( - Reflection.findMethod(FinSet.class, "setCrossSection", FinSet.CrossSection.class), - FinSet.CrossSection.class)); - setters.put("FinSet:cant", new DoubleSetter( - Reflection.findMethod(FinSet.class, "setCantAngle", double.class), Math.PI / 180.0)); - setters.put("FinSet:tabheight", new DoubleSetter( - Reflection.findMethod(FinSet.class, "setTabHeight", double.class))); - setters.put("FinSet:tablength", new DoubleSetter( - Reflection.findMethod(FinSet.class, "setTabLength", double.class))); - setters.put("FinSet:tabposition", new FinTabPositionSetter()); - - // TrapezoidFinSet - setters.put("TrapezoidFinSet:rootchord", new DoubleSetter( - Reflection.findMethod(TrapezoidFinSet.class, "setRootChord", double.class))); - setters.put("TrapezoidFinSet:tipchord", new DoubleSetter( - Reflection.findMethod(TrapezoidFinSet.class, "setTipChord", double.class))); - setters.put("TrapezoidFinSet:sweeplength", new DoubleSetter( - Reflection.findMethod(TrapezoidFinSet.class, "setSweep", double.class))); - setters.put("TrapezoidFinSet:height", new DoubleSetter( - Reflection.findMethod(TrapezoidFinSet.class, "setHeight", double.class))); - - // EllipticalFinSet - setters.put("EllipticalFinSet:rootchord", new DoubleSetter( - Reflection.findMethod(EllipticalFinSet.class, "setLength", double.class))); - setters.put("EllipticalFinSet:height", new DoubleSetter( - Reflection.findMethod(EllipticalFinSet.class, "setHeight", double.class))); - - // FreeformFinSet points handled as a special handler - - // LaunchLug - setters.put("LaunchLug:radius", new DoubleSetter( - Reflection.findMethod(LaunchLug.class, "setOuterRadius", double.class))); - setters.put("LaunchLug:length", new DoubleSetter( - Reflection.findMethod(LaunchLug.class, "setLength", double.class))); - setters.put("LaunchLug:thickness", new DoubleSetter( - Reflection.findMethod(LaunchLug.class, "setThickness", double.class))); - setters.put("LaunchLug:radialdirection", new DoubleSetter( - Reflection.findMethod(LaunchLug.class, "setRadialDirection", double.class), - Math.PI / 180.0)); - - // InternalComponent - nothing - - // StructuralComponent - setters.put("StructuralComponent:material", new MaterialSetter( - Reflection.findMethod(StructuralComponent.class, "setMaterial", Material.class), - Material.Type.BULK)); - - // RingComponent - setters.put("RingComponent:length", new DoubleSetter( - Reflection.findMethod(RingComponent.class, "setLength", double.class))); - setters.put("RingComponent:radialposition", new DoubleSetter( - Reflection.findMethod(RingComponent.class, "setRadialPosition", double.class))); - setters.put("RingComponent:radialdirection", new DoubleSetter( - Reflection.findMethod(RingComponent.class, "setRadialDirection", double.class), - Math.PI / 180.0)); - - // ThicknessRingComponent - radius on separate components due to differing automatics - setters.put("ThicknessRingComponent:thickness", new DoubleSetter( - Reflection.findMethod(ThicknessRingComponent.class, "setThickness", double.class))); - - // EngineBlock - setters.put("EngineBlock:outerradius", new DoubleSetter( - Reflection.findMethod(EngineBlock.class, "setOuterRadius", double.class), - "auto", - Reflection.findMethod(EngineBlock.class, "setOuterRadiusAutomatic", boolean.class))); - - // TubeCoupler - setters.put("TubeCoupler:outerradius", new DoubleSetter( - Reflection.findMethod(TubeCoupler.class, "setOuterRadius", double.class), - "auto", - Reflection.findMethod(TubeCoupler.class, "setOuterRadiusAutomatic", boolean.class))); - - // InnerTube - setters.put("InnerTube:outerradius", new DoubleSetter( - Reflection.findMethod(InnerTube.class, "setOuterRadius", double.class))); - setters.put("InnerTube:clusterconfiguration", new ClusterConfigurationSetter()); - setters.put("InnerTube:clusterscale", new DoubleSetter( - Reflection.findMethod(InnerTube.class, "setClusterScale", double.class))); - setters.put("InnerTube:clusterrotation", new DoubleSetter( - Reflection.findMethod(InnerTube.class, "setClusterRotation", double.class), - Math.PI / 180.0)); - - // RadiusRingComponent - - // Bulkhead - setters.put("RadiusRingComponent:innerradius", new DoubleSetter( - Reflection.findMethod(RadiusRingComponent.class, "setInnerRadius", double.class))); - setters.put("Bulkhead:outerradius", new DoubleSetter( - Reflection.findMethod(Bulkhead.class, "setOuterRadius", double.class), - "auto", - Reflection.findMethod(Bulkhead.class, "setOuterRadiusAutomatic", boolean.class))); - - // CenteringRing - setters.put("CenteringRing:innerradius", new DoubleSetter( - Reflection.findMethod(CenteringRing.class, "setInnerRadius", double.class), - "auto", - Reflection.findMethod(CenteringRing.class, "setInnerRadiusAutomatic", boolean.class))); - setters.put("CenteringRing:outerradius", new DoubleSetter( - Reflection.findMethod(CenteringRing.class, "setOuterRadius", double.class), - "auto", - Reflection.findMethod(CenteringRing.class, "setOuterRadiusAutomatic", boolean.class))); - - - // MassObject - setters.put("MassObject:packedlength", new DoubleSetter( - Reflection.findMethod(MassObject.class, "setLength", double.class))); - setters.put("MassObject:packedradius", new DoubleSetter( - Reflection.findMethod(MassObject.class, "setRadius", double.class))); - setters.put("MassObject:radialposition", new DoubleSetter( - Reflection.findMethod(MassObject.class, "setRadialPosition", double.class))); - setters.put("MassObject:radialdirection", new DoubleSetter( - Reflection.findMethod(MassObject.class, "setRadialDirection", double.class), - Math.PI / 180.0)); - - // MassComponent - setters.put("MassComponent:mass", new DoubleSetter( - Reflection.findMethod(MassComponent.class, "setComponentMass", double.class))); - - // ShockCord - setters.put("ShockCord:cordlength", new DoubleSetter( - Reflection.findMethod(ShockCord.class, "setCordLength", double.class))); - setters.put("ShockCord:material", new MaterialSetter( - Reflection.findMethod(ShockCord.class, "setMaterial", Material.class), - Material.Type.LINE)); - - // RecoveryDevice - setters.put("RecoveryDevice:cd", new DoubleSetter( - Reflection.findMethod(RecoveryDevice.class, "setCD", double.class), - "auto", - Reflection.findMethod(RecoveryDevice.class, "setCDAutomatic", boolean.class))); - setters.put("RecoveryDevice:deployevent", new EnumSetter<RecoveryDevice.DeployEvent>( - Reflection.findMethod(RecoveryDevice.class, "setDeployEvent", RecoveryDevice.DeployEvent.class), - RecoveryDevice.DeployEvent.class)); - setters.put("RecoveryDevice:deployaltitude", new DoubleSetter( - Reflection.findMethod(RecoveryDevice.class, "setDeployAltitude", double.class))); - setters.put("RecoveryDevice:deploydelay", new DoubleSetter( - Reflection.findMethod(RecoveryDevice.class, "setDeployDelay", double.class))); - setters.put("RecoveryDevice:material", new MaterialSetter( - Reflection.findMethod(RecoveryDevice.class, "setMaterial", Material.class), - Material.Type.SURFACE)); - - // Parachute - setters.put("Parachute:diameter", new DoubleSetter( - Reflection.findMethod(Parachute.class, "setDiameter", double.class))); - setters.put("Parachute:linecount", new IntSetter( - Reflection.findMethod(Parachute.class, "setLineCount", int.class))); - setters.put("Parachute:linelength", new DoubleSetter( - Reflection.findMethod(Parachute.class, "setLineLength", double.class))); - setters.put("Parachute:linematerial", new MaterialSetter( - Reflection.findMethod(Parachute.class, "setLineMaterial", Material.class), - Material.Type.LINE)); - - // Streamer - setters.put("Streamer:striplength", new DoubleSetter( - Reflection.findMethod(Streamer.class, "setStripLength", double.class))); - setters.put("Streamer:stripwidth", new DoubleSetter( - Reflection.findMethod(Streamer.class, "setStripWidth", double.class))); - - // Rocket - // <motorconfiguration> handled by separate handler - setters.put("Rocket:referencetype", new EnumSetter<ReferenceType>( - Reflection.findMethod(Rocket.class, "setReferenceType", ReferenceType.class), - ReferenceType.class)); - setters.put("Rocket:customreference", new DoubleSetter( - Reflection.findMethod(Rocket.class, "setCustomReferenceLength", double.class))); - setters.put("Rocket:designer", new StringSetter( - Reflection.findMethod(Rocket.class, "setDesigner", String.class))); - setters.put("Rocket:revision", new StringSetter( - Reflection.findMethod(Rocket.class, "setRevision", String.class))); - } - - - /** - * Search for a enum value that has the corresponding name as an XML value. The current - * conversion from enum name to XML value is to lowercase the name and strip out all - * underscore characters. This method returns a match to these criteria, or <code>null</code> - * if no such enum exists. - * - * @param <T> then enum type. - * @param name the XML value, null ok. - * @param enumClass the class of the enum. - * @return the found enum value, or <code>null</code>. - */ - public static <T extends Enum<T>> Enum<T> findEnum(String name, - Class<? extends Enum<T>> enumClass) { - - if (name == null) - return null; - name = name.trim(); - for (Enum<T> e : enumClass.getEnumConstants()) { - if (e.name().toLowerCase().replace("_", "").equals(name)) { - return e; - } - } - return null; - } - - - /** - * Convert a string to a double including formatting specifications of the OpenRocket - * file format. This accepts all formatting that is valid for - * <code>Double.parseDouble(s)</code> and a few others as well ("Inf", "-Inf"). - * - * @param s the string to parse. - * @return the numerical value. - * @throws NumberFormatException the the string cannot be parsed. - */ - public static double stringToDouble(String s) throws NumberFormatException { - if (s == null) - throw new NumberFormatException("null string"); - if (s.equalsIgnoreCase("NaN")) - return Double.NaN; - if (s.equalsIgnoreCase("Inf")) - return Double.POSITIVE_INFINITY; - if (s.equalsIgnoreCase("-Inf")) - return Double.NEGATIVE_INFINITY; - return Double.parseDouble(s); - } -} - - - - - -/** - * The starting point of the handlers. Accepts a single <openrocket> element and hands - * the contents to be read by a OpenRocketContentsHandler. - */ -class OpenRocketHandler extends ElementHandler { - private OpenRocketContentHandler handler = null; - - /** - * Return the OpenRocketDocument read from the file, or <code>null</code> if a document - * has not been read yet. - * - * @return the document read, or null. - */ - public OpenRocketDocument getDocument() { - return handler.getDocument(); - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - // Check for unknown elements - if (!element.equals("openrocket")) { - warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); - return null; - } - - // Check for first call - if (handler != null) { - warnings.add(Warning.fromString("Multiple document elements found, ignoring later " - + "ones.")); - return null; - } - - // Check version number - String version = null; - String creator = attributes.remove("creator"); - String docVersion = attributes.remove("version"); - for (String v : DocumentConfig.SUPPORTED_VERSIONS) { - if (v.equals(docVersion)) { - version = v; - break; - } - } - if (version == null) { - String str = "Unsupported document version"; - if (docVersion != null) - str += " " + docVersion; - if (creator != null && !creator.trim().equals("")) - str += " (written using '" + creator.trim() + "')"; - str += ", attempting to read file anyway."; - warnings.add(str); - } - - handler = new OpenRocketContentHandler(); - return handler; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - attributes.remove("version"); - attributes.remove("creator"); - super.closeElement(element, attributes, content, warnings); - } - - -} - - -/** - * Handles the content of the <openrocket> tag. - */ -class OpenRocketContentHandler extends ElementHandler { - private final OpenRocketDocument doc; - private final Rocket rocket; - - private boolean rocketDefined = false; - private boolean simulationsDefined = false; - - public OpenRocketContentHandler() { - this.rocket = new Rocket(); - this.doc = new OpenRocketDocument(rocket); - } - - - public OpenRocketDocument getDocument() { - if (!rocketDefined) - return null; - return doc; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (element.equals("rocket")) { - if (rocketDefined) { - warnings.add(Warning - .fromString("Multiple rocket designs within one document, " - + "ignoring later ones.")); - return null; - } - rocketDefined = true; - return new ComponentParameterHandler(rocket); - } - - if (element.equals("simulations")) { - if (simulationsDefined) { - warnings.add(Warning - .fromString("Multiple simulation definitions within one document, " - + "ignoring later ones.")); - return null; - } - simulationsDefined = true; - return new SimulationsHandler(doc); - } - - warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); - - return null; - } -} - - - - -/** - * A handler that creates components from the corresponding elements. The control of the - * contents is passed on to ComponentParameterHandler. - */ -class ComponentHandler extends ElementHandler { - private final RocketComponent parent; - - public ComponentHandler(RocketComponent parent) { - this.parent = parent; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - // Attempt to construct new component - Constructor<? extends RocketComponent> constructor = DocumentConfig.constructors - .get(element); - if (constructor == null) { - warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); - return null; - } - - RocketComponent c; - try { - c = constructor.newInstance(); - } catch (InstantiationException e) { - throw new BugException("Error constructing component.", e); - } catch (IllegalAccessException e) { - throw new BugException("Error constructing component.", e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - - parent.addChild(c); - - return new ComponentParameterHandler(c); - } -} - - -/** - * A handler that populates the parameters of a previously constructed rocket component. - * This uses the setters, or delegates the handling to another handler for specific - * elements. - */ -class ComponentParameterHandler extends ElementHandler { - private final RocketComponent component; - - public ComponentParameterHandler(RocketComponent c) { - this.component = c; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - // Check for specific elements that contain other elements - if (element.equals("subcomponents")) { - return new ComponentHandler(component); - } - if (element.equals("motormount")) { - if (!(component instanceof MotorMount)) { - warnings.add(Warning.fromString("Illegal component defined as motor mount.")); - return null; - } - return new MotorMountHandler((MotorMount) component); - } - if (element.equals("finpoints")) { - if (!(component instanceof FreeformFinSet)) { - warnings.add(Warning.fromString("Illegal component defined for fin points.")); - return null; - } - return new FinSetPointHandler((FreeformFinSet) component); - } - if (element.equals("motorconfiguration")) { - if (!(component instanceof Rocket)) { - warnings.add(Warning.fromString("Illegal component defined for motor configuration.")); - return null; - } - return new MotorConfigurationHandler((Rocket) component); - } - - - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - if (element.equals("subcomponents") || element.equals("motormount") || - element.equals("finpoints") || element.equals("motorconfiguration")) { - return; - } - - // Search for the correct setter class - - Class<?> c; - for (c = component.getClass(); c != null; c = c.getSuperclass()) { - String setterKey = c.getSimpleName() + ":" + element; - Setter s = DocumentConfig.setters.get(setterKey); - if (s != null) { - // Setter found - s.set(component, content, attributes, warnings); - break; - } - if (DocumentConfig.setters.containsKey(setterKey)) { - // Key exists but is null -> invalid parameter - c = null; - break; - } - } - if (c == null) { - warnings.add(Warning.fromString("Unknown parameter type '" + element + "' for " - + component.getComponentName() + ", ignoring.")); - } - } -} - - -/** - * A handler that reads the <point> specifications within the freeformfinset's - * <finpoints> elements. - */ -class FinSetPointHandler extends ElementHandler { - private final FreeformFinSet finset; - private final ArrayList<Coordinate> coordinates = new ArrayList<Coordinate>(); - - public FinSetPointHandler(FreeformFinSet finset) { - this.finset = finset; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - String strx = attributes.remove("x"); - String stry = attributes.remove("y"); - if (strx == null || stry == null) { - warnings.add(Warning.fromString("Illegal fin points specification, ignoring.")); - return; - } - try { - double x = Double.parseDouble(strx); - double y = Double.parseDouble(stry); - coordinates.add(new Coordinate(x, y)); - } catch (NumberFormatException e) { - warnings.add(Warning.fromString("Illegal fin points specification, ignoring.")); - return; - } - - super.closeElement(element, attributes, content, warnings); - } - - @Override - public void endHandler(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - try { - finset.setPoints(coordinates.toArray(new Coordinate[0])); - } catch (IllegalFinPointException e) { - warnings.add(Warning.fromString("Freeform fin set point definitions illegal, ignoring.")); - } - } -} - - -class MotorMountHandler extends ElementHandler { - private final MotorMount mount; - private MotorHandler motorHandler; - - public MotorMountHandler(MotorMount mount) { - this.mount = mount; - mount.setMotorMount(true); - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (element.equals("motor")) { - motorHandler = new MotorHandler(); - return motorHandler; - } - - if (element.equals("ignitionevent") || - element.equals("ignitiondelay") || - element.equals("overhang")) { - return PlainTextHandler.INSTANCE; - } - - warnings.add(Warning.fromString("Unknown element '" + element + "' encountered, ignoring.")); - return null; - } - - - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - if (element.equals("motor")) { - String id = attributes.get("configid"); - if (id == null || id.equals("")) { - warnings.add(Warning.fromString("Illegal motor specification, ignoring.")); - return; - } - - Motor motor = motorHandler.getMotor(warnings); - mount.setMotor(id, motor); - mount.setMotorDelay(id, motorHandler.getDelay(warnings)); - return; - } - - if (element.equals("ignitionevent")) { - MotorMount.IgnitionEvent event = null; - for (MotorMount.IgnitionEvent e : MotorMount.IgnitionEvent.values()) { - if (e.name().toLowerCase().replaceAll("_", "").equals(content)) { - event = e; - break; - } - } - if (event == null) { - warnings.add(Warning.fromString("Unknown ignition event type '" + content + "', ignoring.")); - return; - } - mount.setIgnitionEvent(event); - return; - } - - if (element.equals("ignitiondelay")) { - double d; - try { - d = Double.parseDouble(content); - } catch (NumberFormatException nfe) { - warnings.add(Warning.fromString("Illegal ignition delay specified, ignoring.")); - return; - } - mount.setIgnitionDelay(d); - return; - } - - if (element.equals("overhang")) { - double d; - try { - d = Double.parseDouble(content); - } catch (NumberFormatException nfe) { - warnings.add(Warning.fromString("Illegal overhang specified, ignoring.")); - return; - } - mount.setMotorOverhang(d); - return; - } - - super.closeElement(element, attributes, content, warnings); - } -} - - - - -class MotorConfigurationHandler extends ElementHandler { - private final Rocket rocket; - private String name = null; - private boolean inNameElement = false; - - public MotorConfigurationHandler(Rocket rocket) { - this.rocket = rocket; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (inNameElement || !element.equals("name")) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return null; - } - inNameElement = true; - - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - name = content; - } - - @Override - public void endHandler(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - String configid = attributes.remove("configid"); - if (configid == null || configid.equals("")) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - if (!rocket.addMotorConfigurationID(configid)) { - warnings.add("Duplicate motor configuration ID used."); - return; - } - - if (name != null && name.trim().length() > 0) { - rocket.setMotorConfigurationName(configid, name); - } - - if ("true".equals(attributes.remove("default"))) { - rocket.getDefaultConfiguration().setMotorConfigurationID(configid); - } - - super.closeElement(element, attributes, content, warnings); - } -} - - -class MotorHandler extends ElementHandler { - private Motor.Type type = null; - private String manufacturer = null; - private String designation = null; - private String digest = null; - private double diameter = Double.NaN; - private double length = Double.NaN; - private double delay = Double.NaN; - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - - /** - * Return the motor to use, or null. - */ - public Motor getMotor(WarningSet warnings) { - if (designation == null) { - warnings.add(Warning.fromString("No motor specified, ignoring.")); - return null; - } - - List<? extends Motor> motors = Application.getMotorSetDatabase().findMotors(type, manufacturer, - designation, diameter, length); - - // No motors - if (motors.size() == 0) { - String str = "No motor with designation '" + designation + "'"; - if (manufacturer != null) - str += " for manufacturer '" + manufacturer + "'"; - str += " found."; - warnings.add(str); - return null; - } - - // One motor - if (motors.size() == 1) { - Motor m = motors.get(0); - if (digest != null && !MotorDigest.digestMotor(m).equals(digest)) { - String str = "Motor with designation '" + designation + "'"; - if (manufacturer != null) - str += " for manufacturer '" + manufacturer + "'"; - str += " has differing thrust curve than the original."; - warnings.add(str); - } - return m; - } - - // Multiple motors, check digest for which one to use - if (digest != null) { - - // Check for motor with correct digest - for (Motor m : motors) { - if (MotorDigest.digestMotor(m).equals(digest)) { - return m; - } - } - String str = "Motor with designation '" + designation + "'"; - if (manufacturer != null) - str += " for manufacturer '" + manufacturer + "'"; - str += " has differing thrust curve than the original."; - warnings.add(str); - - } else { - - // No digest, check for preferred digest (OpenRocket <= 1.1.0) - // TODO: MEDIUM: This should only be done for document versions 1.1 and below - for (Motor m : motors) { - if (PreferredMotorDigests.DIGESTS.contains(MotorDigest.digestMotor(m))) { - return m; - } - } - - String str = "Multiple motors with designation '" + designation + "'"; - if (manufacturer != null) - str += " for manufacturer '" + manufacturer + "'"; - str += " found, one chosen arbitrarily."; - warnings.add(str); - - } - return motors.get(0); - } - - /** - * Return the delay to use for the motor. - */ - public double getDelay(WarningSet warnings) { - if (Double.isNaN(delay)) { - warnings.add(Warning.fromString("Motor delay not specified, assuming no ejection charge.")); - return Motor.PLUGGED; - } - return delay; - } - - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - content = content.trim(); - - if (element.equals("type")) { - - // Motor type - type = null; - for (Motor.Type t : Motor.Type.values()) { - if (t.name().toLowerCase().equals(content.trim())) { - type = t; - break; - } - } - if (type == null) { - warnings.add(Warning.fromString("Unknown motor type '" + content + "', ignoring.")); - } - - } else if (element.equals("manufacturer")) { - - // Manufacturer - manufacturer = content.trim(); - - } else if (element.equals("designation")) { - - // Designation - designation = content.trim(); - - } else if (element.equals("digest")) { - - // Digest - digest = content.trim(); - - } else if (element.equals("diameter")) { - - // Diameter - diameter = Double.NaN; - try { - diameter = Double.parseDouble(content.trim()); - } catch (NumberFormatException e) { - // Ignore - } - if (Double.isNaN(diameter)) { - warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring.")); - } - - } else if (element.equals("length")) { - - // Length - length = Double.NaN; - try { - length = Double.parseDouble(content.trim()); - } catch (NumberFormatException ignore) { - } - - if (Double.isNaN(length)) { - warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring.")); - } - - } else if (element.equals("delay")) { - - // Delay - delay = Double.NaN; - if (content.equals("none")) { - delay = Motor.PLUGGED; - } else { - try { - delay = Double.parseDouble(content.trim()); - } catch (NumberFormatException ignore) { - } - - if (Double.isNaN(delay)) { - warnings.add(Warning.fromString("Illegal motor delay specified, ignoring.")); - } - - } - - } else { - super.closeElement(element, attributes, content, warnings); - } - } - -} - - - -class SimulationsHandler extends ElementHandler { - private final OpenRocketDocument doc; - private SingleSimulationHandler handler; - - public SimulationsHandler(OpenRocketDocument doc) { - this.doc = doc; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (!element.equals("simulation")) { - warnings.add("Unknown element '" + element + "', ignoring."); - return null; - } - - handler = new SingleSimulationHandler(doc); - return handler; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - attributes.remove("status"); - super.closeElement(element, attributes, content, warnings); - } - - -} - -class SingleSimulationHandler extends ElementHandler { - - private final OpenRocketDocument doc; - - private String name; - - private SimulationConditionsHandler conditionHandler; - private FlightDataHandler dataHandler; - - private final List<String> listeners = new ArrayList<String>(); - - public SingleSimulationHandler(OpenRocketDocument doc) { - this.doc = doc; - } - - - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (element.equals("name") || element.equals("simulator") || - element.equals("calculator") || element.equals("listener")) { - return PlainTextHandler.INSTANCE; - } else if (element.equals("conditions")) { - conditionHandler = new SimulationConditionsHandler(doc.getRocket()); - return conditionHandler; - } else if (element.equals("flightdata")) { - dataHandler = new FlightDataHandler(); - return dataHandler; - } else { - warnings.add("Unknown element '" + element + "', ignoring."); - return null; - } - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - if (element.equals("name")) { - name = content; - } else if (element.equals("simulator")) { - if (!content.trim().equals("RK4Simulator")) { - warnings.add("Unknown simulator '" + content.trim() + "' specified, ignoring."); - } - } else if (element.equals("calculator")) { - if (!content.trim().equals("BarrowmanCalculator")) { - warnings.add("Unknown calculator '" + content.trim() + "' specified, ignoring."); - } - } else if (element.equals("listener") && content.trim().length() > 0) { - listeners.add(content.trim()); - } - - } - - @Override - public void endHandler(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - String s = attributes.get("status"); - Simulation.Status status = (Status) DocumentConfig.findEnum(s, Simulation.Status.class); - if (status == null) { - warnings.add("Simulation status unknown, assuming outdated."); - status = Simulation.Status.OUTDATED; - } - - SimulationOptions conditions; - if (conditionHandler != null) { - conditions = conditionHandler.getConditions(); - } else { - warnings.add("Simulation conditions not defined, using defaults."); - conditions = new SimulationOptions(doc.getRocket()); - } - - if (name == null) - name = "Simulation"; - - FlightData data; - if (dataHandler == null) - data = null; - else - data = dataHandler.getFlightData(); - - Simulation simulation = new Simulation(doc.getRocket(), status, name, - conditions, listeners, data); - - doc.addSimulation(simulation); - } -} - - - -class SimulationConditionsHandler extends ElementHandler { - private SimulationOptions conditions; - private AtmosphereHandler atmosphereHandler; - - public SimulationConditionsHandler(Rocket rocket) { - conditions = new SimulationOptions(rocket); - // Set up default loading settings (which may differ from the new defaults) - conditions.setGeodeticComputation(GeodeticComputationStrategy.FLAT); - } - - public SimulationOptions getConditions() { - return conditions; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - if (element.equals("atmosphere")) { - atmosphereHandler = new AtmosphereHandler(attributes.get("model")); - return atmosphereHandler; - } - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - double d = Double.NaN; - try { - d = Double.parseDouble(content); - } catch (NumberFormatException ignore) { - } - - - if (element.equals("configid")) { - if (content.equals("")) { - conditions.setMotorConfigurationID(null); - } else { - conditions.setMotorConfigurationID(content); - } - } else if (element.equals("launchrodlength")) { - if (Double.isNaN(d)) { - warnings.add("Illegal launch rod length defined, ignoring."); - } else { - conditions.setLaunchRodLength(d); - } - } else if (element.equals("launchrodangle")) { - if (Double.isNaN(d)) { - warnings.add("Illegal launch rod angle defined, ignoring."); - } else { - conditions.setLaunchRodAngle(d * Math.PI / 180); - } - } else if (element.equals("launchroddirection")) { - if (Double.isNaN(d)) { - warnings.add("Illegal launch rod direction defined, ignoring."); - } else { - conditions.setLaunchRodDirection(d * Math.PI / 180); - } - } else if (element.equals("windaverage")) { - if (Double.isNaN(d)) { - warnings.add("Illegal average windspeed defined, ignoring."); - } else { - conditions.setWindSpeedAverage(d); - } - } else if (element.equals("windturbulence")) { - if (Double.isNaN(d)) { - warnings.add("Illegal wind turbulence intensity defined, ignoring."); - } else { - conditions.setWindTurbulenceIntensity(d); - } - } else if (element.equals("launchaltitude")) { - if (Double.isNaN(d)) { - warnings.add("Illegal launch altitude defined, ignoring."); - } else { - conditions.setLaunchAltitude(d); - } - } else if (element.equals("launchlatitude")) { - if (Double.isNaN(d)) { - warnings.add("Illegal launch latitude defined, ignoring."); - } else { - conditions.setLaunchLatitude(d); - } - } else if (element.equals("launchlongitude")) { - if (Double.isNaN(d)) { - warnings.add("Illegal launch longitude."); - } else { - conditions.setLaunchLongitude(d); - } - } else if (element.equals("geodeticmethod")) { - GeodeticComputationStrategy gcs = - (GeodeticComputationStrategy) DocumentConfig.findEnum(content, GeodeticComputationStrategy.class); - if (gcs != null) { - conditions.setGeodeticComputation(gcs); - } else { - warnings.add("Unknown geodetic computation method '" + content + "'"); - } - } else if (element.equals("atmosphere")) { - atmosphereHandler.storeSettings(conditions, warnings); - } else if (element.equals("timestep")) { - if (Double.isNaN(d)) { - warnings.add("Illegal time step defined, ignoring."); - } else { - conditions.setTimeStep(d); - } - } - } -} - - -class AtmosphereHandler extends ElementHandler { - private final String model; - private double temperature = Double.NaN; - private double pressure = Double.NaN; - - public AtmosphereHandler(String model) { - this.model = model; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - double d = Double.NaN; - try { - d = Double.parseDouble(content); - } catch (NumberFormatException ignore) { - } - - if (element.equals("basetemperature")) { - if (Double.isNaN(d)) { - warnings.add("Illegal base temperature specified, ignoring."); - } - temperature = d; - } else if (element.equals("basepressure")) { - if (Double.isNaN(d)) { - warnings.add("Illegal base pressure specified, ignoring."); - } - pressure = d; - } else { - super.closeElement(element, attributes, content, warnings); - } - } - - - public void storeSettings(SimulationOptions cond, WarningSet warnings) { - if (!Double.isNaN(pressure)) { - cond.setLaunchPressure(pressure); - } - if (!Double.isNaN(temperature)) { - cond.setLaunchTemperature(temperature); - } - - if ("isa".equals(model)) { - cond.setISAAtmosphere(true); - } else if ("extendedisa".equals(model)) { - cond.setISAAtmosphere(false); - } else { - cond.setISAAtmosphere(true); - warnings.add("Unknown atmospheric model, using ISA."); - } - } - -} - - -class FlightDataHandler extends ElementHandler { - - private FlightDataBranchHandler dataHandler; - private WarningSet warningSet = new WarningSet(); - private List<FlightDataBranch> branches = new ArrayList<FlightDataBranch>(); - - private FlightData data; - - public FlightData getFlightData() { - return data; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (element.equals("warning")) { - return PlainTextHandler.INSTANCE; - } - if (element.equals("databranch")) { - if (attributes.get("name") == null || attributes.get("types") == null) { - warnings.add("Illegal flight data definition, ignoring."); - return null; - } - dataHandler = new FlightDataBranchHandler(attributes.get("name"), - attributes.get("types")); - return dataHandler; - } - - warnings.add("Unknown element '" + element + "' encountered, ignoring."); - return null; - } - - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - if (element.equals("databranch")) { - FlightDataBranch branch = dataHandler.getBranch(); - if (branch.getLength() > 0) { - branches.add(branch); - } - } else if (element.equals("warning")) { - warningSet.add(Warning.fromString(content)); - } - } - - - @Override - public void endHandler(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - if (branches.size() > 0) { - data = new FlightData(branches.toArray(new FlightDataBranch[0])); - } else { - double maxAltitude = Double.NaN; - double maxVelocity = Double.NaN; - double maxAcceleration = Double.NaN; - double maxMach = Double.NaN; - double timeToApogee = Double.NaN; - double flightTime = Double.NaN; - double groundHitVelocity = Double.NaN; - - try { - maxAltitude = DocumentConfig.stringToDouble(attributes.get("maxaltitude")); - } catch (NumberFormatException ignore) { - } - try { - maxVelocity = DocumentConfig.stringToDouble(attributes.get("maxvelocity")); - } catch (NumberFormatException ignore) { - } - try { - maxAcceleration = DocumentConfig.stringToDouble(attributes.get("maxacceleration")); - } catch (NumberFormatException ignore) { - } - try { - maxMach = DocumentConfig.stringToDouble(attributes.get("maxmach")); - } catch (NumberFormatException ignore) { - } - try { - timeToApogee = DocumentConfig.stringToDouble(attributes.get("timetoapogee")); - } catch (NumberFormatException ignore) { - } - try { - flightTime = DocumentConfig.stringToDouble(attributes.get("flighttime")); - } catch (NumberFormatException ignore) { - } - try { - groundHitVelocity = - DocumentConfig.stringToDouble(attributes.get("groundhitvelocity")); - } catch (NumberFormatException ignore) { - } - - // TODO: HIGH: Store and load launchRodVelocity - data = new FlightData(maxAltitude, maxVelocity, maxAcceleration, maxMach, - timeToApogee, flightTime, groundHitVelocity, Double.NaN); - } - - data.getWarningSet().addAll(warningSet); - data.immute(); - } - - -} - - -class FlightDataBranchHandler extends ElementHandler { - private final FlightDataType[] types; - private final FlightDataBranch branch; - - public FlightDataBranchHandler(String name, String typeList) { - String[] split = typeList.split(","); - types = new FlightDataType[split.length]; - for (int i = 0; i < split.length; i++) { - types[i] = FlightDataType.getType(split[i], UnitGroup.UNITS_NONE); - } - - // TODO: LOW: May throw an IllegalArgumentException - branch = new FlightDataBranch(name, types); - } - - public FlightDataBranch getBranch() { - branch.immute(); - return branch; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - if (element.equals("datapoint")) - return PlainTextHandler.INSTANCE; - if (element.equals("event")) - return PlainTextHandler.INSTANCE; - - warnings.add("Unknown element '" + element + "' encountered, ignoring."); - return null; - } - - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - - if (element.equals("event")) { - double time; - FlightEvent.Type type; - - try { - time = DocumentConfig.stringToDouble(attributes.get("time")); - } catch (NumberFormatException e) { - warnings.add("Illegal event specification, ignoring."); - return; - } - - type = (Type) DocumentConfig.findEnum(attributes.get("type"), FlightEvent.Type.class); - if (type == null) { - warnings.add("Illegal event specification, ignoring."); - return; - } - - branch.addEvent(new FlightEvent(type, time)); - return; - } - - if (!element.equals("datapoint")) { - warnings.add("Unknown element '" + element + "' encountered, ignoring."); - return; - } - - // element == "datapoint" - - - // Check line format - String[] split = content.split(","); - if (split.length != types.length) { - warnings.add("Data point did not contain correct amount of values, ignoring point."); - return; - } - - // Parse the doubles - double[] values = new double[split.length]; - for (int i = 0; i < values.length; i++) { - try { - values[i] = DocumentConfig.stringToDouble(split[i]); - } catch (NumberFormatException e) { - warnings.add("Data point format error, ignoring point."); - return; - } - } - - // Add point to branch - branch.addPoint(); - for (int i = 0; i < types.length; i++) { - branch.setValue(types[i], values[i]); - } - } -} - - - - - -///////////////// Setters implementation - - -//// Interface -interface Setter { - /** - * Set the specified value to the given component. - * - * @param component the component to which to set. - * @param value the value within the element. - * @param attributes attributes for the element. - * @param warnings the warning set to use. - */ - public void set(RocketComponent component, String value, - HashMap<String, String> attributes, WarningSet warnings); -} - - -//// StringSetter - sets the value to the contained String -class StringSetter implements Setter { - private final Reflection.Method setMethod; - - public StringSetter(Reflection.Method set) { - setMethod = set; - } - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - setMethod.invoke(c, s); - } -} - -//// IntSetter - set an integer value -class IntSetter implements Setter { - private final Reflection.Method setMethod; - - public IntSetter(Reflection.Method set) { - setMethod = set; - } - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - try { - int n = Integer.parseInt(s); - setMethod.invoke(c, n); - } catch (NumberFormatException e) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - } - } -} - - -//// BooleanSetter - set a boolean value -class BooleanSetter implements Setter { - private final Reflection.Method setMethod; - - public BooleanSetter(Reflection.Method set) { - setMethod = set; - } - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - - s = s.trim(); - if (s.equalsIgnoreCase("true")) { - setMethod.invoke(c, true); - } else if (s.equalsIgnoreCase("false")) { - setMethod.invoke(c, false); - } else { - warnings.add(Warning.FILE_INVALID_PARAMETER); - } - } -} - - - -//// DoubleSetter - sets a double value or (alternatively) if a specific string is encountered -//// calls a setXXX(boolean) method. -class DoubleSetter implements Setter { - private final Reflection.Method setMethod; - private final String specialString; - private final Reflection.Method specialMethod; - private final double multiplier; - - /** - * Set only the double value. - * @param set set method for the double value. - */ - public DoubleSetter(Reflection.Method set) { - this.setMethod = set; - this.specialString = null; - this.specialMethod = null; - this.multiplier = 1.0; - } - - /** - * Multiply with the given multiplier and set the double value. - * @param set set method for the double value. - * @param mul multiplier. - */ - public DoubleSetter(Reflection.Method set, double mul) { - this.setMethod = set; - this.specialString = null; - this.specialMethod = null; - this.multiplier = mul; - } - - /** - * Set the double value, or if the value equals the special string, use the - * special setter and set it to true. - * - * @param set double setter. - * @param special special string - * @param specialMethod boolean setter. - */ - public DoubleSetter(Reflection.Method set, String special, - Reflection.Method specialMethod) { - this.setMethod = set; - this.specialString = special; - this.specialMethod = specialMethod; - this.multiplier = 1.0; - } - - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - - s = s.trim(); - - // Check for special case - if (specialMethod != null && s.equalsIgnoreCase(specialString)) { - specialMethod.invoke(c, true); - return; - } - - // Normal case - try { - double d = Double.parseDouble(s); - setMethod.invoke(c, d * multiplier); - } catch (NumberFormatException e) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - } - } -} - - -class OverrideSetter implements Setter { - private final Reflection.Method setMethod; - private final Reflection.Method enabledMethod; - - public OverrideSetter(Reflection.Method set, Reflection.Method enabledMethod) { - this.setMethod = set; - this.enabledMethod = enabledMethod; - } - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - - try { - double d = Double.parseDouble(s); - setMethod.invoke(c, d); - enabledMethod.invoke(c, true); - } catch (NumberFormatException e) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - } - } -} - -//// EnumSetter - sets a generic enum type -class EnumSetter<T extends Enum<T>> implements Setter { - private final Reflection.Method setter; - private final Class<T> enumClass; - - public EnumSetter(Reflection.Method set, Class<T> enumClass) { - this.setter = set; - this.enumClass = enumClass; - } - - @Override - public void set(RocketComponent c, String name, HashMap<String, String> attributes, - WarningSet warnings) { - - Enum<?> setEnum = DocumentConfig.findEnum(name, enumClass); - if (setEnum == null) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - setter.invoke(c, setEnum); - } -} - - -//// ColorSetter - sets a Color value -class ColorSetter implements Setter { - private final Reflection.Method setMethod; - - public ColorSetter(Reflection.Method set) { - setMethod = set; - } - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - - String red = attributes.get("red"); - String green = attributes.get("green"); - String blue = attributes.get("blue"); - - if (red == null || green == null || blue == null) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - int r, g, b; - try { - r = Integer.parseInt(red); - g = Integer.parseInt(green); - b = Integer.parseInt(blue); - } catch (NumberFormatException e) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - if (r < 0 || g < 0 || b < 0 || r > 255 || g > 255 || b > 255) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - Color color = new Color(r, g, b); - setMethod.invoke(c, color); - - if (!s.trim().equals("")) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - } - } -} - - - -class MaterialSetter implements Setter { - private final Reflection.Method setMethod; - private final Material.Type type; - - public MaterialSetter(Reflection.Method set, Material.Type type) { - this.setMethod = set; - this.type = type; - } - - @Override - public void set(RocketComponent c, String name, HashMap<String, String> attributes, - WarningSet warnings) { - - Material mat; - - // Check name != "" - name = name.trim(); - if (name.equals("")) { - warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - return; - } - - // Parse density - double density; - String str; - str = attributes.remove("density"); - if (str == null) { - warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - return; - } - try { - density = Double.parseDouble(str); - } catch (NumberFormatException e) { - warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - return; - } - - // Parse thickness - // double thickness = 0; - // str = attributes.remove("thickness"); - // try { - // if (str != null) - // thickness = Double.parseDouble(str); - // } catch (NumberFormatException e){ - // warnings.add(Warning.fromString("Illegal material specification, ignoring.")); - // return; - // } - - // Check type if specified - str = attributes.remove("type"); - if (str != null && !type.name().toLowerCase().equals(str)) { - warnings.add(Warning.fromString("Illegal material type specified, ignoring.")); - return; - } - - mat = Databases.findMaterial(type, name, density, false); - - setMethod.invoke(c, mat); - } -} - - - - -class PositionSetter implements Setter { - - @Override - public void set(RocketComponent c, String value, HashMap<String, String> attributes, - WarningSet warnings) { - - RocketComponent.Position type = (Position) DocumentConfig.findEnum(attributes.get("type"), - RocketComponent.Position.class); - if (type == null) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - double pos; - try { - pos = Double.parseDouble(value); - } catch (NumberFormatException e) { - warnings.add(Warning.FILE_INVALID_PARAMETER); - return; - } - - if (c instanceof FinSet) { - ((FinSet) c).setRelativePosition(type); - c.setPositionValue(pos); - } else if (c instanceof LaunchLug) { - ((LaunchLug) c).setRelativePosition(type); - c.setPositionValue(pos); - } else if (c instanceof InternalComponent) { - ((InternalComponent) c).setRelativePosition(type); - c.setPositionValue(pos); - } else { - warnings.add(Warning.FILE_INVALID_PARAMETER); - } - - } -} - - -class FinTabPositionSetter extends DoubleSetter { - - public FinTabPositionSetter() { - super(Reflection.findMethod(FinSet.class, "setTabShift", double.class)); - } - - @Override - public void set(RocketComponent c, String s, HashMap<String, String> attributes, - WarningSet warnings) { - - if (!(c instanceof FinSet)) { - throw new IllegalStateException("FinTabPositionSetter called for component " + c); - } - - String relative = attributes.get("relativeto"); - FinSet.TabRelativePosition position = - (TabRelativePosition) DocumentConfig.findEnum(relative, - FinSet.TabRelativePosition.class); - - if (position != null) { - - ((FinSet) c).setTabRelativePosition(position); - - } else { - if (relative == null) { - warnings.add("Required attribute 'relativeto' not found for fin tab position."); - } else { - warnings.add("Illegal attribute value '" + relative + "' encountered."); - } - } - - super.set(c, s, attributes, warnings); - } - - -} - - -class ClusterConfigurationSetter implements Setter { - - @Override - public void set(RocketComponent component, String value, HashMap<String, String> attributes, - WarningSet warnings) { - - if (!(component instanceof Clusterable)) { - warnings.add("Illegal component defined as cluster."); - return; - } - - ClusterConfiguration config = null; - for (ClusterConfiguration c : ClusterConfiguration.CONFIGURATIONS) { - if (c.getXMLName().equals(value)) { - config = c; - break; - } - } - - if (config == null) { - warnings.add("Illegal cluster configuration specified."); - return; - } - - ((Clusterable) component).setClusterConfiguration(config); - } -} diff --git a/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java deleted file mode 100644 index 7dbb2fce..00000000 --- a/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java +++ /dev/null @@ -1,554 +0,0 @@ -package net.sf.openrocket.file.openrocket; - -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.zip.GZIPOutputStream; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.document.StorageOptions; -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationOptions; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.TextUtil; - -public class OpenRocketSaver extends RocketSaver { - private static final LogHelper log = Application.getLogger(); - - - /** - * Divisor used in converting an integer version to the point-represented version. - * The integer version divided by this value is the major version and the remainder is - * the minor version. For example 101 corresponds to file version "1.1". - */ - public static final int FILE_VERSION_DIVISOR = 100; - - - private static final String OPENROCKET_CHARSET = "UTF-8"; - - private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket.savers"; - private static final String METHOD_SUFFIX = "Saver"; - - - // Estimated storage used by different portions - // These have been hand-estimated from saved files - private static final int BYTES_PER_COMPONENT_UNCOMPRESSED = 590; - private static final int BYTES_PER_COMPONENT_COMPRESSED = 80; - private static final int BYTES_PER_SIMULATION_UNCOMPRESSED = 1000; - private static final int BYTES_PER_SIMULATION_COMPRESSED = 100; - private static final int BYTES_PER_DATAPOINT_UNCOMPRESSED = 350; - private static final int BYTES_PER_DATAPOINT_COMPRESSED = 100; - - - private int indent; - private Writer dest; - - @Override - public void save(OutputStream output, OpenRocketDocument document, StorageOptions options) - throws IOException { - - log.info("Saving .ork file"); - - if (options.isCompressionEnabled()) { - log.debug("Enabling compression"); - output = new GZIPOutputStream(output); - } - - dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET)); - - // Select file version number - final int fileVersion = calculateNecessaryFileVersion(document, options); - final String fileVersionString = - (fileVersion / FILE_VERSION_DIVISOR) + "." + (fileVersion % FILE_VERSION_DIVISOR); - log.debug("Storing file version " + fileVersionString); - - - this.indent = 0; - - - writeln("<?xml version='1.0' encoding='utf-8'?>"); - writeln("<openrocket version=\"" + fileVersionString + "\" creator=\"OpenRocket " - + BuildProperties.getVersion() + "\">"); - indent++; - - // Recursively save the rocket structure - saveComponent(document.getRocket()); - - writeln(""); - - // Save all simulations - writeln("<simulations>"); - indent++; - boolean first = true; - for (Simulation s : document.getSimulations()) { - if (!first) - writeln(""); - first = false; - saveSimulation(s, options.getSimulationTimeSkip()); - } - indent--; - writeln("</simulations>"); - - indent--; - writeln("</openrocket>"); - - log.debug("Writing complete, flushing buffers"); - dest.flush(); - if (options.isCompressionEnabled()) { - ((GZIPOutputStream) output).finish(); - } - } - - - - @Override - public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) { - - long size = 0; - - // Size per component - int componentCount = 0; - Rocket rocket = doc.getRocket(); - Iterator<RocketComponent> iterator = rocket.iterator(true); - while (iterator.hasNext()) { - iterator.next(); - componentCount++; - } - - if (options.isCompressionEnabled()) - size += componentCount * BYTES_PER_COMPONENT_COMPRESSED; - else - size += componentCount * BYTES_PER_COMPONENT_UNCOMPRESSED; - - - // Size per simulation - if (options.isCompressionEnabled()) - size += doc.getSimulationCount() * BYTES_PER_SIMULATION_COMPRESSED; - else - size += doc.getSimulationCount() * BYTES_PER_SIMULATION_UNCOMPRESSED; - - - // Size per flight data point - int pointCount = 0; - double timeSkip = options.getSimulationTimeSkip(); - if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) { - for (Simulation s : doc.getSimulations()) { - FlightData data = s.getSimulatedData(); - if (data != null) { - for (int i = 0; i < data.getBranchCount(); i++) { - pointCount += countFlightDataBranchPoints(data.getBranch(i), timeSkip); - } - } - } - } - - if (options.isCompressionEnabled()) - size += pointCount * BYTES_PER_DATAPOINT_COMPRESSED; - else - size += pointCount * BYTES_PER_DATAPOINT_UNCOMPRESSED; - - return size; - } - - - /** - * Determine which file version is required in order to store all the features of the - * current design. By default the oldest version that supports all the necessary features - * will be used. - * - * @param document the document to output. - * @param opts the storage options. - * @return the integer file version to use. - */ - private int calculateNecessaryFileVersion(OpenRocketDocument document, StorageOptions opts) { - /* - * File version 1.2 is required for: - * - saving motor data - * - * File version 1.1 is required for: - * - fin tabs - * - components attached to tube coupler - * - * Otherwise use version 1.0. - */ - - // Check if design has simulations defined (version 1.3) - if (document.getSimulationCount() > 0) { - return FILE_VERSION_DIVISOR + 3; - } - - // Check for motor definitions (version 1.2) - Iterator<RocketComponent> iterator = document.getRocket().iterator(); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - if (!(c instanceof MotorMount)) - continue; - - MotorMount mount = (MotorMount) c; - for (String id : document.getRocket().getMotorConfigurationIDs()) { - if (mount.getMotor(id) != null) { - return FILE_VERSION_DIVISOR + 2; - } - } - } - - // Check for fin tabs (version 1.1) - iterator = document.getRocket().iterator(); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - - // Check for fin tabs - if (c instanceof FinSet) { - FinSet fin = (FinSet) c; - if (!MathUtil.equals(fin.getTabHeight(), 0) && - !MathUtil.equals(fin.getTabLength(), 0)) { - return FILE_VERSION_DIVISOR + 1; - } - } - - // Check for components attached to tube coupler - if (c instanceof TubeCoupler) { - if (c.getChildCount() > 0) { - return FILE_VERSION_DIVISOR + 1; - } - } - } - - // Default (version 1.0) - return FILE_VERSION_DIVISOR + 0; - } - - - - @SuppressWarnings("unchecked") - private void saveComponent(RocketComponent component) throws IOException { - - log.debug("Saving component " + component.getComponentName()); - - Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX, - "getElements", RocketComponent.class); - if (m == null) { - throw new BugException("Unable to find saving class for component " + - component.getComponentName()); - } - - // Get the strings to save - List<String> list = (List<String>) m.invokeStatic(component); - int length = list.size(); - - if (length == 0) // Nothing to do - return; - - if (length < 2) { - throw new RuntimeException("BUG, component data length less than two lines."); - } - - // Open element - writeln(list.get(0)); - indent++; - - // Write parameters - for (int i = 1; i < length - 1; i++) { - writeln(list.get(i)); - } - - // Recursively write subcomponents - if (component.getChildCount() > 0) { - writeln(""); - writeln("<subcomponents>"); - indent++; - boolean emptyline = false; - for (RocketComponent subcomponent : component.getChildren()) { - if (emptyline) - writeln(""); - emptyline = true; - saveComponent(subcomponent); - } - indent--; - writeln("</subcomponents>"); - } - - // Close element - indent--; - writeln(list.get(length - 1)); - } - - - private void saveSimulation(Simulation simulation, double timeSkip) throws IOException { - SimulationOptions cond = simulation.getOptions(); - - writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) + "\">"); - indent++; - - writeln("<name>" + escapeXML(simulation.getName()) + "</name>"); - // TODO: MEDIUM: Other simulators/calculators - writeln("<simulator>RK4Simulator</simulator>"); - writeln("<calculator>BarrowmanCalculator</calculator>"); - writeln("<conditions>"); - indent++; - - writeElement("configid", cond.getMotorConfigurationID()); - writeElement("launchrodlength", cond.getLaunchRodLength()); - writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI); - writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0 / Math.PI); - writeElement("windaverage", cond.getWindSpeedAverage()); - writeElement("windturbulence", cond.getWindTurbulenceIntensity()); - writeElement("launchaltitude", cond.getLaunchAltitude()); - writeElement("launchlatitude", cond.getLaunchLatitude()); - writeElement("launchlongitude", cond.getLaunchLongitude()); - writeElement("geodeticmethod", cond.getGeodeticComputation().name().toLowerCase()); - - if (cond.isISAAtmosphere()) { - writeln("<atmosphere model=\"isa\"/>"); - } else { - writeln("<atmosphere model=\"extendedisa\">"); - indent++; - writeElement("basetemperature", cond.getLaunchTemperature()); - writeElement("basepressure", cond.getLaunchPressure()); - indent--; - writeln("</atmosphere>"); - } - - writeElement("timestep", cond.getTimeStep()); - - indent--; - writeln("</conditions>"); - - - for (String s : simulation.getSimulationListeners()) { - writeElement("listener", escapeXML(s)); - } - - - // Write basic simulation data - - FlightData data = simulation.getSimulatedData(); - if (data != null) { - String str = "<flightdata"; - if (!Double.isNaN(data.getMaxAltitude())) - str += " maxaltitude=\"" + TextUtil.doubleToString(data.getMaxAltitude()) + "\""; - if (!Double.isNaN(data.getMaxVelocity())) - str += " maxvelocity=\"" + TextUtil.doubleToString(data.getMaxVelocity()) + "\""; - if (!Double.isNaN(data.getMaxAcceleration())) - str += " maxacceleration=\"" + TextUtil.doubleToString(data.getMaxAcceleration()) + "\""; - if (!Double.isNaN(data.getMaxMachNumber())) - str += " maxmach=\"" + TextUtil.doubleToString(data.getMaxMachNumber()) + "\""; - if (!Double.isNaN(data.getTimeToApogee())) - str += " timetoapogee=\"" + TextUtil.doubleToString(data.getTimeToApogee()) + "\""; - if (!Double.isNaN(data.getFlightTime())) - str += " flighttime=\"" + TextUtil.doubleToString(data.getFlightTime()) + "\""; - if (!Double.isNaN(data.getGroundHitVelocity())) - str += " groundhitvelocity=\"" + TextUtil.doubleToString(data.getGroundHitVelocity()) + "\""; - str += ">"; - writeln(str); - indent++; - - for (Warning w : data.getWarningSet()) { - writeElement("warning", escapeXML(w.toString())); - } - - // Check whether to store data - if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data - timeSkip = 0; - - if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) { - for (int i = 0; i < data.getBranchCount(); i++) { - FlightDataBranch branch = data.getBranch(i); - saveFlightDataBranch(branch, timeSkip); - } - } - - indent--; - writeln("</flightdata>"); - } - - indent--; - writeln("</simulation>"); - - } - - - - private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) - throws IOException { - double previousTime = -100000; - - if (branch == null) - return; - - // Retrieve the types from the branch - FlightDataType[] types = branch.getTypes(); - - if (types.length == 0) - return; - - // Retrieve the data from the branch - List<List<Double>> data = new ArrayList<List<Double>>(types.length); - for (int i = 0; i < types.length; i++) { - data.add(branch.get(types[i])); - } - List<Double> timeData = branch.get(FlightDataType.TYPE_TIME); - - // Build the <databranch> tag - StringBuilder sb = new StringBuilder(); - sb.append("<databranch name=\""); - sb.append(escapeXML(branch.getBranchName())); - sb.append("\" types=\""); - for (int i = 0; i < types.length; i++) { - if (i > 0) - sb.append(","); - sb.append(escapeXML(types[i].getName())); - } - sb.append("\">"); - writeln(sb.toString()); - indent++; - - // Write events - for (FlightEvent event : branch.getEvents()) { - writeln("<event time=\"" + TextUtil.doubleToString(event.getTime()) - + "\" type=\"" + enumToXMLName(event.getType()) + "\"/>"); - } - - // Write the data - int length = branch.getLength(); - if (length > 0) { - writeDataPointString(data, 0, sb); - previousTime = timeData.get(0); - } - - for (int i = 1; i < length - 1; i++) { - if (timeData != null) { - if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) { - writeDataPointString(data, i, sb); - previousTime = timeData.get(i); - } - } else { - // If time data is not available, write all points - writeDataPointString(data, i, sb); - } - } - - if (length > 1) { - writeDataPointString(data, length - 1, sb); - } - - indent--; - writeln("</databranch>"); - } - - - - /* TODO: LOW: This is largely duplicated from above! */ - private int countFlightDataBranchPoints(FlightDataBranch branch, double timeSkip) { - int count = 0; - - double previousTime = -100000; - - if (branch == null) - return 0; - - // Retrieve the types from the branch - FlightDataType[] types = branch.getTypes(); - - if (types.length == 0) - return 0; - - List<Double> timeData = branch.get(FlightDataType.TYPE_TIME); - if (timeData == null) { - // If time data not available, store all points - return branch.getLength(); - } - - // Write the data - int length = branch.getLength(); - if (length > 0) { - count++; - previousTime = timeData.get(0); - } - - for (int i = 1; i < length - 1; i++) { - if (Math.abs(timeData.get(i) - previousTime - timeSkip) < Math.abs(timeData.get(i + 1) - previousTime - timeSkip)) { - count++; - previousTime = timeData.get(i); - } - } - - if (length > 1) { - count++; - } - - return count; - } - - - - private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb) - throws IOException { - sb.setLength(0); - sb.append("<datapoint>"); - for (int j = 0; j < data.size(); j++) { - if (j > 0) - sb.append(","); - sb.append(TextUtil.doubleToString(data.get(j).get(index))); - } - sb.append("</datapoint>"); - writeln(sb.toString()); - } - - - - private void writeElement(String element, Object content) throws IOException { - if (content == null) - content = ""; - writeln("<" + element + ">" + content + "</" + element + ">"); - } - - - - private void writeln(String str) throws IOException { - if (str.length() == 0) { - dest.write("\n"); - return; - } - String s = ""; - for (int i = 0; i < indent; i++) - s = s + " "; - s = s + str + "\n"; - dest.write(s); - } - - - - - /** - * Return the XML equivalent of an enum name. - * - * @param e the enum to save. - * @return the corresponding XML name. - */ - public static String enumToXMLName(Enum<?> e) { - return e.name().toLowerCase().replace("_", ""); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/PreferredMotorDigests.java b/src/net/sf/openrocket/file/openrocket/PreferredMotorDigests.java deleted file mode 100644 index d05cccbb..00000000 --- a/src/net/sf/openrocket/file/openrocket/PreferredMotorDigests.java +++ /dev/null @@ -1,885 +0,0 @@ -package net.sf.openrocket.file.openrocket; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * This class contains the motor digests of motors included in OpenRocket versions prior to 1.1.1. - * Before this the motor digest was not included in the design files, and therefore if the motor - * digest is missing when loading a file, the loader should prefer the motors with a digest defined - * in this class. - * <p> - * This is not a requirement for supporting the OpenRocket format, but allows opening older OpenRocket - * design files accurately without warnings of multiple motors. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -final class PreferredMotorDigests { - - /** - * A set containing the preferred motor digests. - */ - public static final Set<String> DIGESTS; - static { - /* - * The list contains 845 digests, set initial parameters suitably to - * prevent any rehashing operations and to minimize size (power of two). - * 845/1024 = 0.825 - */ - Set<String> set = new HashSet<String>(1024, 0.85f); - - set.add("000ffb4c8e49ae47b2ab9a659da9e59b"); - set.add("0039ed088e61360d934d9bd8503fad92"); - set.add("003eeba358de7ebf9293b0e4c4ca9e66"); - set.add("00e1a0576a93101d458c1c3d68d3eee0"); - set.add("0111b89926277a6ea3f6075052343105"); - set.add("0142c270a670ffff41c43268b0f129b9"); - set.add("01be1f9100e05fb15df4c13395f7181c"); - set.add("026f5924c48693077f2b11cdcdeb7452"); - set.add("029082f7acda395568ca7f7df40764e1"); - set.add("02dd1b3e2df7daf48b763f5ace35345e"); - set.add("036e124dce42859ff08efa79e1f202e8"); - set.add("03b88e64af521b03803247922801c996"); - set.add("0468d7dc3dca25ac073dac1bd674e271"); - set.add("048cfb7c2477c6e957d501c5ed3bc252"); - set.add("049dda2ad1a709321734f393dc8a115b"); - set.add("056d61b6a268283411e9dc9731dbb5e6"); - set.add("05b85612f288726b02cdc47af7026aac"); - set.add("05e205dc5dbd95db25305aa5c77b1192"); - set.add("0601c6944d02e8736c09c2a8bb7cba49"); - set.add("0622884d0a0954b1df6694ead24868bf"); - set.add("063e7748d9a96508a70b1a2a1887aa3d"); - set.add("06634321a8c5d533eb5efcbb40143257"); - set.add("069a54372ed2776286160384ca0cac4f"); - set.add("075539867b13c2afcc5198e00d7f4b5c"); - set.add("076d9374af5fb0f2469083f9b57b7b96"); - set.add("07c44b615a67060bca83c6faed56c0c6"); - set.add("0825628215a980eed5fb4bed4eaec3b8"); - set.add("082bad018f6d1e5622c371c1fe3148d6"); - set.add("0837c3014078c8c8e79961b939be83cb"); - set.add("08abceec22c5f6be5e9864be38df8ad5"); - set.add("08c3b40a4bcf7a33256e5543e484f995"); - set.add("08ca5be1a598772a8683016db619de36"); - set.add("0a80cecafb53ae0ac73e6aec1a4567dd"); - set.add("0add7ca688bcd32df8c3367a094e7847"); - set.add("0b175b4beb0057db1b169d61061208a7"); - set.add("0b955870dc2007c2b5f07eea57609420"); - set.add("0c60a51d36ee5118fe29173aff2f6e49"); - set.add("0c96cd95432d8e2ce6a6463ebf50beb1"); - set.add("0d06d195c29a7f6fde6d002171922f2e"); - set.add("0d642d7cb1544d19ec471124db97b92e"); - set.add("0dd49968e2b1c4b1077e3c7ade056a79"); - set.add("0e0d93ee28216440a5fa9452c9082351"); - set.add("0e6774068b61579e20b89771b8a8f273"); - set.add("0eac15679d3ae2fbd41083492b356b03"); - set.add("0eca4c015dd1de561c2bbc47eaa4daf6"); - set.add("0f0e1d09c7dec3a05b870b399ddbf6ee"); - set.add("0f3c31b26b5768b3202f02f9d6bcc71c"); - set.add("0f47293601d59fbad2076012090665dc"); - set.add("0f5a1b31c333b722e4a72acbeba3a189"); - set.add("0f6a55aca8a317f4d3d3236e4944343d"); - set.add("0ffaa291ee52495d7dfec03b3a845636"); - set.add("1092f5c5b48c2dcd4ac0bb16fa5383a8"); - set.add("10a1689703dc533d435bef7265dd9ac0"); - set.add("11bcc433b82e714f59809d76eed0ba05"); - set.add("11ce2ec12b216f3e8d71fd9c53782b23"); - set.add("11d11cdff93f18d10a1286cc3485c8c7"); - set.add("11eac86852e12b9c3a2d0a4b183c3b79"); - set.add("120eab6dd03b9bee7f5eb717e4e9d491"); - set.add("1272d1a6979ea20a2efee3eb04657915"); - set.add("12f6c5360c83656356c902ece3c0ff1b"); - set.add("138a603a483dcf4127f1dcf208843e67"); - set.add("140276d009fde1357ecdcb5d9ddc8a80"); - set.add("1491fae1c7ce940915dd8296c74320f3"); - set.add("14955ccec83043f3b1ef92f8524b3e67"); - set.add("150b1279bc8b7f509a030274ee8e3f35"); - set.add("153374d45687af1e96d5b8b1b03a2515"); - set.add("1536a1389a9cd4ecf5bfaac9f4333852"); - set.add("1539231d9952fdbe0533df405c46356c"); - set.add("15d6a88bb656579740291df01297aa5c"); - set.add("15fbf68a7c02161beb6cad00325752c3"); - set.add("161cd37f60e13b9850e881bac61c840f"); - set.add("161ed36663b694184f7f4131d1d5f3df"); - set.add("167df7bf13809a19da8ff90a27f4b522"); - set.add("170e81af0371550ea20c827669fbf0fd"); - set.add("177c0df08cb85a4e13bf7412dacf2699"); - set.add("179b9694bca64255ce9c0b06a08f46e6"); - set.add("17d55e2cd3df50ef07aff9be6b160915"); - set.add("1835337dfceafa20029fe6e472e7c7f0"); - set.add("185820cacfb62e34c1f6c2e1feb42d27"); - set.add("18981fde366efeca850bdf490253f0ec"); - set.add("18b7f1dce04bc7838f3d2b234923de27"); - set.add("18c2d213b8de15fc11ef66f7a7ad04a4"); - set.add("1914ab609416b8559eeefda814867b9b"); - set.add("19ae231357c49b6bf9427fa178dc58ab"); - set.add("19b0b447800ba36f2d4ce76264009e2d"); - set.add("19c9120e2fb7bbf6d21d71659f77439c"); - set.add("19c9753bd99d7f0328792a434625f8a5"); - set.add("1a508ce5b461be1998750bdad26764a3"); - set.add("1a77681a4646cd21461df84c49074fe3"); - set.add("1aa169a73004fc66a932576ac2732b15"); - set.add("1aa1f3cc21a0f6a6eadb6166d468284c"); - set.add("1ac8dac1b547a064a306bf42e568b5bc"); - set.add("1af11d2e99f06b69ab5103731592ae8e"); - set.add("1af30f73640ac1f9f3c8ef32fd04bfb8"); - set.add("1b337a115a491abfc3abcd62399704d2"); - set.add("1bb9c002f22ccd24bfcec36957ac0367"); - set.add("1cbb12c9b58adc33642e1165b77c2e58"); - set.add("1d30457aa2af0f212a26b9d2c203a216"); - set.add("1d390d2ede88fb2f77ad7e7432155466"); - set.add("1d920d4ee2bef0c7ffb28a91b9e325f6"); - set.add("1e09cd01462e6d4728efafc4a550a5a6"); - set.add("1e26c7969adb5bfc507da22802f17053"); - set.add("1e5378337317821ffa4f53e9ecf47fbd"); - set.add("1e68b1ce7eb224dc65b39546d0892299"); - set.add("1e757827e2e03a781905e9f983c89933"); - set.add("1f2564b3f0d78751b4e1d5836d54f7b1"); - set.add("210bd4536d1c1872d213995420cf9513"); - set.add("21bdc48d9411ffc8e811e32c45640f58"); - set.add("21d4e53c3308cf3a1e916ef6cafff873"); - set.add("21db7fea27e33cbab6fa2984017c241c"); - set.add("221ab691a72a6f8b65792233b7bdf884"); - set.add("222b7613b7a9a85d45051fb263b511bf"); - set.add("224c062a8213f22058c0479c91ce470a"); - set.add("22777fde378d9610258e4223fb5563f5"); - set.add("22929b4849129644531a722397786513"); - set.add("22c31705c3948c39721ced4ca04b2e65"); - set.add("22e355a9e573b7f6f86c7e0791647ba7"); - set.add("2320f4b15fb78448ce16a5a625f6f8f2"); - set.add("234467bcf00a15e7377ceca46b7302f8"); - set.add("23e140b2126af53487781f63082615e5"); - set.add("245d147c568faf00dfb47d9c9080871c"); - set.add("24a5102957c91107a092704f4f166e77"); - set.add("24b7b0f55cea9329f981f00d922cfe61"); - set.add("24d9308fa5d88f89365760a6e54f557f"); - set.add("24fe3f1922a6d16b21f57b9925558296"); - set.add("2571d40a353d275cdd8a4ba6e80a32fd"); - set.add("259a0325a52acf54184fd439d1b2521d"); - set.add("259d90773b3387c58aecb648c2c3812e"); - set.add("25fd0f44fbbadfb70cee0467f9b53d3e"); - set.add("26331fa38f2af84b18df5dd1db0244f0"); - set.add("26a5e7834018943090396d419ca64662"); - set.add("271f29d0b199d0d3f036e8f99ce76975"); - set.add("2762f40ffacbc78b4c949cd38101a02a"); - set.add("2769033a0acfff04e1f427027643c03a"); - set.add("27b1601bb3a33c7cd2745caa651f0705"); - set.add("27e522bd25f54343584ae89e90e64ee3"); - set.add("2815e68ed1683663820682c8e00fd795"); - set.add("285e598a676de414661a022f72967d29"); - set.add("2886ee93f5dd4f02b331089928520e4f"); - set.add("28f53f74ab45da2ab83072143f0d01d0"); - set.add("2967cd7a160b396ef96f09695429d8e9"); - set.add("29e99fbfab8c9771f4b5a86195db0c46"); - set.add("2a1f5f5a829badfd64e2c20cd17bd38b"); - set.add("2a941643d418880e0e7337aaaa00c555"); - set.add("2a9d2a64b4601046774c9d27202de593"); - set.add("2ad8de03de84415f1397cb2d4c77fb84"); - set.add("2af7bcae566ada617d8888f34a5f70a3"); - set.add("2bb2cea5465ab43f9b7e83cb44851223"); - set.add("2bc22736450a8d0efb8d898bdcf52d43"); - set.add("2c19c0cd4c005877798821dd65a2ff2e"); - set.add("2c39985a5a49fa07759dc880e3722203"); - set.add("2c58d5382b8d5fdbe1800e013f200f38"); - set.add("2c8f6954ba9842ad9fc9bb367d14cf72"); - set.add("2d13c151bbf6e1d7d7378c86d191d2d8"); - set.add("2df4ee3f8a2c3611b267936e47bd3d3f"); - set.add("2e6c8ecf50ee9ea82f407a8b0acd4f85"); - set.add("2e97a2f015b1247b01b5e022bf6109cc"); - set.add("2eae476e3eb97e2a1ad54c5b8fa48208"); - set.add("2f44b9347e986c91ab886dc2e508885f"); - set.add("2f478d2efa82571d5c3e49fde16c936e"); - set.add("2f7460de6e7581a6775f739f894d86c6"); - set.add("2fa429a16950f6c3f19a051b3417aac7"); - set.add("2fa4545430dae966dce186984c98d0b7"); - set.add("3027d63763f7aca58b41d52689f38dbd"); - set.add("302b34ea5ec261fd74a4901d041d3f82"); - set.add("30b5952157345beb00d753425a728757"); - set.add("3136fef31b6d0e1d9a0dbbbdac05b0a3"); - set.add("321377ccf346be4efa1fb8658032298a"); - set.add("325e3898dc252f6c936301412be06505"); - set.add("32fe6eecb5e97a6ff9c4f1c005857435"); - set.add("33197b8e7194af401da6150c68004d7b"); - set.add("3393a92e46a045c4eaf6b9e18f7044e3"); - set.add("33a89133876e91fccc4058627b34d617"); - set.add("3466c5940034ddb1371c4f86dabce964"); - set.add("348abf304c63a702e4a229db28feee16"); - set.add("349260e7bc0291ba2e4c26d4db00bee9"); - set.add("3507c7d2b11a693620235ea3872dce66"); - set.add("353236b8cb07eef73d80f25e240acddb"); - set.add("35aeed248d254fbc3542b5cd3aa9842d"); - set.add("36218bbb51e99aed53ea822ebaa2c873"); - set.add("3666b06f839465adc5d36a6e75066a47"); - set.add("36fb9fb79c253ee61e498e459f0cf395"); - set.add("3703dd15904a118a05d771e7ee6e3f11"); - set.add("370b98cc77577db1a07021e46c21cd3b"); - set.add("3719475cc57cf3b5312f21b1efd228ef"); - set.add("3738564e4327367bc2f359cdbb442304"); - set.add("37bf1e76b05f333eefc0495e4f725838"); - set.add("38715f11bf91b5ce06494e1ddd94c444"); - set.add("387eea945f83c9567fa42c6e150b7ba9"); - set.add("389687548b9f05e6c99d93a2ecf76307"); - set.add("38b1e93cc1910ecc5301502fbb9bd8a3"); - set.add("3a0b2ffd2d4593581c52bdc1094d92d8"); - set.add("3a99a5318497e7108995a08675fa70d5"); - set.add("3b4573f1f11db1ffedd14e10d539aad3"); - set.add("3bc526028cf0be42fcbb75936810d41c"); - set.add("3bc5834ec0726b10465b67f17b77044e"); - set.add("3bf858e6c91e0292259a886b8bf793c3"); - set.add("3c4eea57e65806dc59dd4c206bef79e1"); - set.add("3c7b9e1836fe07f7a4ffaea90e7f33fc"); - set.add("3c8aee818229c48b9a882caa6de58c18"); - set.add("3cf831629486be08e747671a14d113f5"); - set.add("3d6b990aaee7dff0be939343711dfa74"); - set.add("3e2d355d7fd39e54ceead835d14df7e9"); - set.add("3e8697fe46496d41528387e2d37d734a"); - set.add("3ea538f54677ecaffbed1ae5f2e12d28"); - set.add("3f654d824783b4392396b34ad2b44974"); - set.add("3fc4889ea40aea23fedc994704ba4708"); - set.add("41145e8204991f0b644b831cd859c4e2"); - set.add("415fecbed32f8de16ffbab8e97edb4cb"); - set.add("41633604778611785d7453c23823b0b3"); - set.add("41d37971a99bb08d0f5f4fdcfcd87e8d"); - set.add("428c0aeb520fe9e77d3b464543620716"); - set.add("42cc2865a6fc399e689d2d569c58de2a"); - set.add("43a6db581840e3645459ce51953ca9a5"); - set.add("43a72eab1f3f874da7d68092e83357ec"); - set.add("44255564acd68eca32ffab8e6130c5cc"); - set.add("4448ff245bfd8d2606b418f33797571f"); - set.add("44a4e02e520657221706cd6d69bcfb13"); - set.add("44b12361fee8a2385a9b90e44fd079f3"); - set.add("44b7c1c17e8e4728fadeecb6ba797af0"); - set.add("44d734a18b45937c3035a047f9063dfd"); - set.add("44edf41dd7624a6e2259d8e451622527"); - set.add("4528bda7194c6dfafada95d68c2faa3a"); - set.add("45a8a995a3614f823c04f3c157effe97"); - set.add("45d2f014e70681483d6bc5864cf94b20"); - set.add("46232174087bfb178ad7cc35bfb387a8"); - set.add("46401106d96b729d330375a63e655374"); - set.add("46ac2356b12ed7519ae2dd5f199b7c10"); - set.add("4790684e33d48e3dfe99b6ff7712be8a"); - set.add("479a2848353fef692063ec37e7d556dc"); - set.add("47a649cae70a90e7d1ae2b2ab10465f0"); - set.add("47bc150e2585e61cf9380ed540de4465"); - set.add("4863872b48ecad3005e7b60d114c0fde"); - set.add("487c3163ebf25cd3b4479e13e30cba5b"); - set.add("48c5d84e56a982689f4268ed9b50cded"); - set.add("493a84bde424f5946290238998d64873"); - set.add("499e8c7c38dd4d8068eefc4eb58d4cf5"); - set.add("4a03d963b887b4ade3d574e87d111e9d"); - set.add("4a5509929d6991507c6e136178942a2d"); - set.add("4a933f8824eba082555515e69d3bfe43"); - set.add("4abc93cb926e33fbb97aa0d2ffe7885a"); - set.add("4ad536d6aee9fffe1e84c9e75698f5cf"); - set.add("4af14f94870a2e3d47dbd78cc05e58a8"); - set.add("4b0a7961ee650f518533f03c38ca8320"); - set.add("4b166cec69dc9ace3a9f598674c35b3c"); - set.add("4b5a632e55f4dbea5435e151145337a7"); - set.add("4b797a7d23faae4daf8b2946d6cb24dd"); - set.add("4b9e8ea91d6bd67b22be67dd40b871a7"); - set.add("4bd7e46dd429e45ddee5736f86d494cc"); - set.add("4beec7064114b6e49cc76cc2f71594ec"); - set.add("4c3f47c263ea5b933ac5184466573f6d"); - set.add("4c9b11094fa43b8a9aaf1a1568bf60c2"); - set.add("4ca44906c21909f396774827017e007e"); - set.add("4ca7dd633f56f76f794faf958416f4c1"); - set.add("4d6956c8d3db98528dfbdafa4a3316b6"); - set.add("4d84b18416836b7297b990a408a6eda3"); - set.add("4e13b8d5d4a77393b2fbfbaebe9ea1ca"); - set.add("4e3b029d124b898f1d11a8d15d2a6688"); - set.add("4e9723863a06235d9339cd00912871ed"); - set.add("4efdf67cd98424e7cb008dd09b169942"); - set.add("4f25dd1fcb4aedb512f24758961d87f9"); - set.add("4f86907e557c00d13b42a2393b834d8d"); - set.add("4fdb3ba6ebc9b3e9ab133c15312f995a"); - set.add("504bbb5991ad94728e6b73c6ddc6c476"); - set.add("515f449c1e9fd279dbdadf3cc38fd008"); - set.add("51d9a0c78486de462af6a490acea1fcb"); - set.add("52032bb8c1acb8bf7320aa73babd2e50"); - set.add("5203feb9b0d422a38052d9df4103e3ab"); - set.add("5222c37a7e8618d4cb43ce8c4a188129"); - set.add("52731882ea73ad5b1d39c25e2969d9aa"); - set.add("536af35745c20e4ee25486a31c2fb57c"); - set.add("5379086fb93464cbdad4459101ed4d07"); - set.add("542f3505366c2d6575e73064aacf536a"); - set.add("54350b63fafc31af32bdf49cf0bbfda2"); - set.add("5498ead583ab6cd6900a533b1cb69df8"); - set.add("553eb9e396b2f304f963b717bb3a9059"); - set.add("55c5181d0e1b170cfd05c3a9271b3bc6"); - set.add("566ff170b185c5cfd893030c97080451"); - set.add("568c906117202c4d4451dfb3be697921"); - set.add("56a9926b91222c8943640da0b642d617"); - set.add("56fcddb2fc61ab068e1ce98a226fd34d"); - set.add("573f9b1aa16e771da95df44fe3a62167"); - set.add("5805ae3e1c5fa9f7a604152c40c9d06d"); - set.add("5844ffd995e179e21476fe41a72c7e85"); - set.add("5866a0ca3348c1b406e4a4a869b183ae"); - set.add("5922a04c19e52d4a3844b61b559a89d4"); - set.add("5957b399b3380e097b70cfc09bae1bd3"); - set.add("59785d3feccf91b7a9fcd96fe5e686de"); - set.add("59cc15fde8f2bab7beac6a9542662df3"); - set.add("59ef8fd572ad56b7c00092f185512c0a"); - set.add("5a26e5d6effb9634941bbdaecf1cc4ce"); - set.add("5a94fedb054c29331d22c4442ad619a6"); - set.add("5b1a41ab325cdfb925f500868f77e058"); - set.add("5b20fd5088ed40d65a52c38fbe314373"); - set.add("5b3510c0aa53405e1fbd7a67b3af34fd"); - set.add("5b96ce711afb37fb511e70ac12cb717f"); - set.add("5bb8c694f0d7e39aceaa7fe7a885a6e1"); - set.add("5bc7dae98ed248bc85f4782783c7a383"); - set.add("5c1e091a898470db28aaddc968071a00"); - set.add("5c603c37c8ae3b7441a49bfdd93a2426"); - set.add("5ca4eac1f0b43f03288c19c42e1ccb2b"); - set.add("5ced682df2330890f2239e8da8d33061"); - set.add("5d437ac21a6da33b77c980abef5af0ac"); - set.add("5d4f136bcd4f5f71e0402819f2f666ea"); - set.add("5d9d43530d78a07475063de4f6b82578"); - set.add("5e8973f53dfe0e36537e7d88ac84cfaa"); - set.add("5e8b973df438112549cbd73b38240094"); - set.add("5ec17176ac8ca3ffe5c7707d4b33aba0"); - set.add("5ecdf016b2374b2029c58dce498548cf"); - set.add("5f4d8576e9299aecd4ece33f8b4ffb3d"); - set.add("5f5bc13ecb72cde7c4c6e89279e836f0"); - set.add("5fc7b23ca79086fde585ac92b8ddfa61"); - set.add("5fc8dfad0c6503b16fcbdaf2140f5bd6"); - set.add("610b21fa92e08d26b0ebbd27ac406558"); - set.add("618c82b1f690b74209a68062f0b7f50e"); - set.add("6214548d7b510154960ca3d23da4f38d"); - set.add("6244a9533207c9f3d89bd48d2c946d72"); - set.add("628475d3f98ce21920983b2335d29771"); - set.add("62b5f08d8f9087672b45485f5177b688"); - set.add("62c0ca2c1be43447418c673a27f69a53"); - set.add("62dd2d23b56d1991111028754f7d5718"); - set.add("62fe634a6ec154d4b675a8944ab98a7b"); - set.add("638e84fef470490263300ed27293aca9"); - set.add("643181e6ca3418a86b5dac6858805839"); - set.add("6431d2ee351952b0ca1c8e24aee89d9a"); - set.add("64cf9d529b625818f06f910fd5a51ebc"); - set.add("64f5901476b159bd9c4f5ed9aa2b4cc7"); - set.add("651c5d94aa8b742ea6bf89eb4760d88b"); - set.add("6535664e59493ee6581d3ec49d666a05"); - set.add("659bd0331a1348d14e9cd72006008d5b"); - set.add("659d8f3f58c60862ec21306475d5b86c"); - set.add("65ed980ed9e129e301e3c219be11999c"); - set.add("661c50f934f8b021101df91c063c2662"); - set.add("66289df1c8d41414638696d9847188a7"); - set.add("667f3707995e365e210a1bb9e1926455"); - set.add("66ff6174d6a5b1c8a40637c8a3a8a7b9"); - set.add("673d52c40a3153d07e7a81ad3bf2027c"); - set.add("67c803799e8e1d877eb3f713dc775444"); - set.add("680708ce5f383f0c7511eb3d7b7209d9"); - set.add("68ee06fe147e143b6e1486d292fbc9b4"); - set.add("690da28e475ebd2dec079e7de4c60719"); - set.add("693db94b6ffb0c1afa7b82499d17b25f"); - set.add("6961f9a08f066d0318669a9c9c94017d"); - set.add("69a38fb26f994ccc04e84e66441e0287"); - set.add("69f5b82d6acf05cee8615ff5d05f7921"); - set.add("6a1e040ce59176bcbe4c47654dcf83a7"); - set.add("6a26a773a6223c79697e12838582f122"); - set.add("6a6e0e4350ef8d4a3489aa4398bd674b"); - set.add("6a8abe4a6fe236bf93d9b85681db2c0e"); - set.add("6aaddb50ae45f1006852479932dfbd54"); - set.add("6adb0778b8930a8e6a2f1e99de2ef074"); - set.add("6b54ec7203070bb29e514eb7d684e380"); - set.add("6b598530a066271059bc94c1fa0cd7a1"); - set.add("6be4f1c5af0ff30131481d009e87133b"); - set.add("6be8cb8a938f1ecef2b36c823e8e6ade"); - set.add("6bfe9b78813cfa4014e630454f7a06a5"); - set.add("6cb4d52135f005a2c7ba8ccc3b8781e3"); - set.add("6cd5f8dd36648fcafcfecc8d5b990e9b"); - set.add("6cfdb07efc0d3c1f36b2d992263253f9"); - set.add("6d95c9c12fe5498af055b013bf7ceb7d"); - set.add("6e8f160f1b2b54c3c89b81c4f9a98283"); - set.add("6eadec5ff4cb05c8ef1a64d2c94d627b"); - set.add("6eceba3c0a19666f5a9adbc13ceb1ae7"); - set.add("6f47bff8d62f5bd51cee156f78e8cfcb"); - set.add("6f484725ba3abcadfe8fbfb4e6e83db6"); - set.add("7031b89c62590b6a41e7ad524bb0a308"); - set.add("7058fc792efe7aaddf8a0323bf998f72"); - set.add("706e502b5a6db25407c2565457944633"); - set.add("70cfd491765d3e4e6a4e4b1ccaf9c343"); - set.add("711e7a11c4be36563cae1b71234dc414"); - set.add("71794c9ad0e60b6d0dcd44b51c3704f0"); - set.add("7193a4c6f286f7b86b18cc7908498d06"); - set.add("71b17eeb05fd473e42aa5c4e51e47a15"); - set.add("71e0014aeaebda1113a12cecb51fd20c"); - set.add("71e2d06eaa0ab3ae59d0f7b7ef20fc31"); - set.add("71fe7c7f2a54587c2278b3e630faee56"); - set.add("729ba9dde60740e6d5e8140fae54f4c6"); - set.add("72b5be92417a4a6a09f5e01c106cf96a"); - set.add("72c0c6f5a653bb57a1aba44e7edb202b"); - set.add("72f6580c0aa3b101acffce59adf7809b"); - set.add("730ac94082f25931179a313af186b335"); - set.add("73243d82b8157406558562b5eb70818b"); - set.add("73371ae751751a5998c3bc8de577b83e"); - set.add("733c6d4df3b6781473ba0a3a169ca74a"); - set.add("7376d2568492e6f6c0fadab86e22416b"); - set.add("737b791d27930ccba061fa36c4046208"); - set.add("73a47e531e9c0ddf5a334c40508f6361"); - set.add("73b2859aedfe1bf317841bbc150e0491"); - set.add("7413c1de6d5f286055e8244101da306c"); - set.add("741bfaabd99301c5503fd36d17912627"); - set.add("7423f1b74c8367863a1071bcd0864d42"); - set.add("747ddec4bc30cbde2ebefac7b8df466c"); - set.add("7494df6c5102bbfb6e48c9b30281325b"); - set.add("74c5cb4f540e2c9b27ae60dcc8247eae"); - set.add("74f6a218f8877fb76f74eacc9df32fc6"); - set.add("751ff3c7f3e77c8d3ba24c568fd11845"); - set.add("757c05f3194d6d4b848b83c0e3e5f1a3"); - set.add("75f724e20c3f2e26d2de13927fbf78f1"); - set.add("761c6d190a411231fccfeef67f08eacf"); - set.add("763eddbcb1f6787b3350f65c64b44ba4"); - set.add("765a1a5d8f09ddffec749d3a6920c4a7"); - set.add("76acd0d7e4d38152771480bedacba209"); - set.add("76dc5d4b4f65aacb1dfc1a5a8f61b023"); - set.add("771cc4503662f2fc2c5f15f46e7e58b6"); - set.add("772a3193a3cf3e18fd2058c3f45c35f8"); - set.add("7823311e8d60374d4b37c50d927507c8"); - set.add("78282abebd03a40f2dd21f1505a5f789"); - set.add("782e12a60bbef943f13f2fa1af1e39f1"); - set.add("782e6df5b10a60b26117e0648e76c6c4"); - set.add("7875a865fbaf33ba617cdb7c7f0f7140"); - set.add("78ee37b2f7acb3d892e54f0e9d2e0c14"); - set.add("791f7d21ea119ccd49df31b2f614a0d6"); - set.add("795036eafd006f62ee5a68ba1c309865"); - set.add("795fcaf2d6d9e1c37a4c283876f26cec"); - set.add("79688fa15c424b73454d9cd0a070472f"); - set.add("796c759040e72b8efd4630754bd3f30b"); - set.add("798ad9ae186a9d89e6f69e065bc22a86"); - set.add("7a2a604d923e5bd87846902e47acc438"); - set.add("7a39ea82b6b2bb75f9f6a7b817dab9cb"); - set.add("7a62872422cf100af636b434f4a0b307"); - set.add("7acda66f5d077fa708de7d153882b97c"); - set.add("7b1aca3caab3a61147d4ebf5f7971d42"); - set.add("7b5bc0bfd0de5126b4d278aa5775abd7"); - set.add("7bd0735d3b5d579f0c97e11309a12451"); - set.add("7be2fb055d29d5c8e42c559295ee8113"); - set.add("7c14e11e0126b310217db2488f898127"); - set.add("7c4ab23d9b1db15ea1f49fe017edf346"); - set.add("7c6080928783078289d9a473efecc134"); - set.add("7ccde35451846731eff4ae16e40f661f"); - set.add("7cce66eec1313c11f5b9005db8f2823d"); - set.add("7d40723bc0200f13675626309559ce6d"); - set.add("7da7fa494528cd0836f9988f3e7ada96"); - set.add("7e3bc2bc33f34ad791573e94021381d5"); - set.add("7fb1e485fa41841779a0a1f95a2b7cd8"); - set.add("809b63d7a153ee60272ffc224766fd72"); - set.add("80fc9ff72737286ad64fe7de1524c799"); - set.add("82b602bacfe681cee58d5530ac9e8b99"); - set.add("82f69b66499f2bc375467ee933fe4577"); - set.add("83243e87941f4ec7f8841571dd90b3b2"); - set.add("836481fe9bfd7612506e0545bdcf279d"); - set.add("83a498353a59dea68538962eb2642ba8"); - set.add("83eafb190276935630f43cddf3d78143"); - set.add("845c54809778f5b838e32443a7d44072"); - set.add("849b5885cbf965447675610ee1d0dca2"); - set.add("84a895acdcd487244b6803082036fad7"); - set.add("84bdf63a67691194e37082e3f7f6d712"); - set.add("84c99be383e4ada00f4e6bd335774655"); - set.add("84ed2fb163b5b2525a9a731056ffd144"); - set.add("8517e14d6f657f1244c0344d4f1a828b"); - set.add("8541aca6dd06f2dc6984d5e1b664900c"); - set.add("85cc38b178bd721bf7225144dd301b0f"); - set.add("85d00ae1ce88ace2bc6918750a97502f"); - set.add("868af0eab556081846fdbff18df40b28"); - set.add("871f7fe309f61ec7e45e4b29833349d9"); - set.add("878e7848ab58bf9271fc04766e969c8f"); - set.add("87b872efe9433147c61d5d2c3dcca14f"); - set.add("87cd3518578a2ef76341b33f9c95198f"); - set.add("87cd3a0a86f398ba1026cdb969e55090"); - set.add("87cdeb3fcaa050209091a1600ce1df11"); - set.add("88008ed2e9b600fa2e453390688aaa7e"); - set.add("8833c25743e0f9725ca22dbc6e54d1bf"); - set.add("88693556ff80aacd354c1948675c0674"); - set.add("888664c26a3296f9571d561723b44255"); - set.add("88ed07b96422ec99767fb35bf6a51966"); - set.add("88ed43ef6f483b9a7e34c34b46335dea"); - set.add("8a2e4445364c3c9151dcf4622c6add51"); - set.add("8a73ce2e18dacf250a961dac49338407"); - set.add("8ba75b207cc0bee8ec624e9f33019161"); - set.add("8bc592cc7aaa336637b9c82c43cbb081"); - set.add("8c1bdef25d6a6df69c303be204748da9"); - set.add("8c8b182ec0845de4a5fed3245e5601ea"); - set.add("8c8d724fba729940b1234e58e64125b8"); - set.add("8ce47ac01efd8c0ab75ae5371ff0f7ba"); - set.add("8e1600a04363c86199d660ccb7085735"); - set.add("8eb548ee8bf98a6426f0b5a11e32c88a"); - set.add("8ec54a8bd1ab30f324eb0f03ef79d097"); - set.add("8ede1653debc5617deae6a7632c18502"); - set.add("903594c774fd5be01132f559c00778b4"); - set.add("9079d8f7488bca4504d58be0bc76deea"); - set.add("909a1f7458c8f1f1138dff9ce019fb6c"); - set.add("90b8dd2817509c2374b20a1975ca1a54"); - set.add("90d0f3d40769a6219608964628d40e55"); - set.add("9104737f888d85480d0cc9aef8587e77"); - set.add("9118a19b2abc5d1d624b10f2bceb18bb"); - set.add("912e499f9a4a55f11133e01b33542ad1"); - set.add("915fcc373ba5d8a13814f236c1a9e4e5"); - set.add("918ca652867678984ae1149a3b5467bd"); - set.add("91fbebd600bbd88370994b739ae8e1f8"); - set.add("92fc949a982c897ca4a17750e2ee4afd"); - set.add("93c0446ee508efe75a176035986627cc"); - set.add("93d4329e22ed50d3502b2d0bc117baa6"); - set.add("93f33bcfa6201057376a3fe53eb29959"); - set.add("944b74b5ff9c617312ca2b0868e8cbc2"); - set.add("94bacf4caccc653a74a68698be0da4bc"); - set.add("9572f2ed73f01766b0ede9ec3d52715a"); - set.add("965e3d6087eec8e991175aada15a550a"); - set.add("967119411833b80c9ea55f0e64dacad6"); - set.add("968c5025a59e782d96682b09c4e94746"); - set.add("97824aa7746b63a32ea6d0dedb3e3f84"); - set.add("97aa914f28281f67ae3ac5222566c2a0"); - set.add("97f5a198489144a2f306456c1a346f9b"); - set.add("98a7e979d454d7f46ceb4a4283794d3c"); - set.add("98ff8ee9107e864d7c52d631070fff3b"); - set.add("993739fad4a47f34eb65e3ee48d15c09"); - set.add("99bb411f89eb34ebfa59900de36581fc"); - set.add("9a13940746bcf4cbe210150833b2a58b"); - set.add("9a3d7af6ccb7d277e3ed582d8932b5db"); - set.add("9a76e86b4275099983c5ede78626e0dd"); - set.add("9a9caad4a9c674daf41b5cb616092501"); - set.add("9ae6b0ad5010301ea610f49e008adf8c"); - set.add("9b6033bd4470408ecf2949d88531d6a1"); - set.add("9bfc7853ff00c7ea0e2f8972dc2595d4"); - set.add("9c8bdd485912f9d9eaaba3d5872be726"); - set.add("9cba07b76b4e79c0265deda5df2e2381"); - set.add("9e082b9bb6c1361965c0f0e06f051acb"); - set.add("9e24dbadcadc67447af65d571ffaee55"); - set.add("9e6a5f03a8b524ffa3264a3f32818e1c"); - set.add("9ead837b9e4f8c922f74ddbff0d2b88a"); - set.add("9fb7aa659c0475d5dc72bb35567247c9"); - set.add("a0006978c9a542518b425c0caa67042b"); - set.add("a01bdd6575c3cad9f9a4cb8aac9c716a"); - set.add("a02500e28eeb7e56e343607a822e2a7e"); - set.add("a05c1799e061712459e6c46f533263a6"); - set.add("a0799831bfb3f9b77b63c03fad39cce0"); - set.add("a0d4911294ccb20c0920a3cc6705f326"); - set.add("a11dfa1b02b1671d42b1abc858de2f2e"); - set.add("a11e237bd6d3c4a4ee8a7ee791444ad3"); - set.add("a148d83d50cf0852f6c08ceacbea0296"); - set.add("a1d8b81c03860585fb40613e828c1b2e"); - set.add("a20c867fdbb93bbe1d1231d9a8ea76c5"); - set.add("a21e0795fe0977d50a4496ba60e685e1"); - set.add("a260bb11468a2252a8dedff81f5672fd"); - set.add("a2b01bf43bc1d403e6ab3a9b71f32484"); - set.add("a2c15ded3e16d9aa12b9c07f3383c098"); - set.add("a360659a62e2e34f4edc89ce5e8b8a0c"); - set.add("a3a985e0ae5a9c96c74b8ee647101439"); - set.add("a3bf05e31288667a43b4c39cc0604c97"); - set.add("a427397e35b28706c5b8aa50e8f92a1c"); - set.add("a432e1b27b7d9814368d8f4071cf2dd0"); - set.add("a4b4800082feb6fcaf1cd56dda3a28c6"); - set.add("a4b83742cb45f1dd9130250cd72c460e"); - set.add("a5a8b20a222bd51b707e50303fdae33a"); - set.add("a5cf16d12d611ddc5ae1b010083690ad"); - set.add("a67b1720a7f123bb943c3f1ee74b8f00"); - set.add("a6b31c2e971254f85e231653abdc3a06"); - set.add("a6f9fe8c867cbef07730db4d1d461960"); - set.add("a706de20cf1a31e379d98807d1cb2060"); - set.add("a7b5467023706646a6b8ce2602bba226"); - set.add("a7bb7f7f68b538fb778856d4fbda50b7"); - set.add("a7fee39f2151056370c57d383e486348"); - set.add("a84a5f90f1083075081f135c74314bff"); - set.add("a8a6b73342c6a92638e41b86e6838958"); - set.add("a8f1c8b28c017186778e3074637e52ef"); - set.add("a90e513d9b2d0f55b875c3229e2d9816"); - set.add("a9e697026e08d1a8765497a9569b04e6"); - set.add("aa3218984177ce38bfdf06e38fbaa64b"); - set.add("aaa0291aa11c304b3a2300d4025db74d"); - set.add("aad63a3685d9f906a7c6c8716d626c0b"); - set.add("aafee591c7a3ae5f3c4f44f2d0f8a70f"); - set.add("ab85503c9acb730fcb9ed2a4dd74e2d7"); - set.add("ab8ad454409604808d1b444b068e602d"); - set.add("ac4c8af4d29676c8c79ac9ef180fc5df"); - set.add("ac4cd34387b04786cc5315b464006ec8"); - set.add("ac9c443698ac77bcb3a73a959f6ca0f0"); - set.add("acde934989eba2c7fef7cce095ce85c7"); - set.add("ad053830e5d0bb7e736ab98a8f3f1612"); - set.add("ad08d0d2d84298deb08b4c4a1cf62f39"); - set.add("ad0a1f2424a1b831f9777e638e8f829a"); - set.add("add039636134cb123908df5053304f3e"); - set.add("adf89cbcb01a2ec6d4afb24914790a67"); - set.add("ae1ae7c31f46325ce6a28104fa7070e6"); - set.add("af8c83664fd6eec8089ef1992aec463f"); - set.add("afcd59e32572ecb7ebe2d9c993d5fa9d"); - set.add("b012115b4276791c5049dace174933f7"); - set.add("b218489d2d4d7ddbfee2f073e238ff30"); - set.add("b251290e1d8690953d9cc0c1ea3bac6f"); - set.add("b2843a551894de318b80f790565dcfe3"); - set.add("b2a414aeb8800edfa8945863ffa5fbc9"); - set.add("b2d68ad2619bbb15a827b7baca6677b0"); - set.add("b2fe203ee319ae28b9ccdad26a8f21de"); - set.add("b33afd95fbd9aae903bbe7cb853cbbf3"); - set.add("b385f0f86168fea4f5f456b7700a8ffe"); - set.add("b3bd462a51847d58ed348f17c8718dca"); - set.add("b3d1befe2272f354b85f0ca5a3162dc8"); - set.add("b3f50d0da11772487101b44ae8aeb4ac"); - set.add("b42625f51295803ae1f99daf241c0bd0"); - set.add("b49cdae29a3394a25058e94b4eb5573c"); - set.add("b4ce8f683ec172aecf22cf8e516cce05"); - set.add("b4ffd04e41c1b8757149777a894f33f2"); - set.add("b5a1510fcf6dd596e87f497bfd5317bb"); - set.add("b5a75d8c18db0a96a3423e06554122c8"); - set.add("b5d312d32267bd15ee43f36077feefe9"); - set.add("b6645bb07f58754b8838d54e24562c06"); - set.add("b69831350ae6a3dfc18c0c05db0c25a8"); - set.add("b6b70e569be8de2fdecf285730319735"); - set.add("b6ee0ea7d82d3d7e0ab8bc62057c0385"); - set.add("b707a076a44ca9b3e6b8dc1dcde7d877"); - set.add("b77df6081bbeb4da02c075befb4beb9b"); - set.add("b7bdcedd416cccc742643e8e244f6282"); - set.add("b7ea4565381c6dc534cf0af8305f27ac"); - set.add("b7f3fb01d8c41525b103fc5faba23362"); - set.add("b80bf674f28284a3614596800ec02b3a"); - set.add("b81ab08e53854aba9462ebbaee1ff248"); - set.add("b87e12381d354360f7945299ad92a4d2"); - set.add("b8bd5737f81fddbaf120ebe43a0010e4"); - set.add("b92f1e45fdb62c1fd6d7f3e774b4b610"); - set.add("b9769bfc0d93a570d94fa130de750b1f"); - set.add("b980c7a501ce63ebb6e20b04348298b7"); - set.add("b9e4b006db3e1597b21fb7aba13f79c2"); - set.add("ba031cf2febc03ddbff278200dca45a0"); - set.add("bb0f54037f5ab70e96c7d8ba7f57ca4b"); - set.add("bb2eb6b3f7383d7ef521409fa7710385"); - set.add("bb3eb6a5dbe0582b31f35e5dc2b457a7"); - set.add("bbc22cc7f6851e06fadfac40a6225419"); - set.add("bc4d886813fe2eba9ccd7bef0b4d05ca"); - set.add("bc8162e261658ece13f8bc61aa43ab74"); - set.add("bc89ec14f4629c3afe1e286b07e588f6"); - set.add("bccdb576cb50b44ae34806c2a2781c52"); - set.add("bd10772f1554ccd6e245d6869d84efe8"); - set.add("bd969e90ff4b1362e2f24553124a66cc"); - set.add("bde145f138ed13399b8a747da20c1826"); - set.add("be11a726b56813c4b1aea0574c8302b2"); - set.add("be1cfa6a82eb4fbf7186efd6ddbb0161"); - set.add("be5f3dcf0badef84475741cc47e2ddc0"); - set.add("bf316e6ad7e72f7dc4033207dd033c76"); - set.add("bfadbf9c4cde6071e54e43a5da64aca9"); - set.add("c029503ea85e7a7742d826bc184d65ce"); - set.add("c049499ca03fd69115352e5d4be72de7"); - set.add("c0524ddd036991b4718f6ab0ab4e4f42"); - set.add("c056cf25df6751f7bb8a94bc4f64750f"); - set.add("c0a9c2fd4f0ed2b8f2bdc7f2c0a7a7ce"); - set.add("c165e480073bcdccb3fad1c5e507159f"); - set.add("c24ac1ab205eb3fbd1c64841f0f288d6"); - set.add("c26987c1c7e95810bbb6f2e284861494"); - set.add("c295b3b2493aff880daac1532a063b72"); - set.add("c2b18390691697037d5698b683eee361"); - set.add("c2cd680e3032ce3a70d3bffdb7d0582f"); - set.add("c2defcfb93d217c4be28aa27ec62978b"); - set.add("c332adf9d689dcbbb38fead7098781b3"); - set.add("c4d8f1baafe99501b0d80e8a9c8c3086"); - set.add("c4e5ca3e96b219539e3e61c3c4fbe5a9"); - set.add("c5e1448d1fb24ebcef161ee65f21a725"); - set.add("c60db0ccfc2a22a152f7470505eef8d3"); - set.add("c65e2561352e75a66b5738268b1d126a"); - set.add("c69c01ed9b781941561c3a9dcfacf7ca"); - set.add("c76bb0011d2519fc9e3af85de593e8a9"); - set.add("c7a946bb164a3f642e4c5f1b7af337f1"); - set.add("c833820441cbbf28a25d1ea7934ad6f8"); - set.add("c8762972b9325b7ec040c782aa9414d0"); - set.add("c8b1563c45f4fd4dc8ba5fafd5c566d2"); - set.add("c8f2a5a0533de5eae8d1d01da8fcfc1c"); - set.add("c94045226f625ab9a507552f64892fbe"); - set.add("ca365baface31f6167328e65a0aec03b"); - set.add("ca3dc74a6eb57042ea911afa05b1021b"); - set.add("ca5b57fca35c5bfa4281802b13381d0c"); - set.add("cab05efb1584bddbc5e4f064c1628a13"); - set.add("cad2db4a8a73a867a6cdacceec4044ac"); - set.add("cb6b65a06bbb9ba5536936188a08d836"); - set.add("cba49b7c8d1982635866a32956861df3"); - set.add("cbd94e882bdb84ec79ea2bebc1eb4aed"); - set.add("cc49ecf163d383488b44dbb17dd5b4d9"); - set.add("cc4bcc37de4d7bf87acea95ac914997e"); - set.add("cc9300ecd7f2799c750ca3efcde7ce20"); - set.add("ccccf43f691ed8bb94ac27d3eab98659"); - set.add("cd0d8952d3e5742f4bf62195e4b385ec"); - set.add("cd57964f0a86f3c9afbcca44733081d2"); - set.add("cd80d4f5366cd31ae31e377c636a773a"); - set.add("cd987a30667f7ff04a5852fd3f15fe3b"); - set.add("ce16d46a26c771b1adbff14cc7272bf2"); - set.add("ce43a9cf2d81a1e09237ed2279ca04be"); - set.add("ce7a500ffd8f5925bea7a48401fae95e"); - set.add("cf3894401e317e2545d0ae284802291f"); - set.add("cf779cafecefb6bae5137bb77582e7e2"); - set.add("cfdbc0be3829a6d55a5403ad92c77bcf"); - set.add("cff24b5ef480cd58324e927e4ba0ed37"); - set.add("d05a83622e817871306a3878a9d867e9"); - set.add("d060e6662cda17c2c523c74963d2d556"); - set.add("d0b058971d8a20e840de043c68c140b1"); - set.add("d0c122a8a62cb5728f1423816b26c49f"); - set.add("d141ed3ad5b33d6f92c976ad8d518b3b"); - set.add("d15c7bdb5fc7b9316d1cc60a85acdc64"); - set.add("d1c8ba5392f01f13dfef28e4ecd11cc2"); - set.add("d1e661c0bfe1c23ca4e91cfa0a40a9d3"); - set.add("d25a8aef0d42d7a31783e3da52dd4ee8"); - set.add("d26ef38d4ea39ba85171f444a8947443"); - set.add("d30b7d8663c7852f2be21d886b79a6eb"); - set.add("d315c862f290b99466e3d406d3104413"); - set.add("d4007ee1fd6ede4a148bdca7ab778dd3"); - set.add("d4805374b5f1ece75c0dd51d6283a0f6"); - set.add("d55b77d76f6ece8bbd33cb2afdbd040f"); - set.add("d570cb3cbfe88dfdf086bb1ab9ef76f8"); - set.add("d587ebc9902ba2620d52a610441470cc"); - set.add("d6196bfd55d758dd5a4899ce84cea85b"); - set.add("d6815ec0b3e12fea0f470c01c0da3888"); - set.add("d68971ffd271de2ddde6ac10c2817451"); - set.add("d68ff175d25d083eee4f31bf0701a0d8"); - set.add("d7fca16ff4e4ed99b72046f99533eef3"); - set.add("d815d7a8dd0446ba21ddbc37d3733f36"); - set.add("d8d40312d0751951b4c45f2100379950"); - set.add("d97e0d0fbea2a015d5e7ea229b99a6c3"); - set.add("d98c6a23fafafb0f9981f2123b1a1938"); - set.add("d99d6bfaede491ceae109a644cf54098"); - set.add("da0f523e815ce990a3a5f5b5574cec4a"); - set.add("da686b7f2a26a36051b273bbabdd7ecc"); - set.add("dadb54d4a8ba762a8d2a2fad5fcd7582"); - set.add("db03b5399e1a324660934ad81036d987"); - set.add("db29f48ac45b41ad389b1b4e8e30f553"); - set.add("db98a87e4be1e5b59f589335e1509996"); - set.add("db9be69a1dd929be69406f9e75533fd3"); - set.add("dba45dfda4d06aebf3adc194ca4ec35d"); - set.add("dc339714def04ec3e5e280faec8445e5"); - set.add("dc3fb757715c96f7c629b7712d87ca61"); - set.add("dc5dabf20b3fbee0d3a0b1d800c74d4f"); - set.add("dcc7b2c56f358641ea9a73c6a81479f5"); - set.add("dd922d98ecf5096482946d8671b647e3"); - set.add("dda8214d7b53392f8ed9fbe4178e90b9"); - set.add("ddd37af5af0a69bed06bc50cc0a6f4c2"); - set.add("de8d217fad9883d9bfdee5af01472971"); - set.add("def34fc0fd41527b300d3181a70cdecf"); - set.add("df00d9361332c48cba23bfcd41e799d4"); - set.add("df35772f10769bc28701c488d33e89b2"); - set.add("df769f9dc2477135b0c4320e7e7b4c2f"); - set.add("df95377a3f69b8fbe5dcdfa041491717"); - set.add("df98c3766238aa84f9a9dd92cd73fe72"); - set.add("dfd019d69302047a67434458d0fa8952"); - set.add("e037a9e26a8b319437ab7c962714dc56"); - set.add("e0d2f02d29a965fafd85a4ae6ad37246"); - set.add("e10e651cd85e41be3402f51885bbf107"); - set.add("e162d7b2e436ae6d369f4fbaf53af4b4"); - set.add("e176177a2b64669a6bcd1cf8beb35df2"); - set.add("e194252a63a3433b5a5278f68678b7dc"); - set.add("e19d16546c555d073454ea56ece1cbd6"); - set.add("e1cb375938189d4090b000ab41c77a06"); - set.add("e1ce2428389f0c2356881e626f161ccc"); - set.add("e27c22419443eb612af1620f4c8be007"); - set.add("e2a5adcdd7b01611736b6b74f8c506ee"); - set.add("e3084721ba7ae53996337e82c5f469ab"); - set.add("e35f4cccfe57bdd7338dadeba55609f1"); - set.add("e39a2ef2eaaaf7ba74623f14c917ee1d"); - set.add("e3e11cf57dc3f1c6ca59acb06370698f"); - set.add("e4557f7733332200116b753408cdb966"); - set.add("e48c96d6025b38addad2278f24c963ef"); - set.add("e48db7db130af48cccb2d830d3cbaa14"); - set.add("e4d169990e34bfeab0c6a780d6a49d58"); - set.add("e4ea1f6b01c9cdcf9e550792ed336384"); - set.add("e4ee8ada1fbbe886fb25a7f484609690"); - set.add("e54846d325334547923d8b64da70f529"); - set.add("e56ccf6eca77d62dde88c895abfc1c1a"); - set.add("e591790779db1c866b179d6f85b09dda"); - set.add("e5bec4799ceef43816054f92de9652b5"); - set.add("e5db5832d59e14d6999144fa8cd10e3f"); - set.add("e611fb9e857f9bee391056e1f971a0aa"); - set.add("e6321fdd099d70352883b45f6c2a20a9"); - set.add("e682fc42ee7ecdbf595116293cbe8a6b"); - set.add("e6ae48418d10883fe9657075b476274d"); - set.add("e6fa2a139e5c56f8f483aaeeee0b7fbb"); - set.add("e77d3c78240ec60f7f4dd67a2e71085a"); - set.add("e78ad15fb1fd450f9221147e458b1abd"); - set.add("e7a1dc89a6cab821776ea61fe6ba10f4"); - set.add("e7df074666f6caa44b798342bdab6230"); - set.add("e7f25163d78c2c658300cd0f9a8a3b04"); - set.add("e80f22347419025053de7da1f07912ec"); - set.add("e828123fa3cdf86dc0fe1b5c86d7c87d"); - set.add("e8764c00097a0a1254f43a16c98a1d7f"); - set.add("e89df60deddf270cbc2232bbe26420d1"); - set.add("e8d289f3c1aa961cf4ac8d164e286dde"); - set.add("e9051eac7829dc1a95987230fb21d2d9"); - set.add("e90846d2c3e16de5ed5dff4c21356edd"); - set.add("e954907cdfba1cf07f19f64af5cf45b1"); - set.add("e96e004e988b8e36b2ab9ed1b0f65649"); - set.add("e984d3924451d3498a3efccd845a77fe"); - set.add("e98b4097ddb057755e561c33a0f3428d"); - set.add("e9a2ba17cc4b93063d61813359fd6798"); - set.add("ea90b42f6ada6e0ac5d179af4129022d"); - set.add("eab4231af5b3ffab13f9a04b8aef0fad"); - set.add("eac17a7d30d24e60e6b2ce10059b37a0"); - set.add("eaf3af4d0b61df60ee8fe3a808de6ffd"); - set.add("eb3178d36a578fd8b430ea82429ee145"); - set.add("eb4fbf9835a3584e6015260f0dff2174"); - set.add("ec3a9437b1481db4d8cbc3b4fc0888a1"); - set.add("ec47d133c23dba3d015ae730a1b9659f"); - set.add("ec6d321581a133fee9766eedff4db9d6"); - set.add("eca16f6d986bd893af3c4a97b99df623"); - set.add("ecf09182c51e5110d636828364ba3ee6"); - set.add("ecfbf6f7017f0981ba6d30331c9dc870"); - set.add("ed1eaef061f0d30503a64f27d8ea2825"); - set.add("ed2e4441ad7dcbe0a5d3e62bc31aa9bc"); - set.add("ed6304572c0f1283fd06f9c09ef32b61"); - set.add("ed7d650fc0f5de8c9da107e53025f220"); - set.add("ef405c5b0de638c01cf57c379aaff45b"); - set.add("ef5ec03860cd32e910a3ddb2d8740300"); - set.add("efdc8c21ee843f98a0dc08ef694b6db7"); - set.add("f0111af67e822944e1dc1ab24e5b8458"); - set.add("f0e8026289bc1f9109b25f4599556aaf"); - set.add("f0ff5a7aa6f4e8785fa406636618c01d"); - set.add("f19a809facb46a7a094c17039126eb3e"); - set.add("f1c7524d454c25cdd837662a80709030"); - set.add("f202d26f911074235ac8e4a5c1ed4dad"); - set.add("f2250dd8736aa007a7e2530dca1c6081"); - set.add("f2e9d36561ed03eb23d38005919625d2"); - set.add("f303e8a2a96635d2f44c414229c349bb"); - set.add("f35b8eac398bae58ba622ef643f64aa2"); - set.add("f3a37dbd51e59af2801272fffe457d64"); - set.add("f3c4afc965427977685da607f8a6edca"); - set.add("f468c204661ab47379613a1d03f56571"); - set.add("f4ff8d1667c95377ac96e870170bfe64"); - set.add("f585dccae7fae67affbf500ecf9a3839"); - set.add("f59a4193ec432bd26f780a6b74b8be38"); - set.add("f5d44e9d1523c3e6834108d9c76b2da9"); - set.add("f69f58acf2b80f5dc29682c64de8da7f"); - set.add("f6adafaf98b92438e1ad916e51a65366"); - set.add("f6f175a7910c5039d0fa51393c920df8"); - set.add("f71a00225b1abf1dddfcace79d0345a2"); - set.add("f7446eb342242f011c118bb3439335a0"); - set.add("f76e6e86c9b0d84638c1c26c55b16cc4"); - set.add("f775463704e3d13817abd9674d62f468"); - set.add("f80df9b85c1219fd2030ada584fbfc35"); - set.add("f843fa1d0cd00122fcbcfd7caf1cb8ca"); - set.add("f88e05d8303a6e5dfbd60ceed3988d78"); - set.add("f92dbcd91aac65e0770f5fe947fc5a80"); - set.add("f9512c5cc198adeff74fed3d4b0f4509"); - set.add("f9e1ffe33f3676d0e39bc24e63cf3a99"); - set.add("fa492225fbf03ad58ee88b6d84914583"); - set.add("fa6279cc58de3fe6c617559684afec4f"); - set.add("fb2a4db1a1a68dae79653dd2e32ade50"); - set.add("fb2bc93f011d62ac03aed40d92b26ba2"); - set.add("fb9b6d2d2d5e3c219e0163092181e014"); - set.add("fbdc7fc274e5c71226372606beedb706"); - set.add("fbe25dc54e2761c2c5e9f1f3a98d7f0f"); - set.add("fbec5f910872b333be655c5143b1cb37"); - set.add("fc372707722b210b257ef9e2f731edc3"); - set.add("fcedc7e1d4fc17c7c4f2c6f6c7a820e0"); - set.add("fcff88f351f2535dcbab726dec9060ee"); - set.add("fd15d45e5f876ac3ff10cef478077e8b"); - set.add("fd21ff84af0fe74de102f1985d068dee"); - set.add("fd30f89057cd8ad151d612def38afb41"); - set.add("fdab6eed0ecadf924ae128f25e8b1a10"); - set.add("fdced723077daed39d0e4da044f75d64"); - set.add("fddbc361461ae318e147e420a805a428"); - set.add("fdee78ddeb6f567a636b9850f942256f"); - set.add("fe858217631f3aaf02d8aaf849c7b2c9"); - set.add("fec4bbfe3563397790d48ce6f5b48260"); - set.add("ff73d7804897f4e1624a3b5571e48fbb"); - set.add("ff78c3b27889d5365a03b3a3fd3a4c1e"); - set.add("ffac65c383eb053e54b267fe4dfd2141"); - - DIGESTS = Collections.unmodifiableSet(set); - } - - private PreferredMotorDigests() { - // Prevent instantiation - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/BodyComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/BodyComponentSaver.java deleted file mode 100644 index 1a65676b..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/BodyComponentSaver.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -public class BodyComponentSaver extends ExternalComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - // Body components have a natural length, store it now - elements.add("<length>"+((net.sf.openrocket.rocketcomponent.BodyComponent)c).getLength()+"</length>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java b/src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java deleted file mode 100644 index 8b1ef6a5..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/BodyTubeSaver.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class BodyTubeSaver extends SymmetricComponentSaver { - - private static final BodyTubeSaver instance = new BodyTubeSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<bodytube>"); - instance.addParams(c, list); - list.add("</bodytube>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube) c; - - if (tube.isOuterRadiusAutomatic()) - elements.add("<radius>auto</radius>"); - else - elements.add("<radius>" + tube.getOuterRadius() + "</radius>"); - - if (tube.isMotorMount()) { - elements.addAll(motorMountParams(tube)); - } - } - - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/BulkheadSaver.java b/src/net/sf/openrocket/file/openrocket/savers/BulkheadSaver.java deleted file mode 100644 index 9c0ef2d8..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/BulkheadSaver.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class BulkheadSaver extends RadiusRingComponentSaver { - - private static final BulkheadSaver instance = new BulkheadSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<bulkhead>"); - instance.addParams(c, list); - list.add("</bulkhead>"); - - return list; - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/CenteringRingSaver.java b/src/net/sf/openrocket/file/openrocket/savers/CenteringRingSaver.java deleted file mode 100644 index bb428a3d..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/CenteringRingSaver.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class CenteringRingSaver extends RadiusRingComponentSaver { - - private static final CenteringRingSaver instance = new CenteringRingSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<centeringring>"); - instance.addParams(c, list); - list.add("</centeringring>"); - - return list; - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/ComponentAssemblySaver.java b/src/net/sf/openrocket/file/openrocket/savers/ComponentAssemblySaver.java deleted file mode 100644 index 9e9d609d..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/ComponentAssemblySaver.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -public class ComponentAssemblySaver extends RocketComponentSaver { - - // No-op - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/EllipticalFinSetSaver.java b/src/net/sf/openrocket/file/openrocket/savers/EllipticalFinSetSaver.java deleted file mode 100644 index 31b9dfb0..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/EllipticalFinSetSaver.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class EllipticalFinSetSaver extends FinSetSaver { - - private static final EllipticalFinSetSaver instance = new EllipticalFinSetSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<ellipticalfinset>"); - instance.addParams(c,list); - list.add("</ellipticalfinset>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - net.sf.openrocket.rocketcomponent.EllipticalFinSet fins = (net.sf.openrocket.rocketcomponent.EllipticalFinSet)c; - elements.add("<rootchord>"+fins.getLength()+"</rootchord>"); - elements.add("<height>"+fins.getHeight()+"</height>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/EngineBlockSaver.java b/src/net/sf/openrocket/file/openrocket/savers/EngineBlockSaver.java deleted file mode 100644 index 921ddde5..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/EngineBlockSaver.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class EngineBlockSaver extends ThicknessRingComponentSaver { - - private static final EngineBlockSaver instance = new EngineBlockSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<engineblock>"); - instance.addParams(c, list); - list.add("</engineblock>"); - - return list; - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/ExternalComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/ExternalComponentSaver.java deleted file mode 100644 index 2f1b2a4e..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/ExternalComponentSaver.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.ExternalComponent; - - -public class ExternalComponentSaver extends RocketComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - ExternalComponent ext = (ExternalComponent)c; - - // Finish enum names are currently the same except for case - elements.add("<finish>" + ext.getFinish().name().toLowerCase() + "</finish>"); - - // Material - elements.add(materialParam(ext.getMaterial())); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java b/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java deleted file mode 100644 index 8756d89f..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/FinSetSaver.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.util.MathUtil; - -public class FinSetSaver extends ExternalComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - net.sf.openrocket.rocketcomponent.FinSet fins = (net.sf.openrocket.rocketcomponent.FinSet) c; - elements.add("<fincount>" + fins.getFinCount() + "</fincount>"); - elements.add("<rotation>" + (fins.getBaseRotation() * 180.0 / Math.PI) + "</rotation>"); - elements.add("<thickness>" + fins.getThickness() + "</thickness>"); - elements.add("<crosssection>" + fins.getCrossSection().name().toLowerCase() - + "</crosssection>"); - elements.add("<cant>" + (fins.getCantAngle() * 180.0 / Math.PI) + "</cant>"); - - // Save fin tabs only if they exist (compatibility with file version < 1.1) - if (!MathUtil.equals(fins.getTabHeight(),0) && - !MathUtil.equals(fins.getTabLength(), 0)) { - - elements.add("<tabheight>" + fins.getTabHeight() + "</tabheight>"); - elements.add("<tablength>" + fins.getTabLength() + "</tablength>"); - elements.add("<tabposition relativeto=\"" + - fins.getTabRelativePosition().name().toLowerCase() + "\">" + - fins.getTabShift() + "</tabposition>"); - - } - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/FreeformFinSetSaver.java b/src/net/sf/openrocket/file/openrocket/savers/FreeformFinSetSaver.java deleted file mode 100644 index c310e4d9..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/FreeformFinSetSaver.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.util.Coordinate; - - -public class FreeformFinSetSaver extends FinSetSaver { - - private static final FreeformFinSetSaver instance = new FreeformFinSetSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<freeformfinset>"); - instance.addParams(c,list); - list.add("</freeformfinset>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - FreeformFinSet fins = (FreeformFinSet)c; - elements.add("<finpoints>"); - for (Coordinate p: fins.getFinPoints()) { - elements.add(" <point x=\"" + p.x + "\" y=\"" + p.y + "\"/>"); - } - elements.add("</finpoints>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/InnerTubeSaver.java b/src/net/sf/openrocket/file/openrocket/savers/InnerTubeSaver.java deleted file mode 100644 index 7eb0a538..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/InnerTubeSaver.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.InnerTube; - - -public class InnerTubeSaver extends ThicknessRingComponentSaver { - - private static final InnerTubeSaver instance = new InnerTubeSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<innertube>"); - instance.addParams(c, list); - list.add("</innertube>"); - - return list; - } - - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - InnerTube tube = (InnerTube) c; - - elements.add("<clusterconfiguration>" + tube.getClusterConfiguration().getXMLName() - + "</clusterconfiguration>"); - elements.add("<clusterscale>" + tube.getClusterScale() + "</clusterscale>"); - elements.add("<clusterrotation>" + (tube.getClusterRotation() * 180.0 / Math.PI) - + "</clusterrotation>"); - - if (tube.isMotorMount()) { - elements.addAll(motorMountParams(tube)); - } - - - } - - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/InternalComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/InternalComponentSaver.java deleted file mode 100644 index 0a2dbe72..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/InternalComponentSaver.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -public class InternalComponentSaver extends RocketComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - // Nothing to save - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java b/src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java deleted file mode 100644 index 1598bc41..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/LaunchLugSaver.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import net.sf.openrocket.rocketcomponent.LaunchLug; - -import java.util.ArrayList; -import java.util.List; - - -public class LaunchLugSaver extends ExternalComponentSaver { - - private static final LaunchLugSaver instance = new LaunchLugSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<launchlug>"); - instance.addParams(c, list); - list.add("</launchlug>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - LaunchLug lug = (LaunchLug) c; - - elements.add("<radius>" + lug.getOuterRadius() + "</radius>"); - elements.add("<length>" + lug.getLength() + "</length>"); - elements.add("<thickness>" + lug.getThickness() + "</thickness>"); - elements.add("<radialdirection>" + (lug.getRadialDirection()*180.0/Math.PI) + "</radialdirection>"); - } - - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/MassComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/MassComponentSaver.java deleted file mode 100644 index 093303c2..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/MassComponentSaver.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.MassComponent; - - -public class MassComponentSaver extends MassObjectSaver { - - private static final MassComponentSaver instance = new MassComponentSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<masscomponent>"); - instance.addParams(c, list); - list.add("</masscomponent>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - MassComponent mass = (MassComponent) c; - - elements.add("<mass>" + mass.getMass() + "</mass>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/MassObjectSaver.java b/src/net/sf/openrocket/file/openrocket/savers/MassObjectSaver.java deleted file mode 100644 index 298cb263..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/MassObjectSaver.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.MassObject; - - -public class MassObjectSaver extends InternalComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - MassObject mass = (MassObject) c; - - elements.add("<packedlength>" + mass.getLength() + "</packedlength>"); - elements.add("<packedradius>" + mass.getRadius() + "</packedradius>"); - elements.add("<radialposition>" + mass.getRadialPosition() + "</radialposition>"); - elements.add("<radialdirection>" + (mass.getRadialDirection() * 180.0 / Math.PI) - + "</radialdirection>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java b/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java deleted file mode 100644 index 9733254f..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/NoseConeSaver.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class NoseConeSaver extends TransitionSaver { - - private static final NoseConeSaver instance = new NoseConeSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<nosecone>"); - instance.addParams(c,list); - list.add("</nosecone>"); - - return list; - } - - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - // Transition handles nose cone saving as well - } -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/ParachuteSaver.java b/src/net/sf/openrocket/file/openrocket/savers/ParachuteSaver.java deleted file mode 100644 index 7d906e67..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/ParachuteSaver.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.Parachute; - - -public class ParachuteSaver extends RecoveryDeviceSaver { - - private static final ParachuteSaver instance = new ParachuteSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<parachute>"); - instance.addParams(c, list); - list.add("</parachute>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - Parachute para = (Parachute) c; - - elements.add("<diameter>" + para.getDiameter() + "</diameter>"); - elements.add("<linecount>" + para.getLineCount() + "</linecount>"); - elements.add("<linelength>" + para.getLineLength() + "</linelength>"); - elements.add(materialParam("linematerial", para.getLineMaterial())); - } - - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/RadiusRingComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/RadiusRingComponentSaver.java deleted file mode 100644 index c1b90204..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/RadiusRingComponentSaver.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.RadiusRingComponent; - - -public class RadiusRingComponentSaver extends RingComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - RadiusRingComponent comp = (RadiusRingComponent)c; - if (comp.isOuterRadiusAutomatic()) - elements.add("<outerradius>auto</outerradius>"); - else - elements.add("<outerradius>" + comp.getOuterRadius() + "</outerradius>"); - if (!(comp instanceof Bulkhead)) { - if (comp.isInnerRadiusAutomatic()) - elements.add("<innerradius>auto</innerradius>"); - else - elements.add("<innerradius>" + comp.getInnerRadius() + "</innerradius>"); - } - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/RecoveryDeviceSaver.java b/src/net/sf/openrocket/file/openrocket/savers/RecoveryDeviceSaver.java deleted file mode 100644 index 22dcaa9c..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/RecoveryDeviceSaver.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.RecoveryDevice; - - -public class RecoveryDeviceSaver extends MassObjectSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - RecoveryDevice dev = (RecoveryDevice) c; - - if (dev.isCDAutomatic()) - elements.add("<cd>auto</cd>"); - else - elements.add("<cd>" + dev.getCD() + "</cd>"); - - elements.add("<deployevent>" + dev.getDeployEvent().name().toLowerCase() + "</deployevent>"); - elements.add("<deployaltitude>" + dev.getDeployAltitude() + "</deployaltitude>"); - elements.add("<deploydelay>" + dev.getDeployDelay() + "</deploydelay>"); - elements.add(materialParam(dev.getMaterial())); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/RingComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/RingComponentSaver.java deleted file mode 100644 index c1999fba..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/RingComponentSaver.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.RingComponent; - - -public class RingComponentSaver extends StructuralComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - RingComponent ring = (RingComponent) c; - - elements.add("<length>" + ring.getLength() + "</length>"); - elements.add("<radialposition>" + ring.getRadialPosition() + "</radialposition>"); - elements.add("<radialdirection>" + (ring.getRadialDirection() * 180.0 / Math.PI) - + "</radialdirection>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java deleted file mode 100644 index 44e71e4b..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/RocketComponentSaver.java +++ /dev/null @@ -1,159 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.rocketcomponent.ComponentAssembly; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Color; -import net.sf.openrocket.util.LineStyle; - - -public class RocketComponentSaver { - - protected RocketComponentSaver() { - // Prevent instantiation from outside the package - } - - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - elements.add("<name>" + RocketSaver.escapeXML(c.getName()) + "</name>"); - - - // Save color and line style if significant - if (!(c instanceof Rocket || c instanceof ComponentAssembly)) { - Color color = c.getColor(); - if (color != null) { - elements.add("<color red=\"" + color.getRed() + "\" green=\"" + color.getGreen() - + "\" blue=\"" + color.getBlue() + "\"/>"); - } - - LineStyle style = c.getLineStyle(); - if (style != null) { - // Type names currently equivalent to the enum names except for case. - elements.add("<linestyle>" + style.name().toLowerCase() + "</linestyle>"); - } - } - - - // Save position unless "AFTER" - if (c.getRelativePosition() != RocketComponent.Position.AFTER) { - // The type names are currently equivalent to the enum names except for case. - String type = c.getRelativePosition().name().toLowerCase(); - elements.add("<position type=\"" + type + "\">" + c.getPositionValue() + "</position>"); - } - - - // Overrides - boolean overridden = false; - if (c.isMassOverridden()) { - elements.add("<overridemass>" + c.getOverrideMass() + "</overridemass>"); - overridden = true; - } - if (c.isCGOverridden()) { - elements.add("<overridecg>" + c.getOverrideCGX() + "</overridecg>"); - overridden = true; - } - if (overridden) { - elements.add("<overridesubcomponents>" + c.getOverrideSubcomponents() - + "</overridesubcomponents>"); - } - - - // Comment - if (c.getComment().length() > 0) { - elements.add("<comment>" + RocketSaver.escapeXML(c.getComment()) + "</comment>"); - } - - } - - - - - protected final String materialParam(Material mat) { - return materialParam("material", mat); - } - - - protected final String materialParam(String tag, Material mat) { - String str = "<" + tag; - - switch (mat.getType()) { - case LINE: - str += " type=\"line\""; - break; - case SURFACE: - str += " type=\"surface\""; - break; - case BULK: - str += " type=\"bulk\""; - break; - default: - throw new BugException("Unknown material type: " + mat.getType()); - } - - return str + " density=\"" + mat.getDensity() + "\">" + RocketSaver.escapeXML(mat.getName()) + "</" + tag + ">"; - } - - - protected final List<String> motorMountParams(MotorMount mount) { - if (!mount.isMotorMount()) - return Collections.emptyList(); - - String[] motorConfigIDs = ((RocketComponent) mount).getRocket().getMotorConfigurationIDs(); - List<String> elements = new ArrayList<String>(); - - elements.add("<motormount>"); - - for (String id : motorConfigIDs) { - Motor motor = mount.getMotor(id); - - // Nothing is stored if no motor loaded - if (motor == null) - continue; - - elements.add(" <motor configid=\"" + id + "\">"); - if (motor.getMotorType() != Motor.Type.UNKNOWN) { - elements.add(" <type>" + motor.getMotorType().name().toLowerCase() + "</type>"); - } - if (motor instanceof ThrustCurveMotor) { - ThrustCurveMotor m = (ThrustCurveMotor) motor; - elements.add(" <manufacturer>" + RocketSaver.escapeXML(m.getManufacturer().getSimpleName()) + - "</manufacturer>"); - elements.add(" <digest>" + MotorDigest.digestMotor(m) + "</digest>"); - } - elements.add(" <designation>" + RocketSaver.escapeXML(motor.getDesignation()) + "</designation>"); - elements.add(" <diameter>" + motor.getDiameter() + "</diameter>"); - elements.add(" <length>" + motor.getLength() + "</length>"); - - // Motor delay - if (mount.getMotorDelay(id) == Motor.PLUGGED) { - elements.add(" <delay>none</delay>"); - } else { - elements.add(" <delay>" + mount.getMotorDelay(id) + "</delay>"); - } - - elements.add(" </motor>"); - } - - elements.add(" <ignitionevent>" - + mount.getIgnitionEvent().name().toLowerCase().replace("_", "") - + "</ignitionevent>"); - - elements.add(" <ignitiondelay>" + mount.getIgnitionDelay() + "</ignitiondelay>"); - elements.add(" <overhang>" + mount.getMotorOverhang() + "</overhang>"); - - elements.add("</motormount>"); - - return elements; - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java b/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java deleted file mode 100644 index e8b7a345..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/RocketSaver.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.ReferenceType; -import net.sf.openrocket.rocketcomponent.Rocket; - - -public class RocketSaver extends RocketComponentSaver { - - private static final RocketSaver instance = new RocketSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<rocket>"); - instance.addParams(c, list); - list.add("</rocket>"); - - return list; - } - - - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - Rocket rocket = (Rocket) c; - - if (rocket.getDesigner().length() > 0) { - elements.add("<designer>" - + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getDesigner()) - + "</designer>"); - } - if (rocket.getRevision().length() > 0) { - elements.add("<revision>" - + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getRevision()) - + "</revision>"); - } - - - // Motor configurations - String defId = rocket.getDefaultConfiguration().getMotorConfigurationID(); - for (String id : rocket.getMotorConfigurationIDs()) { - if (id == null) - continue; - - String str = "<motorconfiguration configid=\"" + id + "\""; - if (id.equals(defId)) - str += " default=\"true\""; - - if (rocket.getMotorConfigurationName(id) == "") { - str += "/>"; - } else { - str += "><name>" + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getMotorConfigurationName(id)) - + "</name></motorconfiguration>"; - } - elements.add(str); - } - - // Reference diameter - elements.add("<referencetype>" + rocket.getReferenceType().name().toLowerCase() - + "</referencetype>"); - if (rocket.getReferenceType() == ReferenceType.CUSTOM) { - elements.add("<customreference>" + rocket.getCustomReferenceLength() - + "</customreference>"); - } - - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/ShockCordSaver.java b/src/net/sf/openrocket/file/openrocket/savers/ShockCordSaver.java deleted file mode 100644 index 8b8ae01f..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/ShockCordSaver.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.ShockCord; - - -public class ShockCordSaver extends MassObjectSaver { - - private static final ShockCordSaver instance = new ShockCordSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<shockcord>"); - instance.addParams(c, list); - list.add("</shockcord>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - ShockCord mass = (ShockCord) c; - - elements.add("<cordlength>" + mass.getCordLength() + "</cordlength>"); - elements.add(materialParam(mass.getMaterial())); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/StageSaver.java b/src/net/sf/openrocket/file/openrocket/savers/StageSaver.java deleted file mode 100644 index 0fd0f6f3..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/StageSaver.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; - -public class StageSaver extends ComponentAssemblySaver { - - private static final StageSaver instance = new StageSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<stage>"); - instance.addParams(c,list); - list.add("</stage>"); - - return list; - } - - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/StreamerSaver.java b/src/net/sf/openrocket/file/openrocket/savers/StreamerSaver.java deleted file mode 100644 index 5b92852e..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/StreamerSaver.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.Streamer; - - -public class StreamerSaver extends RecoveryDeviceSaver { - - private static final StreamerSaver instance = new StreamerSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<streamer>"); - instance.addParams(c, list); - list.add("</streamer>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - Streamer st = (Streamer) c; - - elements.add("<striplength>" + st.getStripLength() + "</striplength>"); - elements.add("<stripwidth>" + st.getStripWidth() + "</stripwidth>"); - } - - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/StructuralComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/StructuralComponentSaver.java deleted file mode 100644 index 5cf5ce70..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/StructuralComponentSaver.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.StructuralComponent; - - -public class StructuralComponentSaver extends InternalComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - StructuralComponent comp = (StructuralComponent)c; - elements.add(materialParam(comp.getMaterial())); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/SymmetricComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/SymmetricComponentSaver.java deleted file mode 100644 index bab5eccb..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/SymmetricComponentSaver.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -public class SymmetricComponentSaver extends BodyComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - net.sf.openrocket.rocketcomponent.SymmetricComponent comp = (net.sf.openrocket.rocketcomponent.SymmetricComponent)c; - if (comp.isFilled()) - elements.add("<thickness>filled</thickness>"); - else - elements.add("<thickness>"+comp.getThickness()+"</thickness>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/ThicknessRingComponentSaver.java b/src/net/sf/openrocket/file/openrocket/savers/ThicknessRingComponentSaver.java deleted file mode 100644 index d5a3c1b3..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/ThicknessRingComponentSaver.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.List; - -import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; - - -public class ThicknessRingComponentSaver extends RingComponentSaver { - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - ThicknessRingComponent comp = (ThicknessRingComponent)c; - if (comp.isOuterRadiusAutomatic()) - elements.add("<outerradius>auto</outerradius>"); - else - elements.add("<outerradius>" + comp.getOuterRadius() + "</outerradius>"); - elements.add("<thickness>" + comp.getThickness() + "</thickness>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java b/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java deleted file mode 100644 index d7bb1ed7..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/TransitionSaver.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Transition; - - -public class TransitionSaver extends SymmetricComponentSaver { - - private static final TransitionSaver instance = new TransitionSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<transition>"); - instance.addParams(c, list); - list.add("</transition>"); - - return list; - } - - - /* - * Note: This method must be capable of handling nose cones as well. - */ - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - net.sf.openrocket.rocketcomponent.Transition trans = (net.sf.openrocket.rocketcomponent.Transition) c; - boolean nosecone = (trans instanceof NoseCone); - - - Transition.Shape shape = trans.getType(); - elements.add("<shape>" + shape.name().toLowerCase() + "</shape>"); - if (shape.isClippable()) { - elements.add("<shapeclipped>" + trans.isClipped() + "</shapeclipped>"); - } - if (shape.usesParameter()) { - elements.add("<shapeparameter>" + trans.getShapeParameter() + "</shapeparameter>"); - } - - - if (!nosecone) { - if (trans.isForeRadiusAutomatic()) - elements.add("<foreradius>auto</foreradius>"); - else - elements.add("<foreradius>" + trans.getForeRadius() + "</foreradius>"); - } - - if (trans.isAftRadiusAutomatic()) - elements.add("<aftradius>auto</aftradius>"); - else - elements.add("<aftradius>" + trans.getAftRadius() + "</aftradius>"); - - - if (!nosecone) { - elements.add("<foreshoulderradius>" + trans.getForeShoulderRadius() - + "</foreshoulderradius>"); - elements.add("<foreshoulderlength>" + trans.getForeShoulderLength() - + "</foreshoulderlength>"); - elements.add("<foreshoulderthickness>" + trans.getForeShoulderThickness() - + "</foreshoulderthickness>"); - elements.add("<foreshouldercapped>" + trans.isForeShoulderCapped() - + "</foreshouldercapped>"); - } - - elements.add("<aftshoulderradius>" + trans.getAftShoulderRadius() - + "</aftshoulderradius>"); - elements.add("<aftshoulderlength>" + trans.getAftShoulderLength() - + "</aftshoulderlength>"); - elements.add("<aftshoulderthickness>" + trans.getAftShoulderThickness() - + "</aftshoulderthickness>"); - elements.add("<aftshouldercapped>" + trans.isAftShoulderCapped() - + "</aftshouldercapped>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/TrapezoidFinSetSaver.java b/src/net/sf/openrocket/file/openrocket/savers/TrapezoidFinSetSaver.java deleted file mode 100644 index 21ac2aa8..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/TrapezoidFinSetSaver.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class TrapezoidFinSetSaver extends FinSetSaver { - - private static final TrapezoidFinSetSaver instance = new TrapezoidFinSetSaver(); - - public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - ArrayList<String> list = new ArrayList<String>(); - - list.add("<trapezoidfinset>"); - instance.addParams(c,list); - list.add("</trapezoidfinset>"); - - return list; - } - - @Override - protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) { - super.addParams(c, elements); - - net.sf.openrocket.rocketcomponent.TrapezoidFinSet fins = (net.sf.openrocket.rocketcomponent.TrapezoidFinSet)c; - elements.add("<rootchord>"+fins.getRootChord()+"</rootchord>"); - elements.add("<tipchord>"+fins.getTipChord()+"</tipchord>"); - elements.add("<sweeplength>"+fins.getSweep()+"</sweeplength>"); - elements.add("<height>"+fins.getHeight()+"</height>"); - } - -} diff --git a/src/net/sf/openrocket/file/openrocket/savers/TubeCouplerSaver.java b/src/net/sf/openrocket/file/openrocket/savers/TubeCouplerSaver.java deleted file mode 100644 index 0e19fd8d..00000000 --- a/src/net/sf/openrocket/file/openrocket/savers/TubeCouplerSaver.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.file.openrocket.savers; - -import java.util.ArrayList; -import java.util.List; - -public class TubeCouplerSaver extends ThicknessRingComponentSaver { - - private static final TubeCouplerSaver instance = new TubeCouplerSaver(); - - public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) { - List<String> list = new ArrayList<String>(); - - list.add("<tubecoupler>"); - instance.addParams(c, list); - list.add("</tubecoupler>"); - - return list; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/TipShapeCode.java b/src/net/sf/openrocket/file/rocksim/TipShapeCode.java deleted file mode 100644 index 976bdc9c..00000000 --- a/src/net/sf/openrocket/file/rocksim/TipShapeCode.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.sf.openrocket.file.rocksim; - -import net.sf.openrocket.rocketcomponent.FinSet; - -/** - */ -public final class TipShapeCode { - - /** - * Convert a Rocksim tip shape to an OpenRocket CrossSection. - * - * @param tipShape the tip shape code from Rocksim - * - * @return a CrossSection instance - */ - public static FinSet.CrossSection convertTipShapeCode (int tipShape) { - switch (tipShape) { - case 0: - return FinSet.CrossSection.SQUARE; - case 1: - return FinSet.CrossSection.ROUNDED; - case 2: - return FinSet.CrossSection.AIRFOIL; - default: - return FinSet.CrossSection.SQUARE; - } - } - - public static int convertTipShapeCode (FinSet.CrossSection cs) { - if (FinSet.CrossSection.ROUNDED.equals(cs)) { - return 1; - } - if (FinSet.CrossSection.AIRFOIL.equals(cs)) { - return 2; - } - return 0; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/AbstractTransitionDTO.java b/src/net/sf/openrocket/file/rocksim/export/AbstractTransitionDTO.java deleted file mode 100644 index ce5db241..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/AbstractTransitionDTO.java +++ /dev/null @@ -1,74 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.file.rocksim.importt.RocksimNoseConeCode; -import net.sf.openrocket.rocketcomponent.Transition; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; - -/** - */ -@XmlAccessorType(XmlAccessType.FIELD) -public class AbstractTransitionDTO extends BasePartDTO { - @XmlElement(name = "ShapeCode") - private int shapeCode = 1; - @XmlElement(name = "ConstructionType") - private int constructionType = 1; - @XmlElement(name = "WallThickness") - private double wallThickness = 0d; - @XmlElement(name = "ShapeParameter") - private double shapeParameter = 0d; - - protected AbstractTransitionDTO() { - - } - - protected AbstractTransitionDTO(Transition nc) { - super(nc); - setConstructionType(nc.isFilled() ? 0 : 1); - setShapeCode(RocksimNoseConeCode.toCode(nc.getType())); - - if (Transition.Shape.POWER.equals(nc.getType()) || - Transition.Shape.HAACK.equals(nc.getType()) || - Transition.Shape.PARABOLIC.equals(nc.getType())) { - setShapeParameter(nc.getShapeParameter()); - } - - setWallThickness(nc.getThickness() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - - } - - public int getShapeCode() { - return shapeCode; - } - - public void setShapeCode(int theShapeCode) { - shapeCode = theShapeCode; - } - - public int getConstructionType() { - return constructionType; - } - - public void setConstructionType(int theConstructionType) { - constructionType = theConstructionType; - } - - public double getWallThickness() { - return wallThickness; - } - - public void setWallThickness(double theWallThickness) { - wallThickness = theWallThickness; - } - - public double getShapeParameter() { - return shapeParameter; - } - - public void setShapeParameter(double theShapeParameter) { - shapeParameter = theShapeParameter; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/BasePartDTO.java b/src/net/sf/openrocket/file/rocksim/export/BasePartDTO.java deleted file mode 100644 index 48a4d910..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/BasePartDTO.java +++ /dev/null @@ -1,238 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.BaseHandler; -import net.sf.openrocket.file.rocksim.importt.RocksimDensityType; -import net.sf.openrocket.file.rocksim.importt.RocksimFinishCode; -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.file.rocksim.importt.RocksimLocationMode; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.StructuralComponent; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement -@XmlAccessorType(XmlAccessType.FIELD) -public abstract class BasePartDTO { - - @XmlElement(name = "KnownMass") - private Double knownMass = 0d; - @XmlElement(name = "Density") - private double density = 0d; - @XmlElement(name = "Material") - private String material = ""; - @XmlElement(name = "Name") - private String name = ""; - @XmlElement(name = "KnownCG") - private Double knownCG = null; - @XmlElement(name = "UseKnownCG") - private int useKnownCG = 1; - @XmlElement(name = "Xb") - private double xb = 0; - @XmlElement(name = "CalcMass") - private double calcMass = 0d; - @XmlElement(name = "CalcCG") - private double calcCG = 0d; - @XmlElement(name = "DensityType") - private int densityType = 0; - @XmlElement(name = "RadialLoc") - private String radialLoc = "0."; - @XmlElement(name = "RadialAngle") - private double radialAngle = 0; - @XmlElement(name = "LocationMode") - private int locationMode = 0; - @XmlElement(name = "Len") - private double len = 0d; - @XmlElement(name = "FinishCode") - private int finishCode = 0; - - protected BasePartDTO() { - } - - protected BasePartDTO(RocketComponent ec) { - setCalcCG(ec.getCG().x * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setCalcMass(ec.getComponentMass() * RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - setKnownCG(ec.getOverrideCGX() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setKnownMass(ec.getOverrideMass() * RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - setLen(ec.getLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setUseKnownCG(ec.isCGOverridden() || ec.isMassOverridden() ? 1 : 0); - setName(ec.getName()); - - setXb(ec.getPositionValue() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - if (ec instanceof ExternalComponent) { - ExternalComponent comp = (ExternalComponent) ec; - setLocationMode(RocksimLocationMode.toCode(comp.getRelativePosition())); - - if (comp.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { - setXb(-1 * getXb()); - } - setDensity(comp.getMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY); - setDensityType(RocksimDensityType.toCode(comp.getMaterial().getType())); - String material = comp.getMaterial().getName(); - if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { - material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); - } - setMaterial(material); - - setFinishCode(RocksimFinishCode.toCode(comp.getFinish())); - } - else if (ec instanceof StructuralComponent) { - StructuralComponent comp = (StructuralComponent) ec; - - setLocationMode(RocksimLocationMode.toCode(comp.getRelativePosition())); - if (comp.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { - setXb(-1 * getXb()); - } - setDensity(comp.getMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY); - setDensityType(RocksimDensityType.toCode(comp.getMaterial().getType())); - String material = comp.getMaterial().getName(); - if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { - material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); - } - setMaterial(material); - } - else if (ec instanceof RecoveryDevice) { - RecoveryDevice comp = (RecoveryDevice) ec; - - setLocationMode(RocksimLocationMode.toCode(comp.getRelativePosition())); - if (comp.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { - setXb(-1 * getXb()); - } - setDensity(comp.getMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_SURFACE_DENSITY); - setDensityType(RocksimDensityType.toCode(comp.getMaterial().getType())); - String material = comp.getMaterial().getName(); - if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { - material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); - } - setMaterial(material); - - } - } - - public Double getKnownMass() { - return knownMass; - } - - public void setKnownMass(Double theKnownMass) { - knownMass = theKnownMass; - } - - public double getDensity() { - return density; - } - - public void setDensity(double theDensity) { - density = theDensity; - } - - public String getMaterial() { - return material; - } - - public void setMaterial(String theMaterial) { - material = theMaterial; - } - - public String getName() { - return name; - } - - public void setName(String theName) { - name = theName; - } - - public Double getKnownCG() { - return knownCG; - } - - public void setKnownCG(Double theKnownCG) { - knownCG = theKnownCG; - } - - public int getUseKnownCG() { - return useKnownCG; - } - - public void setUseKnownCG(int theUseKnownCG) { - useKnownCG = theUseKnownCG; - } - - public double getXb() { - return xb; - } - - public void setXb(double theXb) { - xb = theXb; - } - - public double getCalcMass() { - return calcMass; - } - - public void setCalcMass(double theCalcMass) { - calcMass = theCalcMass; - } - - public double getCalcCG() { - return calcCG; - } - - public void setCalcCG(double theCalcCG) { - calcCG = theCalcCG; - } - - public int getDensityType() { - return densityType; - } - - public void setDensityType(int theDensityType) { - densityType = theDensityType; - } - - public String getRadialLoc() { - return radialLoc; - } - - public void setRadialLoc(String theRadialLoc) { - radialLoc = theRadialLoc; - } - - public double getRadialAngle() { - return radialAngle; - } - - public void setRadialAngle(double theRadialAngle) { - radialAngle = theRadialAngle; - } - - public int getLocationMode() { - return locationMode; - } - - public void setLocationMode(int theLocationMode) { - locationMode = theLocationMode; - } - - public double getLen() { - return len; - } - - public void setLen(double theLen) { - len = theLen; - } - - public int getFinishCode() { - return finishCode; - } - - public void setFinishCode(int theFinishCode) { - finishCode = theFinishCode; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java b/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java deleted file mode 100644 index f9325d85..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/BodyTubeDTO.java +++ /dev/null @@ -1,182 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TubeCoupler; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementRef; -import javax.xml.bind.annotation.XmlElementRefs; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.ArrayList; -import java.util.List; - -/** - */ -@XmlRootElement(name = "BodyTube") -@XmlAccessorType(XmlAccessType.FIELD) -public class BodyTubeDTO extends BasePartDTO { - - @XmlElement(name = "OD") - private double od = 0d; - @XmlElement(name = "ID") - private double id = 0d; - @XmlElement(name = "IsMotorMount") - private int isMotorMount = 0; - @XmlElement(name = "MotorDia") - private double motorDia = 0d; - @XmlElement(name = "EngineOverhang") - private double engineOverhang = 0d; - @XmlElement(name = "IsInsideTube") - private int isInsideTube = 0; - @XmlElementWrapper(name = "AttachedParts") - @XmlElementRefs({ - @XmlElementRef(name = "BodyTube", type = BodyTubeDTO.class), - @XmlElementRef(name = "BodyTube", type = InnerBodyTubeDTO.class), - @XmlElementRef(name = "Ring", type = CenteringRingDTO.class), - @XmlElementRef(name = "LaunchLug", type = LaunchLugDTO.class), - @XmlElementRef(name = "FinSet", type = FinSetDTO.class), - @XmlElementRef(name = "CustomFinSet", type = CustomFinSetDTO.class), - @XmlElementRef(name = "Streamer", type = StreamerDTO.class), - @XmlElementRef(name = "Parachute", type = ParachuteDTO.class), - @XmlElementRef(name = "MassObject", type = MassObjectDTO.class)}) - List<BasePartDTO> attachedParts = new ArrayList<BasePartDTO>(); - - public BodyTubeDTO() { - } - - public BodyTubeDTO(InnerTube inner) { - super(inner); - } - - public BodyTubeDTO(BodyTube bt) { - super(bt); - - setEngineOverhang(bt.getMotorOverhang() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setId(bt.getInnerRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setOd(bt.getOuterRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setMotorDia((bt.getMotorMountDiameter() / 2) * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setMotorMount(bt.isMotorMount()); - - List<RocketComponent> children = bt.getChildren(); - for (int i = 0; i < children.size(); i++) { - RocketComponent rocketComponents = children.get(i); - if (rocketComponents instanceof InnerTube) { - attachedParts.add(new InnerBodyTubeDTO((InnerTube) rocketComponents)); - } else if (rocketComponents instanceof BodyTube) { - attachedParts.add(new BodyTubeDTO((BodyTube) rocketComponents)); - } else if (rocketComponents instanceof Transition) { - attachedParts.add(new TransitionDTO((Transition) rocketComponents)); - } else if (rocketComponents instanceof EngineBlock) { - attachedParts.add(new EngineBlockDTO((EngineBlock) rocketComponents)); - } else if (rocketComponents instanceof TubeCoupler) { - attachedParts.add(new TubeCouplerDTO((TubeCoupler) rocketComponents)); - } else if (rocketComponents instanceof CenteringRing) { - attachedParts.add(new CenteringRingDTO((CenteringRing) rocketComponents)); - } else if (rocketComponents instanceof Bulkhead) { - attachedParts.add(new BulkheadDTO((Bulkhead) rocketComponents)); - } else if (rocketComponents instanceof LaunchLug) { - attachedParts.add(new LaunchLugDTO((LaunchLug) rocketComponents)); - } else if (rocketComponents instanceof Streamer) { - attachedParts.add(new StreamerDTO((Streamer) rocketComponents)); - } else if (rocketComponents instanceof Parachute) { - attachedParts.add(new ParachuteDTO((Parachute) rocketComponents)); - } else if (rocketComponents instanceof MassObject) { - attachedParts.add(new MassObjectDTO((MassObject) rocketComponents)); - } else if (rocketComponents instanceof FreeformFinSet) { - attachedParts.add(new CustomFinSetDTO((FreeformFinSet) rocketComponents)); - } else if (rocketComponents instanceof FinSet) { - attachedParts.add(new FinSetDTO((FinSet) rocketComponents)); - } - - } - } - - public double getOd() { - return od; - } - - public void setOd(double theOd) { - od = theOd; - } - - public double getId() { - return id; - } - - public void setId(double theId) { - id = theId; - } - - public int getMotorMount() { - return isMotorMount; - } - - public void setMotorMount(boolean motorMount) { - if (motorMount) { - isMotorMount = 1; - } else { - isMotorMount = 0; - } - - } - - public void setMotorMount(int theMotorMount) { - isMotorMount = theMotorMount; - } - - public double getMotorDia() { - return motorDia; - } - - public void setMotorDia(double theMotorDia) { - motorDia = theMotorDia; - } - - public double getEngineOverhang() { - return engineOverhang; - } - - public void setEngineOverhang(double theEngineOverhang) { - engineOverhang = theEngineOverhang; - } - - public int getInsideTube() { - return isInsideTube; - } - - public void setInsideTube(boolean inside) { - if (inside) { - isInsideTube = 1; - } else { - isInsideTube = 0; - } - } - - public void setInsideTube(int theInsideTube) { - isInsideTube = theInsideTube; - } - - public List<BasePartDTO> getAttachedParts() { - return attachedParts; - } - - public void addAttachedParts(BasePartDTO thePart) { - attachedParts.add(thePart); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/BulkheadDTO.java b/src/net/sf/openrocket/file/rocksim/export/BulkheadDTO.java deleted file mode 100644 index c5a82bfc..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/BulkheadDTO.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.rocketcomponent.Bulkhead; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "Ring") -@XmlAccessorType(XmlAccessType.FIELD) -public class BulkheadDTO extends CenteringRingDTO { - public BulkheadDTO(Bulkhead bh) { - super(bh); - setUsageCode(CenteringRingDTO.UsageCode.Bulkhead); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/CenteringRingDTO.java b/src/net/sf/openrocket/file/rocksim/export/CenteringRingDTO.java deleted file mode 100644 index e2e8a7c8..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/CenteringRingDTO.java +++ /dev/null @@ -1,94 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.RadiusRingComponent; -import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; -import javax.xml.bind.annotation.XmlTransient; - -/** - */ -@XmlRootElement(name = "Ring") -@XmlAccessorType(XmlAccessType.FIELD) -public class CenteringRingDTO extends BasePartDTO { - - @XmlTransient - protected enum UsageCode { - //UsageCode - CenteringRing(0), - Bulkhead(1), - EngineBlock(2), - Sleeve(3), - TubeCoupler(4); - - int ordinal; - - UsageCode(int x) { - ordinal = x; - } - } - - @XmlElement(name = "OD") - private double od = 0d; - @XmlElement(name = "ID") - private double id = 0d; - @XmlElement(name = "UsageCode") - private int usageCode = UsageCode.CenteringRing.ordinal; - @XmlElement(name = "AutoSize") - private int autoSize = 0; - - public CenteringRingDTO() { - - } - public CenteringRingDTO(RadiusRingComponent cr) { - super(cr); - setId(cr.getInnerRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setOd(cr.getOuterRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - } - - public CenteringRingDTO(ThicknessRingComponent trc) { - super(trc); - setId(trc.getInnerRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setOd(trc.getOuterRadius()* RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - } - public double getOd() { - return od; - } - - public void setOd(double theOd) { - od = theOd; - } - - public double getId() { - return id; - } - - public void setId(double theId) { - id = theId; - } - - public int getUsageCode() { - return usageCode; - } - - public void setUsageCode(int theUsageCode) { - usageCode = theUsageCode; - } - - public void setUsageCode(UsageCode theUsageCode) { - usageCode = theUsageCode.ordinal; - } - - public int getAutoSize() { - return autoSize; - } - - public void setAutoSize(int theAutoSize) { - autoSize = theAutoSize; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/export/CustomFinSetDTO.java b/src/net/sf/openrocket/file/rocksim/export/CustomFinSetDTO.java deleted file mode 100644 index 99112c9d..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/CustomFinSetDTO.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.util.Coordinate; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "CustomFinSet") -@XmlAccessorType(XmlAccessType.FIELD) -public class CustomFinSetDTO extends FinSetDTO { - - @XmlElement(name = "PointList") - private String pointList = ""; - - public CustomFinSetDTO() { - } - - public CustomFinSetDTO(FreeformFinSet ec) { - super(ec); - setPointList(convertFreeFormPoints(ec.getFinPoints())); - } - - - private String convertFreeFormPoints(Coordinate[] points) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < points.length; i++) { - Coordinate point = points[i]; - sb.append(point.x * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH).append(",") - .append(point.y * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH).append("|"); - } - return sb.toString(); - } - - public String getPointList() { - return pointList; - } - - public void setPointList(String thePointList) { - pointList = thePointList; - } -} - diff --git a/src/net/sf/openrocket/file/rocksim/export/EngineBlockDTO.java b/src/net/sf/openrocket/file/rocksim/export/EngineBlockDTO.java deleted file mode 100644 index bcb6d575..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/EngineBlockDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.rocketcomponent.EngineBlock; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "Ring") -@XmlAccessorType(XmlAccessType.FIELD) -public class EngineBlockDTO extends CenteringRingDTO{ - - public EngineBlockDTO(EngineBlock eb) { - super(eb); - setUsageCode(UsageCode.EngineBlock); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java b/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java deleted file mode 100644 index b97571c1..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/FinSetDTO.java +++ /dev/null @@ -1,191 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.TipShapeCode; -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "FinSet") -@XmlAccessorType(XmlAccessType.FIELD) -public class FinSetDTO extends BasePartDTO { - - @XmlElement(name = "FinCount") - private int finCount = 0; - @XmlElement(name = "RootChord") - private double rootChord = 0d; - @XmlElement(name = "TipChord") - private double tipChord = 0d; - @XmlElement(name = "SemiSpan") - private double semiSpan = 0d; - @XmlElement(name = "SweepDistance") - private double sweepDistance = 0d; - @XmlElement(name = "Thickness") - private double thickness = 0d; - @XmlElement(name = "ShapeCode") - private int shapeCode = 0; - @XmlElement(name = "TipShapeCode") - private int tipShapeCode = 0; - @XmlElement(name = "TabLength") - private double tabLength = 0d; - @XmlElement(name = "TabDepth") - private double tabDepth = 0d; - @XmlElement(name = "TabOffset") - private double tabOffset = 0d; - @XmlElement(name = "SweepMode") - private int sweepMode = 1; - @XmlElement(name = "CantAngle") - private double cantAngle = 0d; - - public FinSetDTO() { - } - - public FinSetDTO(FinSet ec) { - super(ec); - - setCantAngle(ec.getCantAngle()); - setFinCount(ec.getFinCount()); - setRootChord(ec.getLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setTabDepth(ec.getTabHeight() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setTabLength(ec.getTabLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setTabOffset(ec.getTabShift() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setThickness(ec.getThickness() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - - if (ec.getRelativePosition().equals(RocketComponent.Position.BOTTOM)) { - setXb(getXb() + getLen()); - } - - setRadialAngle(ec.getBaseRotation()); - setTipShapeCode(TipShapeCode.convertTipShapeCode(ec.getCrossSection())); - if (ec instanceof TrapezoidFinSet) { - TrapezoidFinSet tfs = (TrapezoidFinSet) ec; - setShapeCode(0); - setSemiSpan(tfs.getHeight() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setTipChord(tfs.getTipChord() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setSweepDistance(tfs.getSweep() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setLen(tfs.getLength()); - } - else if (ec instanceof EllipticalFinSet) { - EllipticalFinSet efs = (EllipticalFinSet) ec; - setShapeCode(1); - setSemiSpan(efs.getHeight() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setLen(efs.getLength() *RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - else if (ec instanceof FreeformFinSet) { - setShapeCode(2); - } - } - - public int getFinCount() { - return finCount; - } - - public void setFinCount(int theFinCount) { - finCount = theFinCount; - } - - public double getRootChord() { - return rootChord; - } - - public void setRootChord(double theRootChord) { - rootChord = theRootChord; - } - - public double getTipChord() { - return tipChord; - } - - public void setTipChord(double theTipChord) { - tipChord = theTipChord; - } - - public double getSemiSpan() { - return semiSpan; - } - - public void setSemiSpan(double theSemiSpan) { - semiSpan = theSemiSpan; - } - - public double getSweepDistance() { - return sweepDistance; - } - - public void setSweepDistance(double theSweepDistance) { - sweepDistance = theSweepDistance; - } - - public double getThickness() { - return thickness; - } - - public void setThickness(double theThickness) { - thickness = theThickness; - } - - public int getShapeCode() { - return shapeCode; - } - - public void setShapeCode(int theShapeCode) { - shapeCode = theShapeCode; - } - - public int getTipShapeCode() { - return tipShapeCode; - } - - public void setTipShapeCode(int theTipShapeCode) { - tipShapeCode = theTipShapeCode; - } - - public double getTabLength() { - return tabLength; - } - - public void setTabLength(double theTabLength) { - tabLength = theTabLength; - } - - public double getTabDepth() { - return tabDepth; - } - - public void setTabDepth(double theTabDepth) { - tabDepth = theTabDepth; - } - - public double getTabOffset() { - return tabOffset; - } - - public void setTabOffset(double theTabOffset) { - tabOffset = theTabOffset; - } - - public int getSweepMode() { - return sweepMode; - } - - public void setSweepMode(int theSweepMode) { - sweepMode = theSweepMode; - } - - public double getCantAngle() { - return cantAngle; - } - - public void setCantAngle(double theCantAngle) { - cantAngle = theCantAngle; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java b/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java deleted file mode 100644 index c898abb2..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/InnerBodyTubeDTO.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TubeCoupler; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.List; - -/** - */ -@XmlRootElement(name = "BodyTube") -@XmlAccessorType(XmlAccessType.FIELD) -public class InnerBodyTubeDTO extends BodyTubeDTO { - - public InnerBodyTubeDTO() { - super.setInsideTube(true); - } - - public InnerBodyTubeDTO(InnerTube bt) { - super(bt); - setEngineOverhang(bt.getMotorOverhang() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setId(bt.getInnerRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setOd(bt.getOuterRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setMotorDia((bt.getMotorMountDiameter() / 2) * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setMotorMount(bt.isMotorMount()); - - List<RocketComponent> children = bt.getChildren(); - for (int i = 0; i < children.size(); i++) { - RocketComponent rocketComponents = children.get(i); - if (rocketComponents instanceof InnerTube) { - attachedParts.add(new InnerBodyTubeDTO((InnerTube) rocketComponents)); - } else if (rocketComponents instanceof BodyTube) { - attachedParts.add(new BodyTubeDTO((BodyTube) rocketComponents)); - } else if (rocketComponents instanceof Transition) { - attachedParts.add(new TransitionDTO((Transition) rocketComponents)); - } else if (rocketComponents instanceof EngineBlock) { - attachedParts.add(new EngineBlockDTO((EngineBlock) rocketComponents)); - } else if (rocketComponents instanceof TubeCoupler) { - attachedParts.add(new TubeCouplerDTO((TubeCoupler) rocketComponents)); - } else if (rocketComponents instanceof CenteringRing) { - attachedParts.add(new CenteringRingDTO((CenteringRing) rocketComponents)); - } else if (rocketComponents instanceof Bulkhead) { - attachedParts.add(new BulkheadDTO((Bulkhead) rocketComponents)); - } else if (rocketComponents instanceof Streamer) { - attachedParts.add(new StreamerDTO((Streamer) rocketComponents)); - } else if (rocketComponents instanceof Parachute) { - attachedParts.add(new ParachuteDTO((Parachute) rocketComponents)); - } else if (rocketComponents instanceof MassObject) { - attachedParts.add(new MassObjectDTO((MassObject) rocketComponents)); - } - } - setInsideTube(true); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/LaunchLugDTO.java b/src/net/sf/openrocket/file/rocksim/export/LaunchLugDTO.java deleted file mode 100644 index f2932c83..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/LaunchLugDTO.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.LaunchLug; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "LaunchLug") -@XmlAccessorType(XmlAccessType.FIELD) -public class LaunchLugDTO extends BasePartDTO { - - @XmlElement(name = "OD") - private double od = 0d; - @XmlElement(name = "ID") - private double id = 0d; - - public LaunchLugDTO() { - } - - public LaunchLugDTO(LaunchLug ec) { - super(ec); - setId(ec.getInnerRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setOd(ec.getOuterRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setRadialAngle(ec.getRadialDirection()); - } - - public double getOd() { - return od; - } - - public void setOd(double theOd) { - od = theOd; - } - - public double getId() { - return id; - } - - public void setId(double theId) { - id = theId; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/MassObjectDTO.java b/src/net/sf/openrocket/file/rocksim/export/MassObjectDTO.java deleted file mode 100644 index ea48844d..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/MassObjectDTO.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.rocketcomponent.MassObject; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "MassObject") -@XmlAccessorType(XmlAccessType.FIELD) -public class MassObjectDTO extends BasePartDTO{ - - @XmlElement(name = "TypeCode") - private int typeCode = 0; - - public MassObjectDTO() { - } - - public MassObjectDTO(MassObject ec) { - super(ec); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/NoseConeDTO.java b/src/net/sf/openrocket/file/rocksim/export/NoseConeDTO.java deleted file mode 100644 index 4a154db9..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/NoseConeDTO.java +++ /dev/null @@ -1,114 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TubeCoupler; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlElementRef; -import javax.xml.bind.annotation.XmlElementRefs; -import javax.xml.bind.annotation.XmlElementWrapper; -import javax.xml.bind.annotation.XmlRootElement; -import java.util.ArrayList; -import java.util.List; - -/** - */ -@XmlRootElement(name = "NoseCone") -@XmlAccessorType(XmlAccessType.FIELD) -public class NoseConeDTO extends AbstractTransitionDTO { - - - @XmlElement(name = "BaseDia") - private double baseDia = 0d; - @XmlElement(name = "ShoulderLen") - private double shoulderLen = 0d; - @XmlElement(name = "ShoulderOD") - private double shoulderOD = 0d; - - @XmlElementWrapper(name = "AttachedParts") - @XmlElementRefs({ - @XmlElementRef(name = "BodyTube", type = BodyTubeDTO.class), - @XmlElementRef(name = "BodyTube", type = InnerBodyTubeDTO.class), - @XmlElementRef(name = "FinSet", type = FinSetDTO.class), - @XmlElementRef(name = "CustomFinSet", type = CustomFinSetDTO.class), - @XmlElementRef(name = "Ring", type = CenteringRingDTO.class), - @XmlElementRef(name = "Parachute", type = ParachuteDTO.class), - @XmlElementRef(name = "MassObject", type = MassObjectDTO.class)}) - List<BasePartDTO> attachedParts = new ArrayList<BasePartDTO>(); - - public NoseConeDTO() { - } - - public NoseConeDTO(NoseCone nc) { - super(nc); - setBaseDia(nc.getAftRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setShoulderLen(nc.getAftShoulderLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setShoulderOD(nc.getAftShoulderRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - - List<RocketComponent> children = nc.getChildren(); - for (int i = 0; i < children.size(); i++) { - RocketComponent rocketComponents = children.get(i); - if (rocketComponents instanceof InnerTube) { - attachedParts.add(new InnerBodyTubeDTO((InnerTube) rocketComponents)); - } else if (rocketComponents instanceof BodyTube) { - attachedParts.add(new BodyTubeDTO((BodyTube) rocketComponents)); - } else if (rocketComponents instanceof Transition) { - attachedParts.add(new TransitionDTO((Transition) rocketComponents)); - } else if (rocketComponents instanceof EngineBlock) { - attachedParts.add(new EngineBlockDTO((EngineBlock) rocketComponents)); - } else if (rocketComponents instanceof TubeCoupler) { - attachedParts.add(new TubeCouplerDTO((TubeCoupler) rocketComponents)); - } else if (rocketComponents instanceof CenteringRing) { - attachedParts.add(new CenteringRingDTO((CenteringRing) rocketComponents)); - } else if (rocketComponents instanceof Bulkhead) { - attachedParts.add(new BulkheadDTO((Bulkhead) rocketComponents)); - } else if (rocketComponents instanceof Parachute) { - attachedParts.add(new ParachuteDTO((Parachute) rocketComponents)); - } else if (rocketComponents instanceof MassObject) { - attachedParts.add(new MassObjectDTO((MassObject) rocketComponents)); - } else if (rocketComponents instanceof FreeformFinSet) { - attachedParts.add(new CustomFinSetDTO((FreeformFinSet) rocketComponents)); - } else if (rocketComponents instanceof FinSet) { - attachedParts.add(new FinSetDTO((FinSet) rocketComponents)); - } - } - } - - public double getBaseDia() { - return baseDia; - } - - public void setBaseDia(double theBaseDia) { - baseDia = theBaseDia; - } - - public double getShoulderLen() { - return shoulderLen; - } - - public void setShoulderLen(double theShoulderLen) { - shoulderLen = theShoulderLen; - } - - public double getShoulderOD() { - return shoulderOD; - } - - public void setShoulderOD(double theShoulderOD) { - shoulderOD = theShoulderOD; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/ParachuteDTO.java b/src/net/sf/openrocket/file/rocksim/export/ParachuteDTO.java deleted file mode 100644 index eb612227..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/ParachuteDTO.java +++ /dev/null @@ -1,129 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.BaseHandler; -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.Parachute; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "Parachute") -@XmlAccessorType(XmlAccessType.FIELD) -public class ParachuteDTO extends BasePartDTO { - - @XmlElement(name = "Dia") - private double dia = 0d; - @XmlElement(name = "SpillHoleDia") - private double spillHoleDia = 0d; - @XmlElement(name = "ShroudLineCount") - private int ShroudLineCount = 0; - @XmlElement(name = "Thickness") - private double thickness = 0d; - @XmlElement(name = "ShroudLineLen") - private double shroudLineLen = 0d; - @XmlElement(name = "ChuteCount") - private int chuteCount = 1; - @XmlElement(name = "ShroudLineMassPerMM") - private double shroudLineMassPerMM = 0d; - @XmlElement(name = "ShroudLineMaterial") - private String shroudLineMaterial = ""; - @XmlElement(name = "DragCoefficient") - private double dragCoefficient = 0.75d; - - public ParachuteDTO() { - } - - public ParachuteDTO(Parachute ec) { - super(ec); - - setChuteCount(1); - setDia(ec.getDiameter() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setDragCoefficient(ec.getCD()); - setShroudLineCount(ec.getLineCount()); - setShroudLineLen(ec.getLineLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - - String material = ec.getLineMaterial().getName(); - setShroudLineMassPerMM(ec.getLineMaterial().getDensity() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LINE_DENSITY); - - if (material.startsWith(BaseHandler.ROCKSIM_MATERIAL_PREFIX)) { - material = material.substring(BaseHandler.ROCKSIM_MATERIAL_PREFIX.length()); - } - setShroudLineMaterial(material); - } - - public double getDia() { - return dia; - } - - public void setDia(double theDia) { - dia = theDia; - } - - public double getSpillHoleDia() { - return spillHoleDia; - } - - public void setSpillHoleDia(double theSpillHoleDia) { - spillHoleDia = theSpillHoleDia; - } - - public int getShroudLineCount() { - return ShroudLineCount; - } - - public void setShroudLineCount(int theShroudLineCount) { - ShroudLineCount = theShroudLineCount; - } - - public double getThickness() { - return thickness; - } - - public void setThickness(double theThickness) { - thickness = theThickness; - } - - public double getShroudLineLen() { - return shroudLineLen; - } - - public void setShroudLineLen(double theShroudLineLen) { - shroudLineLen = theShroudLineLen; - } - - public int getChuteCount() { - return chuteCount; - } - - public void setChuteCount(int theChuteCount) { - chuteCount = theChuteCount; - } - - public double getShroudLineMassPerMM() { - return shroudLineMassPerMM; - } - - public void setShroudLineMassPerMM(double theShroudLineMassPerMM) { - shroudLineMassPerMM = theShroudLineMassPerMM; - } - - public String getShroudLineMaterial() { - return shroudLineMaterial; - } - - public void setShroudLineMaterial(String theShroudLineMaterial) { - shroudLineMaterial = theShroudLineMaterial; - } - - public double getDragCoefficient() { - return dragCoefficient; - } - - public void setDragCoefficient(double theDragCoefficient) { - dragCoefficient = theDragCoefficient; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/RocketDesignDTO.java b/src/net/sf/openrocket/file/rocksim/export/RocketDesignDTO.java deleted file mode 100644 index 9abc4464..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/RocketDesignDTO.java +++ /dev/null @@ -1,94 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; - -/** - */ -@XmlAccessorType(XmlAccessType.FIELD) -public class RocketDesignDTO { - - @XmlElement(name = "Name") - private String name; - @XmlElement(name = "StageCount") - private int stageCount = 1; - @XmlElement(name = "ViewType") - private int viewType = 0; - @XmlElement(name = "ViewStageCount") - private int viewStageCount = 3; - @XmlElement(name = "ViewTypeEdit") - private int viewTypeEdit = 0; - @XmlElement(name = "ViewStageCountEdit") - private int viewStageCountEdit = 3; - @XmlElement(name = "ZoomFactor") - private double zoomFactor = 0d; - @XmlElement (name = "ZoomFactorEdit") - private double zoomFactorEdit = 0d; - @XmlElement(name = "ScrollPosX") - private int scrollPosX = 0; - @XmlElement(name = "ScrollPosY") - private int scrollPosY = 0; - @XmlElement(name = "ScrollPosXEdit") - private int scrollPosXEdit = 0; - @XmlElement(name = "ScrollPosYEdit") - private int scrollPosYEdit = 0; - @XmlElement(name = "ThreeDFlags") - private int threeDFlags = 0; - @XmlElement(name = "ThreeDFlagsEdit") - private int threeDFlagsEdit = 0; - - @XmlElement(name = "CPCalcFlags") - private String cpCalcFlags = "1"; - @XmlElement(name = "UseKnownMass") - private String useKnownMass = "0"; - @XmlElement(name = "Stage3Parts") - private StageDTO stage3 = new StageDTO(); - @XmlElement(name = "Stage2Parts", required = true, nillable = false) - private StageDTO stage2 = new StageDTO(); - @XmlElement(name = "Stage1Parts", required = false, nillable = false) - private StageDTO stage1 = new StageDTO(); - - public RocketDesignDTO() { - } - - public String getName() { - return name; - } - - public void setName(String theName) { - name = theName; - } - - public int getStageCount() { - return stageCount; - } - - public void setStageCount(int theStageCount) { - stageCount = theStageCount; - } - - public StageDTO getStage3() { - return stage3; - } - - public void setStage3(StageDTO theStage3) { - stage3 = theStage3; - } - - public StageDTO getStage2() { - return stage2; - } - - public void setStage2(StageDTO theStage2) { - stage2 = theStage2; - } - - public StageDTO getStage1() { - return stage1; - } - - public void setStage1(StageDTO theStage1) { - stage1 = theStage1; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/RocksimDesignDTO.java b/src/net/sf/openrocket/file/rocksim/export/RocksimDesignDTO.java deleted file mode 100644 index 56129250..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/RocksimDesignDTO.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; - -/** - */ -@XmlAccessorType(XmlAccessType.FIELD) -public class RocksimDesignDTO { - - @XmlElement(name = "RocketDesign") - private RocketDesignDTO design; - - public RocksimDesignDTO() { - } - - public RocketDesignDTO getDesign() { - return design; - } - - public void setDesign(RocketDesignDTO theDesign) { - design = theDesign; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/RocksimDocumentDTO.java b/src/net/sf/openrocket/file/rocksim/export/RocksimDocumentDTO.java deleted file mode 100644 index 681f4086..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/RocksimDocumentDTO.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -@XmlRootElement(name = "RockSimDocument") -@XmlAccessorType(XmlAccessType.FIELD) -public class RocksimDocumentDTO { - - @XmlElement(name = "FileVersion") - private final String version = "4"; - - @XmlElement(name = "DesignInformation") - private RocksimDesignDTO design; - - public RocksimDocumentDTO() { - } - - public RocksimDesignDTO getDesign() { - return design; - } - - public void setDesign(RocksimDesignDTO theDesign) { - this.design = theDesign; - } - - public String getVersion() { - return version; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java b/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java deleted file mode 100644 index 39500625..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/RocksimSaver.java +++ /dev/null @@ -1,122 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.StorageOptions; -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Stage; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.startup.Application; - -import javax.xml.bind.JAXBContext; -import javax.xml.bind.Marshaller; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.StringWriter; -import java.util.List; - -/** - */ -public class RocksimSaver extends RocketSaver { - - private static final LogHelper log = Application.getLogger(); - - public String marshalToRocksim(OpenRocketDocument doc) { - - try { - JAXBContext binder = JAXBContext.newInstance(RocksimDocumentDTO.class); - Marshaller marshaller = binder.createMarshaller(); - marshaller.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); - StringWriter sw = new StringWriter(); - - marshaller.marshal(toRocksimDocumentDTO(doc), sw); - return sw.toString(); - } catch (Exception e) { - e.printStackTrace(); - } - - return null; - } - - @Override - public void save(OutputStream dest, OpenRocketDocument doc, StorageOptions options) throws IOException { - log.info("Saving .rkt file"); - - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(dest, "UTF-8")); - writer.write(marshalToRocksim(doc)); - writer.flush(); - writer.close(); - } - - @Override - public long estimateFileSize(OpenRocketDocument doc, StorageOptions options) { - return marshalToRocksim(doc).length(); - } - - private RocksimDocumentDTO toRocksimDocumentDTO(OpenRocketDocument doc) { - RocksimDocumentDTO rsd = new RocksimDocumentDTO(); - - rsd.setDesign(toRocksimDesignDTO(doc.getRocket())); - - return rsd; - } - - private RocksimDesignDTO toRocksimDesignDTO(Rocket rocket) { - RocksimDesignDTO result = new RocksimDesignDTO(); - result.setDesign(toRocketDesignDTO(rocket)); - return result; - } - - private RocketDesignDTO toRocketDesignDTO(Rocket rocket) { - RocketDesignDTO result = new RocketDesignDTO(); - result.setName(rocket.getName()); - int stageCount = rocket.getStageCount(); - result.setStageCount(stageCount); - if (stageCount > 0) { - result.setStage3(toStageDTO(rocket.getChild(0).getStage())); - } - if (stageCount > 1) { - result.setStage2(toStageDTO(rocket.getChild(1).getStage())); - } - if (stageCount > 2) { - result.setStage1(toStageDTO(rocket.getChild(2).getStage())); - } - return result; - } - - private StageDTO toStageDTO(Stage stage) { - StageDTO result = new StageDTO(); - - List<RocketComponent> children = stage.getChildren(); - for (int i = 0; i < children.size(); i++) { - RocketComponent rocketComponents = children.get(i); - if (rocketComponents instanceof NoseCone) { - result.addExternalPart(toNoseConeDTO((NoseCone) rocketComponents)); - } else if (rocketComponents instanceof BodyTube) { - result.addExternalPart(toBodyTubeDTO((BodyTube) rocketComponents)); - } else if (rocketComponents instanceof Transition) { - result.addExternalPart(toTransitionDTO((Transition) rocketComponents)); - } - } - return result; - } - - private NoseConeDTO toNoseConeDTO(NoseCone nc) { - return new NoseConeDTO(nc); - } - - private BodyTubeDTO toBodyTubeDTO(BodyTube bt) { - return new BodyTubeDTO(bt); - } - - private TransitionDTO toTransitionDTO(Transition tran) { - return new TransitionDTO(tran); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/StageDTO.java b/src/net/sf/openrocket/file/rocksim/export/StageDTO.java deleted file mode 100644 index 112be2c6..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/StageDTO.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.util.ArrayList; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElementRef; -import javax.xml.bind.annotation.XmlElementRefs; -import java.util.List; - -/** - */ -@XmlAccessorType(XmlAccessType.FIELD) -public class StageDTO { - - @XmlElementRefs({ - @XmlElementRef(name = "BodyTube", type = BodyTubeDTO.class), - @XmlElementRef(name = "NoseCone", type = NoseConeDTO.class), - @XmlElementRef(name = "Transition", type = TransitionDTO.class) - }) - private List<BasePartDTO> externalPart = new ArrayList<BasePartDTO>(); - - public StageDTO() { - } - - public List<BasePartDTO> getExternalPart() { - return externalPart; - } - - public void addExternalPart(BasePartDTO theExternalPartDTO) { - externalPart.add(theExternalPartDTO); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/StreamerDTO.java b/src/net/sf/openrocket/file/rocksim/export/StreamerDTO.java deleted file mode 100644 index cb687666..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/StreamerDTO.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.Streamer; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "Streamer") -@XmlAccessorType(XmlAccessType.FIELD) -public class StreamerDTO extends BasePartDTO { - - @XmlElement(name = "Width") - private double width = 0d; - @XmlElement(name = "DragCoefficient") - private double dragCoefficient = 0.75d; - - public StreamerDTO() { - } - - public StreamerDTO(Streamer ec) { - super(ec); - setWidth(ec.getStripWidth() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setDragCoefficient(ec.getCD()); - } - - public double getWidth() { - return width; - } - - public void setWidth(double theWidth) { - width = theWidth; - } - - public double getDragCoefficient() { - return dragCoefficient; - } - - public void setDragCoefficient(double theDragCoefficient) { - dragCoefficient = theDragCoefficient; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/TransitionDTO.java b/src/net/sf/openrocket/file/rocksim/export/TransitionDTO.java deleted file mode 100644 index 5d668303..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/TransitionDTO.java +++ /dev/null @@ -1,92 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.file.rocksim.importt.RocksimHandler; -import net.sf.openrocket.rocketcomponent.Transition; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlElement; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "Transition") -@XmlAccessorType(XmlAccessType.FIELD) -public class TransitionDTO extends AbstractTransitionDTO { - - - @XmlElement(name = "FrontShoulderLen") - private double frontShoulderLen = 0d; - @XmlElement(name = "RearShoulderLen") - private double rearShoulderLen = 0d; - @XmlElement(name = "FrontShoulderDia") - private double frontShoulderDia = 0d; - @XmlElement(name = "RearShoulderDia") - private double rearShoulderDia = 0d; - @XmlElement(name = "FrontDia") - private double frontDia = 0d; - @XmlElement(name = "RearDia") - private double rearDia = 0d; - - public TransitionDTO() { - } - - public TransitionDTO(Transition tran) { - super(tran); - setFrontDia(tran.getForeRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setRearDia(tran.getAftRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setFrontShoulderDia(tran.getForeShoulderRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setFrontShoulderLen(tran.getForeShoulderLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - setRearShoulderDia(tran.getAftShoulderRadius() * RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - setRearShoulderLen(tran.getAftShoulderLength() * RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - - - } - public double getFrontShoulderLen() { - return frontShoulderLen; - } - - public void setFrontShoulderLen(double theFrontShoulderLen) { - frontShoulderLen = theFrontShoulderLen; - } - - public double getRearShoulderLen() { - return rearShoulderLen; - } - - public void setRearShoulderLen(double theRearShoulderLen) { - rearShoulderLen = theRearShoulderLen; - } - - public double getFrontShoulderDia() { - return frontShoulderDia; - } - - public void setFrontShoulderDia(double theFrontShoulderDia) { - frontShoulderDia = theFrontShoulderDia; - } - - public double getRearShoulderDia() { - return rearShoulderDia; - } - - public void setRearShoulderDia(double theRearShoulderDia) { - rearShoulderDia = theRearShoulderDia; - } - - public double getFrontDia() { - return frontDia; - } - - public void setFrontDia(double theFrontDia) { - frontDia = theFrontDia; - } - - public double getRearDia() { - return rearDia; - } - - public void setRearDia(double theRearDia) { - rearDia = theRearDia; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/export/TubeCouplerDTO.java b/src/net/sf/openrocket/file/rocksim/export/TubeCouplerDTO.java deleted file mode 100644 index 52d995e3..00000000 --- a/src/net/sf/openrocket/file/rocksim/export/TubeCouplerDTO.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.sf.openrocket.file.rocksim.export; - -import net.sf.openrocket.rocketcomponent.TubeCoupler; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlRootElement; - -/** - */ -@XmlRootElement(name = "Ring") -@XmlAccessorType(XmlAccessType.FIELD) -public class TubeCouplerDTO extends CenteringRingDTO { - - public TubeCouplerDTO(TubeCoupler tc) { - super(tc); - setUsageCode(UsageCode.TubeCoupler); - } -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/AttachedPartsHandler.java b/src/net/sf/openrocket/file/rocksim/importt/AttachedPartsHandler.java deleted file mode 100644 index 4543c519..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/AttachedPartsHandler.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * AttachedPartsHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.rocketcomponent.RocketComponent; - -import java.util.HashMap; - -/** - * A SAX handler for the Rocksim AttachedParts XML type. - */ -class AttachedPartsHandler extends ElementHandler { - /** The parent component. */ - private final RocketComponent component; - - /** - * Constructor. - * - * @param c the parent - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public AttachedPartsHandler(RocketComponent c) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of any attached part may not be null."); - } - component = c; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - if ("FinSet".equals(element)) { - return new FinSetHandler(component); - } - if ("CustomFinSet".equals(element)) { - return new FinSetHandler(component); - } - if ("LaunchLug".equals(element)) { - return new LaunchLugHandler(component, warnings); - } - if ("Parachute".equals(element)) { - return new ParachuteHandler(component, warnings); - } - if ("Streamer".equals(element)) { - return new StreamerHandler(component, warnings); - } - if ("MassObject".equals(element)) { - return new MassObjectHandler(component, warnings); - } - if ("Ring".equals(element)) { - return new RingHandler(component, warnings); - } - if ("BodyTube".equals(element)) { - return new InnerBodyTubeHandler(component, warnings); - } - if ("Transition".equals(element)) { - return new TransitionHandler(component, warnings); - } - if ("TubeFinSet".equals(element)) { - warnings.add("Tube fins are not currently supported. Ignoring."); - } - if ("RingTail".equals(element)) { - warnings.add("Ring tails are not currently supported. Ignoring."); - } - if ("ExternalPod".equals(element)) { - warnings.add("Pods are not currently supported. Ignoring."); - } - return null; - } -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/BaseHandler.java b/src/net/sf/openrocket/file/rocksim/importt/BaseHandler.java deleted file mode 100644 index 59fb05a3..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/BaseHandler.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * BaseHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.HashMap; - -/** - * An abstract base class that handles common parsing. All Rocksim component handlers are subclassed from here. - * - * @param <C> the specific RocketComponent subtype for which the concrete handler can create - */ -public abstract class BaseHandler<C extends RocketComponent> extends ElementHandler { - - /** - * Prepend rocksim materials. - */ - public static final String ROCKSIM_MATERIAL_PREFIX = "RS: "; - /** - * The overridden mass. - */ - private Double mass = 0d; - /** - * The overridden Cg. - */ - private Double cg = 0d; - /** - * The density of the material in the component. - */ - private Double density = 0d; - /** - * The internal Rocksim density type. - */ - private RocksimDensityType densityType = RocksimDensityType.ROCKSIM_BULK; - - /** - * The material name. - */ - private String materialName = ""; - - /** - * The SAX method called when the closing element tag is reached. - * - * @param element the element name. - * @param attributes attributes of the element. - * @param content the textual content of the element. - * @param warnings the warning set to store warnings in. - * @throws SAXException - */ - - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - final C component = getComponent(); - try { - if ("Name".equals(element)) { - component.setName(content); - } - if ("KnownMass".equals(element)) { - mass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - } - if ("Density".equals(element)) { - density = Math.max(0d, Double.parseDouble(content) ); - } - if ("KnownCG".equals(element)) { - cg = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("UseKnownCG".equals(element)) { //Rocksim sets UseKnownCG to true to control the override of both cg and mass - boolean override = "1".equals(content); - setOverride(component, override, mass, cg); - } - if ("DensityType".equals(element)) { - densityType = RocksimDensityType.fromCode(Integer.parseInt(content)); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void endHandler(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - /* Because of the order of XML elements in Rocksim, not all information is known at the time it really needs - to be acted upon. So we keep temporary instance variables to be used here at the end of the parsing. - */ - density = computeDensity(densityType, density); - RocketComponent component = getComponent(); - updateComponentMaterial(component, materialName, getMaterialType(), density); - } - - /** - * Compute the density. Rocksim does strange things with densities. For some streamer material it's in cubic, - * rather than square, units. In those cases it needs to be converted to an appropriate SURFACE material density. - * Some G10 fiberglass materials are in cubic units, other G10 fiberglass is in square units. And due to a - * Rocksim bug, some densities are 0 when they clearly should not be. - * - * This may be overridden for specific component density computations. - * - * @param type the rocksim density - * @param rawDensity the density as specified in the Rocksim design file - * @return a value in OpenRocket SURFACE density units - */ - protected double computeDensity(RocksimDensityType type, double rawDensity) { - return rawDensity / type.asOpenRocket(); - } - - /** - * If the Rocksim component does not override the mass, then create a Material based upon the density defined - * for that component. This *should* result in a consistent representation of Cg between Rocksim and OpenRocket. - * - * @param component the component - * @param type the type of the material - * @param density the density in g/cm^3 - * @param definedMaterial the material that is currently defined on the component; used only to get the name - * as it appears in Rocksim - */ - public static void updateComponentMaterial(RocketComponent component, String definedMaterial, Material.Type type, - double density) { - if (definedMaterial != null) { - Material custom = createCustomMaterial(type, definedMaterial, density); - setMaterial(component, custom); - } - } - - /** - * Override the mass and Cg of the component. - * - * @param component the component - * @param override true if any override should happen - * @param mass the override mass - * @param cg the override cg - */ - public static void setOverride(RocketComponent component, boolean override, double mass, double cg) { - if (override) { - component.setCGOverridden(override); - component.setMassOverridden(override); - component.setOverrideSubcomponents(false); //Rocksim does not support this type of override - component.setOverrideMass(mass); - component.setOverrideCGX(cg); - } - } - - /** - * Get the component this handler is working upon. - * - * @return a component - */ - protected abstract C getComponent(); - - /** - * Get the required type of material for this component. - * - * @return the required material type - */ - protected abstract Material.Type getMaterialType(); - - /** - * Some CG positions in Rocksim do not correspond to the CG position reference in OpenRocket. - * - * @param theCG the CG value to really use when overriding CG on the OpenRocket component - */ - protected void setCG(double theCG) { - cg = theCG; - } - - /** - * Set the material name as specified in the Rocksim design file. - * - * @param content the material name - */ - protected void setMaterialName(String content) { - materialName = content; - } - - /** - * Add child to parent only if the child is compatible. Otherwise add to warning set. - * - * @param parent the parent component - * @param child the child component - * @param warnings the warning set - * - * @return true if the child is compatible with parent - */ - protected static boolean isCompatible(RocketComponent parent, Class<? extends RocketComponent> child, WarningSet warnings) { - if (!parent.isCompatible(child)) { - warnings.add(child.getName() + " can not be attached to " - + parent.getComponentName() + ", ignoring component."); - return false; - } - else { - return true; - } - } - - /** - * Create a custom material based on the density. The name of the material is prepended with 'RS: ' to - * indicate it came from a RockSim material. - * - * @param type the type of the material - * @param name the name of the component - * @param density the density - * - * @return a Material instance - */ - public static Material createCustomMaterial(Material.Type type, String name, double density) { - return Material.newMaterial(type, ROCKSIM_MATERIAL_PREFIX + name, density, true); - } - - /** - * Set the material onto an instance of RocketComponent. This is done because only some subtypes of RocketComponent - * have the setMaterial method. Unfortunately the supertype cannot be used. - * - * @param component the component who's material is to be set - * @param material the material to be set on the component (defined by getComponent()) - */ - private static void setMaterial(RocketComponent component, Material material) { - try { - final Method method = getMethod(component, "setMaterial", new Class[]{Material.class}); - if (method != null) { - method.invoke(component, material); - } - } - catch (IllegalAccessException ignored) { - } - catch (InvocationTargetException ignored) { - } - } - - /** - * Find a method by name and argument list. - * - * @param component the component who's material is to be seta - * @param name the method name - * @param args the class types of the parameters - * - * @return the Method instance, or null - */ - private static Method getMethod(RocketComponent component, String name, Class[] args) { - Method method = null; - try { - method = component.getClass().getMethod(name, args); - } - catch (NoSuchMethodException ignored) { - } - return method; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/BodyTubeHandler.java b/src/net/sf/openrocket/file/rocksim/importt/BodyTubeHandler.java deleted file mode 100644 index f2acb4cb..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/BodyTubeHandler.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * BodyTubeHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A SAX handler for Rocksim Body Tubes. - */ -class BodyTubeHandler extends BaseHandler<BodyTube> { - /** - * The OpenRocket BodyTube. - */ - private final BodyTube bodyTube; - - /** - * Constructor. - * - * @param c parent component - * @param warnings the warning set - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public BodyTubeHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of a body tube may not be null."); - } - bodyTube = new BodyTube(); - if (isCompatible(c, BodyTube.class, warnings)) { - c.addChild(bodyTube); - } - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - if ("AttachedParts".equals(element)) { - return new AttachedPartsHandler(bodyTube); - } - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("OD".equals(element)) { - bodyTube.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - } - if ("ID".equals(element)) { - final double r = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS; - bodyTube.setInnerRadius(r); - } - if ("Len".equals(element)) { - bodyTube.setLength(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("FinishCode".equals(element)) { - bodyTube.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); - } - if ("IsMotorMount".equals(element)) { - bodyTube.setMotorMount("1".equals(content)); - } - if ("EngineOverhang".equals(element)) { - bodyTube.setMotorOverhang(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * Get the component this handler is working upon. - * - * @return a component - */ - @Override - public BodyTube getComponent() { - return bodyTube; - } - - /** - * Get the required type of material for this component. - * - * @return BULK - */ - public Material.Type getMaterialType() { - return Material.Type.BULK; - } -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java b/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java deleted file mode 100644 index 1ddb5677..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/FinSetHandler.java +++ /dev/null @@ -1,393 +0,0 @@ -/* - * FinSetHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.util.Coordinate; -import org.xml.sax.SAXException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -/** - * A SAX handler for Rocksim fin sets. Because the type of fin may not be known first (in Rocksim file format, the fin - * shape type is in the middle of the XML structure), and because we're using SAX not DOM, all of the fin - * characteristics are kept here until the closing FinSet tag. At that point, <code>asOpenRocket</code> method is called - * to construct the corresponding OpenRocket FinSet. - */ -class FinSetHandler extends ElementHandler { - /** - * The parent component. - */ - private final RocketComponent component; - - /** - * The name of the fin. - */ - private String name; - /** - * The Rocksim fin shape code. - */ - private int shapeCode; - /** - * The location of the fin on its parent. - */ - private double location = 0.0d; - /** - * The OpenRocket Position which gives the absolute/relative positioning for location. - */ - private RocketComponent.Position position; - /** - * The number of fins in this fin set. - */ - private int finCount; - /** - * The length of the root chord. - */ - private double rootChord = 0.0d; - /** - * The length of the tip chord. - */ - private double tipChord = 0.0d; - /** - * The length of the mid-chord (aka height). - */ - private double midChordLen = 0.0d; - /** - * The distance of the leading edge from root to top. - */ - private double sweepDistance = 0.0d; - /** - * The angle the fins have been rotated from the y-axis, if looking down the tube, in radians. - */ - private double radialAngle = 0.0d; - /** - * The thickness of the fins. - */ - private double thickness; - /** - * The finish of the fins. - */ - private ExternalComponent.Finish finish; - /** - * The shape of the tip. - */ - private int tipShapeCode; - /** - * The length of the TTW tab. - */ - private double tabLength = 0.0d; - /** - * The depth of the TTW tab. - */ - private double tabDepth = 0.0d; - /** - * The offset of the tab, from the front of the fin. - */ - private double taboffset = 0.0d; - /** - * The elliptical semi-span (height). - */ - private double semiSpan; - /** - * The list of custom points. - */ - private String pointList; - /** - * Override the Cg and mass. - */ - private boolean override = false; - /** - * The overridden mass. - */ - private Double mass = 0d; - /** - * The overridden Cg. - */ - private Double cg = 0d; - /** - * The density of the material in the component. - */ - private Double density = 0d; - /** - * The material name. - */ - private String materialName = ""; - /** - * The Rocksim calculated mass. - */ - private Double calcMass = 0d; - /** - * The Rocksim calculated cg. - */ - private Double calcCg = 0d; - - - /** - * Constructor. - * - * @param c the parent - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public FinSetHandler (RocketComponent c) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of a fin set may not be null."); - } - component = c; - } - - @Override - public ElementHandler openElement (String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement (String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - try { - if ("Name".equals(element)) { - name = content; - } - if ("Material".equals(element)) { - materialName = content; - } - if ("FinishCode".equals(element)) { - finish = RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket(); - } - if ("Xb".equals(element)) { - location = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("LocationMode".equals(element)) { - position = RocksimLocationMode.fromCode(Integer.parseInt(content)).asOpenRocket(); - } - if ("FinCount".equals(element)) { - finCount = Integer.parseInt(content); - } - if ("RootChord".equals(element)) { - rootChord = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("TipChord".equals(element)) { - tipChord = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("SemiSpan".equals(element)) { - semiSpan = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("MidChordLen".equals(element)) { - midChordLen = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("SweepDistance".equals(element)) { - sweepDistance = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("Thickness".equals(element)) { - thickness = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("TipShapeCode".equals(element)) { - tipShapeCode = Integer.parseInt(content); - } - if ("TabLength".equals(element)) { - tabLength = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("TabDepth".equals(element)) { - tabDepth = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("TabOffset".equals(element)) { - taboffset = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("RadialAngle".equals(element)) { - radialAngle = Double.parseDouble(content); - } - if ("ShapeCode".equals(element)) { - shapeCode = Integer.parseInt(content); - } - if ("PointList".equals(element)) { - pointList = content; - } - if ("KnownMass".equals(element)) { - mass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - } - if ("Density".equals(element)) { - density = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY); - } - if ("KnownCG".equals(element)) { - cg = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - } - if ("UseKnownCG".equals(element)) { - override = "1".equals(content); - } - if ("CalcMass".equals(element)) { - calcMass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; - } - if ("CalcCg".equals(element)) { - calcCg = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - @Override - public void endHandler (String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - //Create the fin set and correct for overrides and actual material densities - final FinSet finSet = asOpenRocket(warnings); - if (component.isCompatible(finSet)) { - BaseHandler.setOverride(finSet, override, mass, cg); - if (!override && finSet.getCrossSection().equals(FinSet.CrossSection.AIRFOIL)) { - //Override mass anyway. This is done only for AIRFOIL because Rocksim does not compute different - //mass/cg for different cross sections, but OpenRocket does. This can lead to drastic differences - //in mass. To counteract that, the cross section value is retained but the mass/cg is overridden - //with the calculated values from Rocksim. This will best approximate the Rocksim design in OpenRocket. - BaseHandler.setOverride(finSet, true, calcMass, calcCg); - } - BaseHandler.updateComponentMaterial(finSet, materialName, Material.Type.BULK, density); - component.addChild(finSet); - } - else { - warnings.add(finSet.getComponentName() + " can not be attached to " - + component.getComponentName() + ", ignoring component."); - } - } - - - /** - * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's FinSet. - * - * @param warnings the warning set to convey incompatibilities to the user - * - * @return a FinSet instance - */ - public FinSet asOpenRocket (WarningSet warnings) { - FinSet result; - - if (shapeCode == 0) { - //Trapezoidal - result = new TrapezoidFinSet(); - ((TrapezoidFinSet) result).setFinShape(rootChord, tipChord, sweepDistance, semiSpan, thickness); - } - else if (shapeCode == 1) { - //Elliptical - result = new EllipticalFinSet(); - ((EllipticalFinSet) result).setHeight(semiSpan); - ((EllipticalFinSet) result).setLength(rootChord); - } - else if (shapeCode == 2) { - - result = new FreeformFinSet(); - try { - ((FreeformFinSet) result).setPoints(toCoordinates(pointList, warnings)); - } - catch (IllegalFinPointException e) { - warnings.add("Illegal fin point set. " + e.getMessage() + " Ignoring."); - } - } - else { - return null; - } - result.setThickness(thickness); - result.setName(name); - result.setFinCount(finCount); - result.setFinish(finish); - //All TTW tabs in Rocksim are relative to the front of the fin. - result.setTabRelativePosition(FinSet.TabRelativePosition.FRONT); - result.setTabHeight(tabDepth); - result.setTabLength(tabLength); - result.setTabShift(taboffset); - result.setBaseRotation(radialAngle); - result.setCrossSection(convertTipShapeCode(tipShapeCode)); - result.setRelativePosition(position); - PositionDependentHandler.setLocation(result, position, location); - return result; - - } - - /** - * Convert a Rocksim string that represents fin plan points into an array of OpenRocket coordinates. - * - * @param pointList a comma and pipe delimited string of X,Y coordinates from Rocksim. This is of the format: - * <pre>x0,y0|x1,y1|x2,y2|... </pre> - * @param warnings the warning set to convey incompatibilities to the user - * - * @return an array of OpenRocket Coordinates - */ - private Coordinate[] toCoordinates (String pointList, WarningSet warnings) { - List<Coordinate> result = new ArrayList<Coordinate>(); - if (pointList != null && !pointList.isEmpty()) { - String[] points = pointList.split("\\Q|\\E"); - for (String point : points) { - String[] aPoint = point.split(","); - try { - if (aPoint.length > 1) { - Coordinate c = new Coordinate( - Double.parseDouble(aPoint[0]) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH, - Double.parseDouble(aPoint[1]) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - result.add(c); - } - else { - warnings.add("Invalid fin point pair."); - } - } - catch (NumberFormatException nfe) { - warnings.add("Fin point not in numeric format."); - } - } - if (!result.isEmpty()) { - //OpenRocket requires fin plan points be ordered from leading root chord to trailing root chord in the - //Coordinate array. - Coordinate last = result.get(result.size() - 1); - if (last.x == 0 && last.y == 0) { - Collections.reverse(result); - } - } - } - final Coordinate[] coords = new Coordinate[result.size()]; - return result.toArray(coords); - } - - - /** - * Convert a Rocksim tip shape to an OpenRocket CrossSection. - * - * @param tipShape the tip shape code from Rocksim - * - * @return a CrossSection instance - */ - public static FinSet.CrossSection convertTipShapeCode (int tipShape) { - switch (tipShape) { - case 0: - return FinSet.CrossSection.SQUARE; - case 1: - return FinSet.CrossSection.ROUNDED; - case 2: - return FinSet.CrossSection.AIRFOIL; - default: - return FinSet.CrossSection.SQUARE; - } - } - - public static int convertTipShapeCode (FinSet.CrossSection cs) { - if (FinSet.CrossSection.ROUNDED.equals(cs)) { - return 1; - } - if (FinSet.CrossSection.AIRFOIL.equals(cs)) { - return 2; - } - return 0; - } - -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/InnerBodyTubeHandler.java b/src/net/sf/openrocket/file/rocksim/importt/InnerBodyTubeHandler.java deleted file mode 100644 index edcf83f9..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/InnerBodyTubeHandler.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * InnerBodyTubeHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A SAX handler for Rocksim inside tubes. - */ -class InnerBodyTubeHandler extends PositionDependentHandler<InnerTube> { - - /** - * The OpenRocket InnerTube instance. - */ - private final InnerTube bodyTube; - - /** - * Constructor. - * - * @param c the parent component - * @param warnings the warning set - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public InnerBodyTubeHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of an inner tube may not be null."); - } - bodyTube = new InnerTube(); - if (isCompatible(c, InnerTube.class, warnings)) { - c.addChild(bodyTube); - } - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - if ("AttachedParts".equals(element)) { - return new AttachedPartsHandler(bodyTube); - } - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("OD".equals(element)) { - bodyTube.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - } - if ("ID".equals(element)) { - final double r = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS; - bodyTube.setInnerRadius(r); - } - if ("Len".equals(element)) { - bodyTube.setLength(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("IsMotorMount".equals(element)) { - bodyTube.setMotorMount("1".equals(content)); - } - if ("EngineOverhang".equals(element)) { - bodyTube.setMotorOverhang(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * Get the InnerTube component this handler is working upon. - * - * @return an InnerTube component - */ - @Override - public InnerTube getComponent() { - return bodyTube; - } - - /** - * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not - * public in all components. - * - * @param position the OpenRocket position - */ - @Override - public void setRelativePosition(RocketComponent.Position position) { - bodyTube.setRelativePosition(position); - } - - /** - * Get the required type of material for this component. - * - * @return BULK - */ - @Override - public Material.Type getMaterialType() { - return Material.Type.BULK; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/LaunchLugHandler.java b/src/net/sf/openrocket/file/rocksim/importt/LaunchLugHandler.java deleted file mode 100644 index 1a9d51aa..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/LaunchLugHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * LaunchLugHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * The SAX handler for Rocksim Launch Lugs. - */ -class LaunchLugHandler extends PositionDependentHandler<LaunchLug> { - - /** - * The OpenRocket LaunchLug instance. - */ - private final LaunchLug lug; - - /** - * Constructor. - * - * @param c the parent - * @param warnings the warning set - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public LaunchLugHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of a launch lug may not be null."); - } - lug = new LaunchLug(); - if (isCompatible(c, LaunchLug.class, warnings)) { - c.addChild(lug); - } - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("OD".equals(element)) { - lug.setOuterRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("ID".equals(element)) { - lug.setInnerRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("Len".equals(element)) { - lug.setLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - if ("RadialAngle".equals(element)) { - lug.setRadialDirection(Double.parseDouble(content)); - } - if ("FinishCode".equals(element)) { - lug.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * Get the LaunchLug component this handler is working upon. - * - * @return a LaunchLug component - */ - @Override - public LaunchLug getComponent() { - return lug; - } - - /** - * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not - * public in all components. - * - * @param position the OpenRocket position - */ - @Override - public void setRelativePosition(RocketComponent.Position position) { - lug.setRelativePosition(position); - } - - /** - * Get the required type of material for this component. - * - * @return BULK - */ - @Override - public Material.Type getMaterialType() { - return Material.Type.BULK; - } -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/MassObjectHandler.java b/src/net/sf/openrocket/file/rocksim/importt/MassObjectHandler.java deleted file mode 100644 index 53a33d8b..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/MassObjectHandler.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * MassObjectHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A SAX handler for Rocksim's MassObject XML type. - */ -class MassObjectHandler extends PositionDependentHandler<MassComponent> { - - /** - * The Rocksim Mass length fudge factor. Rocksim completely exaggerates the length of a mass object to the point - * that it looks ridiculous in OpenRocket. This fudge factor is here merely to get the typical mass object to - * render in the OpenRocket UI with it's bounds mostly inside it's parent. The odd thing about it is that - * Rocksim does not expose the length of a mass object in the UI and actually treats mass objects as point objects - - * not 3 or even 2 dimensional. - */ - public static final int MASS_LEN_FUDGE_FACTOR = 100; - - /** - * The OpenRocket MassComponent - counterpart to the RS MassObject. - */ - private final MassComponent mass; - - /** - * Constructor. - *l - * @param c the parent component - * @param warnings the warning set - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public MassObjectHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of a mass component may not be null."); - } - mass = new MassComponent(); - if (isCompatible(c, MassComponent.class, warnings)) { - c.addChild(mass); - } - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - try { - if ("Len".equals(element)) { - mass.setLength(Double.parseDouble(content) / (RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH * MASS_LEN_FUDGE_FACTOR)); - } - if ("KnownMass".equals(element)) { - mass.setComponentMass(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - } - if ("KnownCG".equals(element)) { - //Setting the CG of the Mass Object to 0 is important because of the different ways that Rocksim and - //OpenRocket treat mass objects. Rocksim treats them as points (even though the data file contains a - //length) and because Rocksim sets the CG of the mass object to really be relative to the front of - //the parent. But that value is already assumed in the position and position value for the component. - //Thus it needs to be set to 0 to say that the mass object's CG is at the point of the mass object. - super.setCG(0); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * Get the component this handler is working upon. - * - * @return a component - */ - @Override - public MassComponent getComponent() { - return mass; - } - - /** - * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not - * public in all components. - * - * @param position the OpenRocket position - */ - public void setRelativePosition(RocketComponent.Position position) { - mass.setRelativePosition(position); - } - - /** - * Get the required type of material for this component. Does not apply to MassComponents. - * - * @return BULK - */ - @Override - public Material.Type getMaterialType() { - return Material.Type.BULK; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/NoseConeHandler.java b/src/net/sf/openrocket/file/rocksim/importt/NoseConeHandler.java deleted file mode 100644 index 7021331d..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/NoseConeHandler.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * NoseConeHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * The SAX nose cone handler for Rocksim NoseCones. - */ -class NoseConeHandler extends BaseHandler<NoseCone> { - - /** - * The OpenRocket NoseCone. - */ - private final NoseCone noseCone = new NoseCone(); - - /** - * The wall thickness. Used for hollow nose cones. - */ - private double thickness = 0d; - - /** - * Constructor. - * - * @param c the parent component to the nosecone - * @param warnings the warning set - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public NoseConeHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent component of a nose cone may not be null."); - } - if (isCompatible(c, NoseCone.class, warnings)) { - c.addChild(noseCone); - noseCone.setAftRadiusAutomatic(false); - } - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - //Nose cones in Rocksim may have attached parts - namely Mass Objects - as children. - if ("AttachedParts".equals(element)) { - return new AttachedPartsHandler(noseCone); - } - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("ShapeCode".equals(element)) { - noseCone.setType(RocksimNoseConeCode.fromCode(Integer.parseInt(content)).asOpenRocket()); - } - if ("Len".equals(element)) { - noseCone.setLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("BaseDia".equals(element)) { - noseCone.setAftRadius(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("WallThickness".equals(element)) { - thickness = Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("ShoulderOD".equals(element)) { - noseCone.setAftShoulderRadius(Math.max(0, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("ShoulderLen".equals(element)) { - noseCone.setAftShoulderLength(Math.max(0, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("ShapeParameter".equals(element)) { - //The Rocksim ShapeParameter only applies to certain shapes, although it is included - //in the design file for all nose cones. Applying it when it should not be causes oddities so - //a check is made for the allowable shapes. - if (Transition.Shape.POWER.equals(noseCone.getType()) || - Transition.Shape.HAACK.equals(noseCone.getType()) || - Transition.Shape.PARABOLIC.equals(noseCone.getType())) { - noseCone.setShapeParameter(Double.parseDouble(content)); - } - } - if ("ConstructionType".equals(element)) { - int typeCode = Integer.parseInt(content); - if (typeCode == 0) { - //SOLID - noseCone.setFilled(true); - } - else if (typeCode == 1) { - //HOLLOW - noseCone.setFilled(false); - } - } - if ("FinishCode".equals(element)) { - noseCone.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - @Override - public void endHandler(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.endHandler(element, attributes, content, warnings); - - if (noseCone.isFilled()) { - noseCone.setAftShoulderThickness(noseCone.getAftShoulderRadius()); - } - else { - noseCone.setThickness(thickness); - noseCone.setAftShoulderThickness(thickness); - } - } - - /** - * Get the nose cone component this handler is working upon. - * - * @return a nose cone component - */ - @Override - public NoseCone getComponent() { - return noseCone; - } - - /** - * Get the required type of material for this component. - * - * @return BULK - */ - public Material.Type getMaterialType() { - return Material.Type.BULK; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/ParachuteHandler.java b/src/net/sf/openrocket/file/rocksim/importt/ParachuteHandler.java deleted file mode 100644 index 3ff7555e..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/ParachuteHandler.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * ParachuteHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A SAX handler for Rocksim's Parachute XML type. - */ -class ParachuteHandler extends RecoveryDeviceHandler<Parachute> { - /** - * The OpenRocket Parachute instance - */ - private final Parachute chute; - /** - * The shroud line density. - */ - private double shroudLineDensity = 0.0d; - - /** - * Constructor. - * - * @param c the parent component - * @param warnings the warning set - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public ParachuteHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent of a parachute may not be null."); - } - chute = new Parachute(); - if (isCompatible(c, Parachute.class, warnings)) { - c.addChild(chute); - } - } - - /** - * {@inheritDoc} - */ - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - /** - * {@inheritDoc} - */ - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - try { - if ("Dia".equals(element)) { - chute.setDiameter(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - /* Rocksim doesn't have a packed parachute radius, so we approximate it. */ - double packed; - RocketComponent parent = chute.getParent(); - if (parent instanceof BodyTube) { - packed = ((BodyTube) parent).getOuterRadius() * 0.9; - } - else if (parent instanceof InnerTube) { - packed = ((InnerTube) parent).getInnerRadius() * 0.9; - } - else { - packed = chute.getDiameter() * 0.025; - } - chute.setRadius(packed); - } - if ("ShroudLineCount".equals(element)) { - chute.setLineCount(Math.max(0, Integer.parseInt(content))); - } - if ("ShroudLineLen".equals(element)) { - chute.setLineLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("SpillHoleDia".equals(element)) { - //Not supported in OpenRocket - double spillHoleRadius = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS; - warnings.add("Parachute spill holes are not supported. Ignoring."); - } - if ("ShroudLineMassPerMM".equals(element)) { - shroudLineDensity = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LINE_DENSITY; - } - if ("ShroudLineMaterial".equals(element)) { - chute.setLineMaterial(createCustomMaterial(Material.Type.LINE, content, shroudLineDensity)); - } - if ("DragCoefficient".equals(element)) { - chute.setCD(Double.parseDouble(content)); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * Get the component this handler is working upon. - * - * @return a component - */ - public Parachute getComponent() { - return chute; - } - -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/PositionDependentHandler.java b/src/net/sf/openrocket/file/rocksim/importt/PositionDependentHandler.java deleted file mode 100644 index 6639b050..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/PositionDependentHandler.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * PositionDependentHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * An abstract base class that handles position dependencies for all lower level components that - * are position aware. - * - * @param <C> the specific position dependent RocketComponent subtype for which the concrete handler can create - */ -public abstract class PositionDependentHandler<C extends RocketComponent> extends BaseHandler<C> { - - /** Temporary position value. */ - private Double positionValue = 0d; - - /** Temporary position. */ - private RocketComponent.Position position = RocketComponent.Position.TOP; - - /** - * {@inheritDoc} - */ - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - if ("Xb".equals(element)) { - positionValue = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("LocationMode".equals(element)) { - position = RocksimLocationMode.fromCode(Integer.parseInt( - content)).asOpenRocket(); - } - } - - /** - * This method sets the position information onto the component. Rocksim splits the location/position - * information into two disparate data elements. Both pieces of data are necessary to map into OpenRocket's - * position model. - * - * @param element the element name - * @param attributes the attributes - * @param content the content of the element - * @param warnings the warning set to store warnings in. - * @throws org.xml.sax.SAXException not thrown - */ - @Override - public void endHandler(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - super.endHandler(element, attributes, content, warnings); - setRelativePosition(position); - setLocation(getComponent(), position, positionValue); - } - - /** - * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not - * public in all components. - * - * @param position the OpenRocket position - */ - protected abstract void setRelativePosition(RocketComponent.Position position); - - /** - * Set the position of a component. - * - * @param component the component - * @param position the relative position - * @param location the actual position value - */ - public static void setLocation(RocketComponent component, RocketComponent.Position position, double location) { - if (position.equals(RocketComponent.Position.BOTTOM)) { - component.setPositionValue(-1d * location); - } - else { - component.setPositionValue(location); - } - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/RecoveryDeviceHandler.java b/src/net/sf/openrocket/file/rocksim/importt/RecoveryDeviceHandler.java deleted file mode 100644 index 72e356db..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RecoveryDeviceHandler.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * RecoveryDeviceHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A handler specific to streamers and parachutes. This is done because Rocksim allows any type of material to be - * used as a recovery device, which causes oddities with respect to densities. Density computation is overridden - * here to try to correctly compute a material's density in OpenRocket units. - * - * @param <C> either a Streamer or Parachute - */ -public abstract class RecoveryDeviceHandler<C extends RecoveryDevice> extends PositionDependentHandler<C> { - - /** - * The thickness. Not used by every component, and some component handlers may parse it for their own purposes. - */ - private double thickness = 0d; - /** - * The Rocksim calculated mass. Used only when not overridden and when Rocksim says density == 0 (Rocksim bug). - */ - private Double calcMass = 0d; - - /** - * {@inheritDoc} - */ - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("Thickness".equals(element)) { - thickness = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("CalcMass".equals(element)) { - calcMass = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - - /** - * Compute the density. Rocksim does strange things with densities. For some streamer material it's in cubic, - * rather than square, units. In those cases it needs to be converted to an appropriate SURFACE material density. - * - * @param type the rocksim density - * @param rawDensity the density as specified in the Rocksim design file - * @return a value in OpenRocket SURFACE density units - */ - protected double computeDensity(RocksimDensityType type, double rawDensity) { - - double result; - - if (rawDensity > 0d) { - //ROCKSIM_SURFACE is a square area density; compute normally - //ROCKSIM_LINE is a single length dimension (kg/m) but Rocksim ignores thickness for this type and treats - //it like a SURFACE. - if (RocksimDensityType.ROCKSIM_SURFACE.equals(type) || RocksimDensityType.ROCKSIM_LINE.equals(type)) { - result = rawDensity / RocksimDensityType.ROCKSIM_SURFACE.asOpenRocket(); - } - //ROCKSIM_BULK is a cubic area density; multiple by thickness to make per square area; the result, when - //multiplied by the area will then equal Rocksim's computed mass. - else { - result = (rawDensity / type.asOpenRocket()) * thickness; - } - } - else { - result = calcMass / getComponent().getArea(); - //A Rocksim bug on streamers/parachutes results in a 0 density at times. When that is detected, try - //to compute an approximate density from Rocksim's computed mass. - if (RocksimDensityType.ROCKSIM_BULK.equals(type)) { - //ROCKSIM_BULK is a cubic area density; multiple by thickness to make per square area - result *= thickness; - } - } - return result; - } - - /** - * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not - * public in all components. - * - * @param position the OpenRocket position - */ - @Override - public void setRelativePosition(RocketComponent.Position position) { - getComponent().setRelativePosition(position); - } - - /** - * Get the required type of material for this component. This is the OpenRocket type, which does NOT always - * correspond to Rocksim. Some streamer material is defined as BULK in the Rocksim file. In those cases - * it is adjusted in this handler. - * - * @return SURFACE - */ - @Override - public Material.Type getMaterialType() { - return Material.Type.SURFACE; - } - -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/RingHandler.java b/src/net/sf/openrocket/file/rocksim/importt/RingHandler.java deleted file mode 100644 index 20f6aac0..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RingHandler.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * RingHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.RingComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.TubeCoupler; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A SAX handler for centering rings, tube couplers, and bulkheads. - */ -class RingHandler extends PositionDependentHandler<CenteringRing> { - - /** - * The OpenRocket Ring. - */ - private final CenteringRing ring = new CenteringRing(); - - /** - * The parent component. - */ - private final RocketComponent parent; - - /** - * The parsed Rocksim UsageCode. - */ - private int usageCode = 0; - - /** - * Constructor. - * - * @param theParent the parent component - * @param warnings the warning set - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public RingHandler(RocketComponent theParent, WarningSet warnings) throws IllegalArgumentException { - if (theParent == null) { - throw new IllegalArgumentException("The parent of a ring may not be null."); - } - parent = theParent; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("OD".equals(element)) { - ring.setOuterRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - } - if ("ID".equals(element)) { - ring.setInnerRadius(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS); - } - if ("Len".equals(element)) { - ring.setLength(Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - if ("UsageCode".equals(element)) { - usageCode = Integer.parseInt(content); - } - } catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * Get the ring component this handler is working upon. - * - * @return a component - */ - @Override - public CenteringRing getComponent() { - return ring; - } - - /** - * This method adds the CenteringRing as a child of the parent rocket component. - * - * @param warnings the warning set - */ - public void asCenteringRing(WarningSet warnings) { - - if (isCompatible(parent, CenteringRing.class, warnings)) { - parent.addChild(ring); - } - } - - /** - * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's Bulkhead. - * <p/> - * Side Effect Warning: This method adds the resulting Bulkhead as a child of the parent rocket component! - * - * @param warnings the warning set - */ - public void asBulkhead(WarningSet warnings) { - - Bulkhead result = new Bulkhead(); - - copyValues(result); - - if (isCompatible(parent, Bulkhead.class, warnings)) { - parent.addChild(result); - } - } - - /** - * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's TubeCoupler. - * <p/> - * Side Effect Warning: This method adds the resulting TubeCoupler as a child of the parent rocket component! - * - * @param warnings the warning set - */ - public void asTubeCoupler(WarningSet warnings) { - - TubeCoupler result = new TubeCoupler(); - - copyValues(result); - - if (isCompatible(parent, TubeCoupler.class, warnings)) { - parent.addChild(result); - } - } - - /** - * Convert the parsed Rocksim data values in this object to an instance of OpenRocket's Engine Block. - * <p/> - * Side Effect Warning: This method adds the resulting EngineBlock as a child of the parent rocket component! - * - * @param warnings the warning set - */ - public void asEngineBlock(WarningSet warnings) { - - EngineBlock result = new EngineBlock(); - - copyValues(result); - - if (isCompatible(parent, EngineBlock.class, warnings)) { - parent.addChild(result); - } - } - - /** - * Copy values from the base ring to the specific component. - * - * @param result the target to which ring values will be copied - */ - private void copyValues(RingComponent result) { - result.setOuterRadius(ring.getOuterRadius()); - result.setInnerRadius(ring.getInnerRadius()); - result.setLength(ring.getLength()); - result.setName(ring.getName()); - setOverride(result, ring.isOverrideSubcomponentsEnabled(), ring.getOverrideMass(), ring.getOverrideCGX()); - result.setRelativePosition(ring.getRelativePosition()); - result.setPositionValue(ring.getPositionValue()); - result.setMaterial(ring.getMaterial()); - } - - /** - * Set the relative position onto the component. This cannot be done directly because setRelativePosition is not - * public in all components. - * - * @param position the OpenRocket position - */ - @Override - public void setRelativePosition(RocketComponent.Position position) { - ring.setRelativePosition(position); - } - - @Override - public void endHandler(String element, HashMap<String, String> attributes, String content, WarningSet warnings) throws SAXException { - super.endHandler(element, attributes, content, warnings); - - // The <Ring> XML element in Rocksim design file is used for many types of components, unfortunately. - // Additional subelements are used to indicate the type of the rocket component. When parsing using SAX - // this poses a problem because we can't "look ahead" to see what type is being represented at the start - // of parsing - something that would be nice to do so that we can instantiate the correct OR component - // at the start, then just call setters for the appropriate data. - - // To overcome that, a CenteringRing is instantiated at the start of parsing, it's mutators are called, - // and then at the end (this method) converts the CenteringRing to a more appropriate type. CenteringRing - // is generic enough to support the representation of all similar types without loss of data. - - //UsageCode - // 0 == Centering Ring - // 1 == Bulkhead - // 2 == Engine Block - // 3 == Sleeve - // 4 == Tube Coupler - - if (usageCode == 1) { - //Bulkhead - asBulkhead(warnings); - } else if (usageCode == 2) { - asEngineBlock(warnings); - } else if (usageCode == 4) { - //TubeCoupler - asTubeCoupler(warnings); - } else { - //Default - asCenteringRing(warnings); - } - } - - /** - * Get the required type of material for this component. - * - * @return BULK - */ - @Override - public Material.Type getMaterialType() { - return Material.Type.BULK; - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/file/rocksim/importt/RocksimDensityType.java b/src/net/sf/openrocket/file/rocksim/importt/RocksimDensityType.java deleted file mode 100644 index 9e70fa86..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RocksimDensityType.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * RocksimDensityType.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.material.Material; - -/** - * Models the nose cone shape of a rocket. Maps from Rocksim's notion to OpenRocket's. - */ -public enum RocksimDensityType { - ROCKSIM_BULK (0, RocksimHandler.ROCKSIM_TO_OPENROCKET_BULK_DENSITY), - ROCKSIM_SURFACE(1, RocksimHandler.ROCKSIM_TO_OPENROCKET_SURFACE_DENSITY), - ROCKSIM_LINE (2, RocksimHandler.ROCKSIM_TO_OPENROCKET_LINE_DENSITY); - - /** The Rocksim enumeration value. Sent in XML. */ - private final int ordinal; - - /** The corresponding OpenRocket shape. */ - private final double conversion; - - /** - * Constructor. - * - * @param idx the Rocksim shape code - * @param theConversion the numerical conversion ratio to OpenRocket - */ - private RocksimDensityType(int idx, double theConversion) { - ordinal = idx; - conversion = theConversion; - } - - /** - * Get the OpenRocket shape that corresponds to the Rocksim value. - * - * @return a conversion - */ - public double asOpenRocket() { - return conversion; - } - - /** - * Lookup an instance of this enum based upon the Rocksim code. - * - * @param rocksimDensityType the Rocksim code (from XML) - * @return an instance of this enum - */ - public static RocksimDensityType fromCode(int rocksimDensityType) { - RocksimDensityType[] values = values(); - for (RocksimDensityType value : values) { - if (value.ordinal == rocksimDensityType) { - return value; - } - } - return ROCKSIM_BULK; //Default - } - - /** - * Get the ordinal code. - * - * @param type the OR type - * - * @return the Rocksim XML value - */ - public static int toCode(Material.Type type) { - if (type.equals(Material.Type.BULK)) { - return ROCKSIM_BULK.ordinal; - } - if (type.equals(Material.Type.LINE)) { - return ROCKSIM_LINE.ordinal; - } - if (type.equals(Material.Type.SURFACE)) { - return ROCKSIM_SURFACE.ordinal; - } - return ROCKSIM_BULK.ordinal; - } -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/RocksimFinishCode.java b/src/net/sf/openrocket/file/rocksim/importt/RocksimFinishCode.java deleted file mode 100644 index 658db56a..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RocksimFinishCode.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * RocksimFinishCode.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.rocketcomponent.ExternalComponent; - -/** - * Models the finish of a component. - */ -public enum RocksimFinishCode { - POLISHED(0, ExternalComponent.Finish.POLISHED), - GLOSS(1, ExternalComponent.Finish.SMOOTH), - MATT(2, ExternalComponent.Finish.NORMAL), - UNFINISHED(3, ExternalComponent.Finish.UNFINISHED); - - /** The Rocksim code (from XML). */ - private final int ordinal; - - /** The corresponding OpenRocket finish. */ - private final ExternalComponent.Finish finish; - - /** - * Constructor. - * - * @param idx the Rocksim enum value - * @param theFinish the OpenRocket finish - */ - private RocksimFinishCode(int idx, ExternalComponent.Finish theFinish) { - ordinal = idx; - finish = theFinish; - } - - /** - * Get the OpenRocket finish. - * - * @return a Finish instance - */ - public ExternalComponent.Finish asOpenRocket() { - return finish; - } - - /** - * Lookup an instance of this enum from a Rocksim value. - * - * @param rocksimFinishCode the Rocksim value - * - * @return an instance of this enum; Defaults to MATT - */ - public static RocksimFinishCode fromCode(int rocksimFinishCode) { - RocksimFinishCode[] values = values(); - for (RocksimFinishCode value : values) { - if (value.ordinal == rocksimFinishCode) { - return value; - } - } - return MATT; //Default - } - - /** - * Get the ordinal code. - * - * @param type the OR type - * - * @return the Rocksim XML value - */ - public static int toCode(ExternalComponent.Finish type) { - if (type.equals(ExternalComponent.Finish.UNFINISHED)) { - return UNFINISHED.ordinal; - } - if (type.equals(ExternalComponent.Finish.POLISHED)) { - return POLISHED.ordinal; - } - if (type.equals(ExternalComponent.Finish.SMOOTH)) { - return GLOSS.ordinal; - } - return MATT.ordinal; - } - -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/RocksimHandler.java b/src/net/sf/openrocket/file/rocksim/importt/RocksimHandler.java deleted file mode 100644 index 7d2d527a..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RocksimHandler.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * RocksimHandler.java - * - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Stage; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * This class is a Sax element handler for Rocksim version 9 design files. It parses the Rocksim file (typically - * a .rkt extension) and creates corresponding OpenRocket components. This is a best effort approach and may not - * be an exact replica. - * <p/> - * Limitations: Rocksim flight simulations are not imported; tube fins are not supported; Rocksim 'pods' are not supported. - */ -public class RocksimHandler extends ElementHandler { - - /** - * Length conversion. Rocksim is in millimeters, OpenRocket in meters. - */ - public static final int ROCKSIM_TO_OPENROCKET_LENGTH = 1000; - - /** - * Mass conversion. Rocksim is in grams, OpenRocket in kilograms. - */ - public static final int ROCKSIM_TO_OPENROCKET_MASS = 1000; - - /** - * Bulk Density conversion. Rocksim is in kilograms/cubic meter, OpenRocket in kilograms/cubic meter. - */ - public static final int ROCKSIM_TO_OPENROCKET_BULK_DENSITY = 1; - - /** - * Surface Density conversion. Rocksim is in grams/sq centimeter, OpenRocket in kilograms/sq meter. 1000/(100*100) = 1/10 - */ - public static final double ROCKSIM_TO_OPENROCKET_SURFACE_DENSITY = 1/10d; - - /** - * Line Density conversion. Rocksim is in kilograms/meter, OpenRocket in kilograms/meter. - */ - public static final int ROCKSIM_TO_OPENROCKET_LINE_DENSITY = 1; - - /** - * Radius conversion. Rocksim is always in diameters, OpenRocket mostly in radius. - */ - public static final int ROCKSIM_TO_OPENROCKET_RADIUS = 2 * ROCKSIM_TO_OPENROCKET_LENGTH; - - /** - * The main content handler. - */ - private RocksimContentHandler handler = null; - - /** - * Return the OpenRocketDocument read from the file, or <code>null</code> if a document - * has not been read yet. - * - * @return the document read, or null. - */ - public OpenRocketDocument getDocument() { - return handler.getDocument(); - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - - // Check for unknown elements - if (!element.equals("RockSimDocument")) { - warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); - return null; - } - - // Check for first call - if (handler != null) { - warnings.add(Warning.fromString("Multiple document elements found, ignoring later " - + "ones.")); - return null; - } - - handler = new RocksimContentHandler(); - return handler; - } - -} - -/** - * Handles the content of the <DesignInformation> tag. - */ -class RocksimContentHandler extends ElementHandler { - /** - * The OpenRocketDocument that is the container for the rocket. - */ - private final OpenRocketDocument doc; - - /** - * The top-level component, from which all child components are added. - */ - private final Rocket rocket; - - /** - * The rocksim file version. - */ - private String version; - - /** - * Constructor. - */ - public RocksimContentHandler() { - this.rocket = new Rocket(); - this.doc = new OpenRocketDocument(rocket); - } - - /** - * Get the OpenRocket document that has been created from parsing the Rocksim design file. - * - * @return the instantiated OpenRocketDocument - */ - public OpenRocketDocument getDocument() { - return doc; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - if ("DesignInformation".equals(element)) { - //The next sub-element is "RocketDesign", which is really the only thing that matters. Rather than - //create another handler just for that element, handle it here. - return this; - } - if ("FileVersion".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("RocketDesign".equals(element)) { - return new RocketDesignHandler(rocket); - } - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - /** - * SAX handler for Rocksim file version number. The value is not used currently, but could be used in the future - * for backward/forward compatibility reasons (different lower level handlers could be called via a strategy pattern). - */ - if ("FileVersion".equals(element)) { - version = content; - } - } - - /** - * Answer the file version. - * - * @return the version of the Rocksim design file - */ - public String getVersion() { - return version; - } -} - - -/** - * A SAX handler for the high level Rocksim design. This structure includes sub-structures for each of the stages. - * Correct functioning of this handler is predicated on the stage count element appearing before the actual stage parts - * structures. If that invariant is not true, then behavior will be unpredictable. - */ -class RocketDesignHandler extends ElementHandler { - /** - * The parent component. - */ - private final RocketComponent component; - /** - * The parsed stage count. Defaults to 1. - */ - private int stageCount = 1; - /** - * The overridden stage 1 mass. - */ - private double stage1Mass = 0d; - /** - * The overridden stage 2 mass. - */ - private double stage2Mass = 0d; - /** - * The overridden stage 3 mass. - */ - private double stage3Mass = 0d; - /** - * The overridden stage 1 Cg. - */ - private double stage1CG = 0d; - /** - * The overridden stage 2 Cg. - */ - private double stage2CG = 0d; - /** - * The overridden stage 3 Cg. - */ - private double stage3CG = 0d; - - /** - * Constructor. - * - * @param c the parent component - */ - public RocketDesignHandler(RocketComponent c) { - component = c; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - /** - * In Rocksim stages are from the top down, so a single stage rocket is actually stage '3'. A 2-stage - * rocket defines stage '2' as the initial booster with stage '3' sitting atop it. And so on. - */ - if ("Stage3Parts".equals(element)) { - final Stage stage = new Stage(); - if (stage3Mass > 0.0d) { - stage.setMassOverridden(true); - stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override - stage.setOverrideMass(stage3Mass); - } - if (stage3CG > 0.0d) { - stage.setCGOverridden(true); - stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override - stage.setOverrideCGX(stage3CG); - } - component.addChild(stage); - return new StageHandler(stage); - } - if ("Stage2Parts".equals(element)) { - if (stageCount >= 2) { - final Stage stage = new Stage(); - if (stage2Mass > 0.0d) { - stage.setMassOverridden(true); - stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override - stage.setOverrideMass(stage2Mass); - } - if (stage2CG > 0.0d) { - stage.setCGOverridden(true); - stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override - stage.setOverrideCGX(stage2CG); - } - component.addChild(stage); - return new StageHandler(stage); - } - } - if ("Stage1Parts".equals(element)) { - if (stageCount == 3) { - final Stage stage = new Stage(); - if (stage1Mass > 0.0d) { - stage.setMassOverridden(true); - stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override - stage.setOverrideMass(stage1Mass); - } - if (stage1CG > 0.0d) { - stage.setCGOverridden(true); - stage.setOverrideSubcomponents(true); //Rocksim does not support this type of override - stage.setOverrideCGX(stage1CG); - } - component.addChild(stage); - return new StageHandler(stage); - } - } - if ("Name".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("StageCount".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("Stage3Mass".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("Stage2Mass".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("Stage1Mass".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("Stage3CG".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("Stage2CGAlone".equals(element)) { - return PlainTextHandler.INSTANCE; - } - if ("Stage1CGAlone".equals(element)) { - return PlainTextHandler.INSTANCE; - } - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - try { - if ("Name".equals(element)) { - component.setName(content); - } - if ("StageCount".equals(element)) { - stageCount = Integer.parseInt(content); - } - if ("Stage3Mass".equals(element)) { - stage3Mass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; - } - if ("Stage2Mass".equals(element)) { - stage2Mass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; - } - if ("Stage1Mass".equals(element)) { - stage1Mass = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_MASS; - } - if ("Stage3CG".equals(element)) { - stage3CG = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("Stage2CGAlone".equals(element)) { - stage2CG = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - if ("Stage1CGAlone".equals(element)) { - stage1CG = Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH; - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - -} - -/** - * A SAX handler for a Rocksim stage. - */ -class StageHandler extends ElementHandler { - /** - * The parent OpenRocket component. - */ - private final RocketComponent component; - - /** - * Constructor. - * - * @param c the parent component - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public StageHandler(RocketComponent c) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The stage component may not be null."); - } - component = c; - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - if ("NoseCone".equals(element)) { - return new NoseConeHandler(component, warnings); - } - if ("BodyTube".equals(element)) { - return new BodyTubeHandler(component, warnings); - } - if ("Transition".equals(element)) { - return new TransitionHandler(component, warnings); - } - return null; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/RocksimLoader.java b/src/net/sf/openrocket/file/rocksim/importt/RocksimLoader.java deleted file mode 100644 index a58a8553..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RocksimLoader.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * RocksimLoader.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.file.RocketLoadException; -import net.sf.openrocket.file.RocketLoader; -import net.sf.openrocket.file.simplesax.SimpleSAX; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import java.io.IOException; -import java.io.InputStream; - -/** - * This class is the main entry point for Rocksim design file imported to OpenRocket. Currently only Rocksim v9 - * file formats are supported, although it is possible that v8 formats will work for most components. - * - * In the cases of v9 components that exist in Rocksim but have no corollary in OpenRocket a message is added to - * a warning set and presented to the user. In effect, this loading is a 'best-effort' mapping and is not meant to - * be an exact representation of any possible Rocksim design in an OpenRocket format. - * - * Rocksim simulations are not imported. - * - * Wish List: - * Material interface (or at least make them abstract in RocketComponent) - * setMaterial - * getMaterial - */ -public class RocksimLoader extends RocketLoader { - /** - * This method is called by the default implementations of {@link #load(java.io.File)} - * and {@link #load(java.io.InputStream)} to load the rocket. - * - * @throws net.sf.openrocket.file.RocketLoadException - * if an error occurs during loading. - */ - @Override - protected OpenRocketDocument loadFromStream(InputStream source) throws IOException, RocketLoadException { - - InputSource xmlSource = new InputSource(source); - - RocksimHandler handler = new RocksimHandler(); - - try { - SimpleSAX.readXML(xmlSource, handler, warnings); - } catch (SAXException e) { - throw new RocketLoadException("Malformed XML in input.", e); - } - - final OpenRocketDocument document = handler.getDocument(); - document.setFile(null); - document.clearUndo(); - return document; - } -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/RocksimLocationMode.java b/src/net/sf/openrocket/file/rocksim/importt/RocksimLocationMode.java deleted file mode 100644 index 02daeb83..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RocksimLocationMode.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * RocksimLocationMode.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.rocketcomponent.RocketComponent; - -/** - * Models the relative position of parts on a rocket. Maps from Rocksim's notion to OpenRocket's. - */ -public enum RocksimLocationMode { - FRONT_OF_OWNING_PART (0, RocketComponent.Position.TOP), - FROM_TIP_OF_NOSE (1, RocketComponent.Position.ABSOLUTE), - BACK_OF_OWNING_PART (2, RocketComponent.Position.BOTTOM); - - /** The value Rocksim uses internally (and in the XML file). */ - private final int ordinal; - - /** The OpenRocket position equivalent. */ - private final RocketComponent.Position position; - - /** - * Constructor. - * - * @param idx the rocksim enum value - * @param theOpenRocketPosition the corresponding OpenRocket position - */ - RocksimLocationMode(int idx, RocketComponent.Position theOpenRocketPosition) { - ordinal = idx; - position = theOpenRocketPosition; - } - - /** - * Get the OpenRocket position. - * - * @return the position instance - */ - public RocketComponent.Position asOpenRocket() { - return position; - } - - /** - * Lookup an instance of this class from a rocksim enum value. - * - * @param rocksimCode the rocksim enum value - * - * @return an instance of this enum - */ - public static RocksimLocationMode fromCode(int rocksimCode) { - RocksimLocationMode[] values = values(); - for (RocksimLocationMode value : values) { - if (value.ordinal == rocksimCode) { - return value; - } - } - return FRONT_OF_OWNING_PART; - } - - public static int toCode(RocketComponent.Position position) { - if (RocketComponent.Position.TOP.equals(position)) { - return 0; - } - if (RocketComponent.Position.ABSOLUTE.equals(position)) { - return 1; - } - if (RocketComponent.Position.BOTTOM.equals(position)) { - return 2; - } - return 0; - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/file/rocksim/importt/RocksimNoseConeCode.java b/src/net/sf/openrocket/file/rocksim/importt/RocksimNoseConeCode.java deleted file mode 100644 index 04540020..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/RocksimNoseConeCode.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * RocksimNoseConeCode.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.rocketcomponent.Transition; - -/** - * Models the nose cone shape of a rocket. Maps from Rocksim's notion to OpenRocket's. - */ -public enum RocksimNoseConeCode { - CONICAL (0, Transition.Shape.CONICAL), - OGIVE (1, Transition.Shape.OGIVE), - PARABOLIC (2, Transition.Shape.ELLIPSOID), //Rocksim' PARABOLIC most closely resembles an ELLIPSOID in OpenRocket - ELLIPTICAL (3, Transition.Shape.ELLIPSOID), - POWER_SERIES (4, Transition.Shape.POWER), - PARABOLIC_SERIES(5, Transition.Shape.PARABOLIC), - HAACK (6, Transition.Shape.HAACK); - - /** The Rocksim enumeration value. Sent in XML. */ - private final int ordinal; - - /** The corresponding OpenRocket shape. */ - private final Transition.Shape shape; - - /** - * Constructor. - * - * @param idx the Rocksim shape code - * @param aShape the corresponding OpenRocket shape - */ - private RocksimNoseConeCode(int idx, Transition.Shape aShape) { - ordinal = idx; - shape = aShape; - } - - /** - * Get the OpenRocket shape that corresponds to the Rocksim shape. - * - * @return a shape - */ - public Transition.Shape asOpenRocket() { - return shape; - } - - /** - * Lookup an instance of this enum based upon the Rocksim code. - * - * @param rocksimShapeCode the Rocksim code (from XML) - * @return an instance of this enum - */ - public static RocksimNoseConeCode fromCode(int rocksimShapeCode) { - RocksimNoseConeCode[] values = values(); - for (RocksimNoseConeCode value : values) { - if (value.ordinal == rocksimShapeCode) { - return value; - } - } - return PARABOLIC; //Default - } - - /** - * Lookup an ordinal value for the Rocksim code. - * - * @param type the OR Shape - * - * @return the Rocksim code - */ - public static int toCode(Transition.Shape type) { - RocksimNoseConeCode[] values = values(); - for (RocksimNoseConeCode value : values) { - if (value.shape.equals(type)) { - if (value.ordinal == 2) { - return 3; - } - return value.ordinal; - } - } - return ELLIPTICAL.ordinal; //Default - } -} diff --git a/src/net/sf/openrocket/file/rocksim/importt/StreamerHandler.java b/src/net/sf/openrocket/file/rocksim/importt/StreamerHandler.java deleted file mode 100644 index 3002a783..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/StreamerHandler.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * StreamerHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Streamer; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * A SAX handler for Streamer components. - */ -class StreamerHandler extends RecoveryDeviceHandler<Streamer> { - - /** - * The OpenRocket Streamer. - */ - private final Streamer streamer; - - /** - * Constructor. - * - * @param c the parent component - * @param warnings the warning set - * - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public StreamerHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent of a streamer may not be null."); - } - streamer = new Streamer(); - if (isCompatible(c, Streamer.class, warnings)) { - c.addChild(streamer); - } - } - - /** - * {@inheritDoc} - */ - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - /** - * {@inheritDoc} - */ - @Override - public void closeElement(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("Width".equals(element)) { - streamer.setStripWidth(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("Len".equals(element)) { - streamer.setStripLength(Math.max(0, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("DragCoefficient".equals(element)) { - streamer.setCD(Double.parseDouble(content)); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - /** - * {@inheritDoc} - */ - @Override - public Streamer getComponent() { - return streamer; - } - -} - diff --git a/src/net/sf/openrocket/file/rocksim/importt/TransitionHandler.java b/src/net/sf/openrocket/file/rocksim/importt/TransitionHandler.java deleted file mode 100644 index b589b677..00000000 --- a/src/net/sf/openrocket/file/rocksim/importt/TransitionHandler.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * TransitionHandler.java - */ -package net.sf.openrocket.file.rocksim.importt; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.file.simplesax.ElementHandler; -import net.sf.openrocket.file.simplesax.PlainTextHandler; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import org.xml.sax.SAXException; - -import java.util.HashMap; - -/** - * The SAX handler for Transition components. - */ -class TransitionHandler extends BaseHandler<Transition> { - /** - * The OpenRocket Transition. - */ - private final Transition transition = new Transition(); - - /** - * The wall thickness. Used for hollow nose cones. - */ - private double thickness = 0d; - - /** - * Constructor. - * - * @param c the parent component - * @param warnings the warning set - * @throws IllegalArgumentException thrown if <code>c</code> is null - */ - public TransitionHandler(RocketComponent c, WarningSet warnings) throws IllegalArgumentException { - if (c == null) { - throw new IllegalArgumentException("The parent of a transition may not be null."); - } - if (isCompatible(c, Transition.class, warnings)) { - c.addChild(transition); - } - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, WarningSet warnings) { - return PlainTextHandler.INSTANCE; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - super.closeElement(element, attributes, content, warnings); - - try { - if ("ShapeCode".equals(element)) { - transition.setType(RocksimNoseConeCode.fromCode(Integer.parseInt(content)).asOpenRocket()); - } - if ("Len".equals(element)) { - transition.setLength(Math.max(0, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("FrontDia".equals(element)) { - transition.setForeRadius(Math.max(0, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("RearDia".equals(element)) { - transition.setAftRadius(Math.max(0, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("WallThickness".equals(element)) { - thickness = Math.max(0d, Double.parseDouble(content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH); - } - if ("FrontShoulderDia".equals(element)) { - transition.setForeShoulderRadius(Math.max(0d, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("RearShoulderDia".equals(element)) { - transition.setAftShoulderRadius(Math.max(0d, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_RADIUS)); - } - if ("FrontShoulderLen".equals(element)) { - transition.setForeShoulderLength(Math.max(0d, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("RearShoulderLen".equals(element)) { - transition.setAftShoulderLength(Math.max(0d, Double.parseDouble( - content) / RocksimHandler.ROCKSIM_TO_OPENROCKET_LENGTH)); - } - if ("ShapeParameter".equals(element)) { - if (Transition.Shape.POWER.equals(transition.getType()) || - Transition.Shape.HAACK.equals(transition.getType()) || - Transition.Shape.PARABOLIC.equals(transition.getType())) { - transition.setShapeParameter(Double.parseDouble(content)); - } - } - if ("ConstructionType".equals(element)) { - int typeCode = Integer.parseInt(content); - if (typeCode == 0) { - //SOLID - transition.setFilled(true); - } - else if (typeCode == 1) { - //HOLLOW - transition.setFilled(false); - } - } - if ("FinishCode".equals(element)) { - transition.setFinish(RocksimFinishCode.fromCode(Integer.parseInt(content)).asOpenRocket()); - } - if ("Material".equals(element)) { - setMaterialName(content); - } - } - catch (NumberFormatException nfe) { - warnings.add("Could not convert " + element + " value of " + content + ". It is expected to be a number."); - } - } - - @Override - public void endHandler(String element, HashMap<String, String> attributes, String content, WarningSet warnings) - throws SAXException { - super.endHandler(element, attributes, content, warnings); - - if (transition.isFilled()) { - transition.setAftShoulderThickness(transition.getAftShoulderRadius()); - transition.setForeShoulderThickness(transition.getForeShoulderRadius()); - } - else { - transition.setThickness(thickness); - transition.setAftShoulderThickness(thickness); - transition.setForeShoulderThickness(thickness); - } - } - - - @Override - public Transition getComponent() { - return transition; - } - - /** - * Get the required type of material for this component. - * - * @return BULK - */ - public Material.Type getMaterialType() { - return Material.Type.BULK; - } - - -} - diff --git a/src/net/sf/openrocket/file/simplesax/DelegatorHandler.java b/src/net/sf/openrocket/file/simplesax/DelegatorHandler.java deleted file mode 100644 index 3ec6bdc1..00000000 --- a/src/net/sf/openrocket/file/simplesax/DelegatorHandler.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.sf.openrocket.file.simplesax; - -import java.util.ArrayDeque; -import java.util.Deque; -import java.util.HashMap; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; - -import org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -/** - * The actual SAX handler class. Contains the necessary methods for parsing the SAX source. - * Delegates the actual content parsing to {@link ElementHandler} objects. - */ -class DelegatorHandler extends DefaultHandler { - private final WarningSet warnings; - - private final Deque<ElementHandler> handlerStack = new ArrayDeque<ElementHandler>(); - private final Deque<StringBuilder> elementData = new ArrayDeque<StringBuilder>(); - private final Deque<HashMap<String, String>> elementAttributes = new ArrayDeque<HashMap<String, String>>(); - - - // Ignore all elements as long as ignore > 0 - private int ignore = 0; - - - public DelegatorHandler(ElementHandler initialHandler, WarningSet warnings) { - this.warnings = warnings; - handlerStack.add(initialHandler); - elementData.add(new StringBuilder()); // Just in case - } - - - ///////// SAX handlers - - @Override - public void startElement(String uri, String localName, String name, - Attributes attributes) throws SAXException { - - // Check for ignore - if (ignore > 0) { - ignore++; - return; - } - - // Check for unknown namespace - if (!uri.equals("")) { - warnings.add(Warning.fromString("Unknown namespace element '" + uri - + "' encountered, ignoring.")); - ignore++; - return; - } - - // Add layer to data stacks - elementData.push(new StringBuilder()); - elementAttributes.push(copyAttributes(attributes)); - - // Call the handler - ElementHandler h = handlerStack.peek(); - h = h.openElement(localName, elementAttributes.peek(), warnings); - if (h != null) { - handlerStack.push(h); - } else { - // Start ignoring elements - ignore++; - } - } - - - /** - * Stores encountered characters in the elementData stack. - */ - @Override - public void characters(char[] chars, int start, int length) throws SAXException { - // Check for ignore - if (ignore > 0) - return; - - StringBuilder sb = elementData.peek(); - sb.append(chars, start, length); - } - - - /** - * Removes the last layer from the stack. - */ - @Override - public void endElement(String uri, String localName, String name) throws SAXException { - - // Check for ignore - if (ignore > 0) { - ignore--; - return; - } - - // Remove data from stack - String data = elementData.pop().toString(); // throws on error - HashMap<String, String> attr = elementAttributes.pop(); - - // Remove last handler and call the next one - ElementHandler h; - - h = handlerStack.pop(); - h.endHandler(localName, attr, data, warnings); - - h = handlerStack.peek(); - h.closeElement(localName, attr, data, warnings); - } - - - private static HashMap<String, String> copyAttributes(Attributes atts) { - HashMap<String, String> ret = new HashMap<String, String>(); - for (int i = 0; i < atts.getLength(); i++) { - ret.put(atts.getLocalName(i), atts.getValue(i)); - } - return ret; - } -} diff --git a/src/net/sf/openrocket/file/simplesax/ElementHandler.java b/src/net/sf/openrocket/file/simplesax/ElementHandler.java deleted file mode 100644 index ae678f80..00000000 --- a/src/net/sf/openrocket/file/simplesax/ElementHandler.java +++ /dev/null @@ -1,94 +0,0 @@ -package net.sf.openrocket.file.simplesax; - -import java.util.HashMap; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; - -import org.xml.sax.SAXException; - - -/** - * A "simple XML" element handler. An object of this class handles a single element of - * an XML file. If the input file is: - * - * <foo> - * <bar>message</bar> - * </foo> - * - * and the initial handler is initHandler, then the following methods will be called: - * - * 1. initHandler.{@link #openElement(String, HashMap, WarningSet)} is called for - * the opening element <bar>, which returns fooHandler - * 2. fooHandler.{@link #openElement(String, HashMap, WarningSet)} is called for - * the opening element <bar>, which returns barHandler - * 3. barHandler.{@link #endHandler(String, HashMap, String, WarningSet)} is called for - * the closing element </bar> - * 4. fooHandler.{@link #closeElement(String, HashMap, String, WarningSet)} is called for - * the closing element </bar> - * 5. fooHandler.{@link #endHandler(String, HashMap, String, WarningSet)} is called for - * the closing element </foo> - * 6. initHandler.{@link #closeElement(String, HashMap, String, WarningSet)} is called for - * the closing element </foo> - * - * Note that {@link #endHandler(String, HashMap, String, WarningSet)} is not called for - * the initial handler. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class ElementHandler { - - /** - * Called when an opening element is encountered. Returns the handler that will handle - * the elements within that element, or <code>null</code> if the element and all of - * its contents is to be ignored. - * <p> - * Note that this method may also return <code>this</code>, in which case this - * handler will also handle the subelement. - * - * @param element the element name. - * @param attributes attributes of the element. - * @param warnings the warning set to store warnings in. - * @return the handler that handles elements encountered within this element, - * or <code>null</code> if the element is to be ignored. - */ - public abstract ElementHandler openElement(String element, - HashMap<String, String> attributes, WarningSet warnings) throws SAXException; - - /** - * Called when an element is closed. The default implementation checks whether there is - * any non-space text within the element and if there exists any attributes, and adds - * a warning of both. This can be used at the and of the method to check for - * spurious data. - * - * @param element the element name. - * @param attributes attributes of the element. - * @param content the textual content of the element. - * @param warnings the warning set to store warnings in. - */ - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - - if (!content.trim().equals("")) { - warnings.add(Warning.fromString("Unknown text in element '" + element - + "', ignoring.")); - } - if (!attributes.isEmpty()) { - warnings.add(Warning.fromString("Unknown attributes in element '" + element - + "', ignoring.")); - } - } - - - /** - * Called when the element block that this handler is handling ends. - * The default implementation is a no-op. - * - * @param warnings the warning set to store warnings in. - */ - public void endHandler(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - // No-op - } - -} diff --git a/src/net/sf/openrocket/file/simplesax/NullElementHandler.java b/src/net/sf/openrocket/file/simplesax/NullElementHandler.java deleted file mode 100644 index e8dd2df8..00000000 --- a/src/net/sf/openrocket/file/simplesax/NullElementHandler.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.sf.openrocket.file.simplesax; - -import java.util.HashMap; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; - -import org.xml.sax.SAXException; - -/** - * A singleton element handler that does not accept any content in the element - * except whitespace text. All subelements are ignored and a warning is produced - * of them. It ignores any attributes. - * <p> - * This class can be used for elements that have no content but contain attributes. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class NullElementHandler extends ElementHandler { - public static final NullElementHandler INSTANCE = new NullElementHandler(); - - private static final HashMap<String, String> EMPTY_MAP = new HashMap<String,String>(); - - private NullElementHandler() { - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) throws SAXException { - super.closeElement(element, EMPTY_MAP, content, warnings); - } - -} diff --git a/src/net/sf/openrocket/file/simplesax/PlainTextHandler.java b/src/net/sf/openrocket/file/simplesax/PlainTextHandler.java deleted file mode 100644 index 03be721a..00000000 --- a/src/net/sf/openrocket/file/simplesax/PlainTextHandler.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.sf.openrocket.file.simplesax; - -import java.util.HashMap; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; - -/** - * An element handler that does not allow any sub-elements. If any are encountered - * a warning is generated and they are ignored. - */ -public class PlainTextHandler extends ElementHandler { - public static final PlainTextHandler INSTANCE = new PlainTextHandler(); - - private PlainTextHandler() { - } - - @Override - public ElementHandler openElement(String element, HashMap<String, String> attributes, - WarningSet warnings) { - warnings.add(Warning.fromString("Unknown element " + element + ", ignoring.")); - return null; - } - - @Override - public void closeElement(String element, HashMap<String, String> attributes, - String content, WarningSet warnings) { - // Warning from openElement is sufficient. - } -} - diff --git a/src/net/sf/openrocket/file/simplesax/SimpleSAX.java b/src/net/sf/openrocket/file/simplesax/SimpleSAX.java deleted file mode 100644 index 025c8886..00000000 --- a/src/net/sf/openrocket/file/simplesax/SimpleSAX.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.sf.openrocket.file.simplesax; - -import java.io.IOException; - -import net.sf.openrocket.aerodynamics.WarningSet; - -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; - - -/** - * A "simple SAX" XML reader. This system imposes the limit that an XML element may - * contain either textual (non-whitespace) content OR additional elements, but not - * both. This holds true for both the OpenRocket and RockSim design formats and the - * RockSim engine definition format. - * <p> - * The actual handling is performed by subclasses of {@link ElementHandler}. The - * initial handler is provided to the {@link #readXML(InputSource, ElementHandler, WarningSet)} - * method. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimpleSAX { - - /** - * Read a simple XML file. - * - * @param source the SAX input source. - * @param initialHandler the initial content handler. - * @param warnings a warning set to store warning (cannot be <code>null</code>). - * @throws IOException if an I/O exception occurs while reading. - * @throws SAXException if e.g. malformed XML is encountered. - */ - public static void readXML(InputSource source, ElementHandler initialHandler, - WarningSet warnings) throws IOException, SAXException { - - DelegatorHandler xmlhandler = new DelegatorHandler(initialHandler, warnings); - - XMLReader reader = XMLReaderFactory.createXMLReader(); - reader.setContentHandler(xmlhandler); - reader.setErrorHandler(xmlhandler); - reader.parse(source); - } - -} diff --git a/src/net/sf/openrocket/gui/Resettable.java b/src/net/sf/openrocket/gui/Resettable.java deleted file mode 100644 index 1f30d2b7..00000000 --- a/src/net/sf/openrocket/gui/Resettable.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.sf.openrocket.gui; - -/** - * An interface for GUI elements with a resettable model. The resetModel() method in - * this interface resets the model to some default model, releasing the old model - * listening connections. - * - * Some components that don't have a settable model simply release the current model. - * These components cannot therefore be reused after calling resetModel(). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface Resettable { - public void resetModel(); -} diff --git a/src/net/sf/openrocket/gui/SpinnerEditor.java b/src/net/sf/openrocket/gui/SpinnerEditor.java deleted file mode 100644 index 843eed6a..00000000 --- a/src/net/sf/openrocket/gui/SpinnerEditor.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.sf.openrocket.gui; - -import javax.swing.JSpinner; - -/** - * Editable editor for a JSpinner. Simply uses JSpinner.DefaultEditor, which has been made - * editable. Why the f*** isn't this possible in the normal API? - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class SpinnerEditor extends JSpinner.NumberEditor { -//public class SpinnerEditor extends JSpinner.DefaultEditor { - - public SpinnerEditor(JSpinner spinner) { - //super(spinner); - super(spinner,"0.0##"); - //getTextField().setEditable(true); - } - -} diff --git a/src/net/sf/openrocket/gui/StorageOptionChooser.java b/src/net/sf/openrocket/gui/StorageOptionChooser.java deleted file mode 100644 index 49b21974..00000000 --- a/src/net/sf/openrocket/gui/StorageOptionChooser.java +++ /dev/null @@ -1,287 +0,0 @@ -package net.sf.openrocket.gui; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.BorderFactory; -import javax.swing.ButtonGroup; -import javax.swing.JCheckBox; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JRadioButton; -import javax.swing.JSpinner; -import javax.swing.SpinnerNumberModel; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.document.StorageOptions; -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.file.openrocket.OpenRocketSaver; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.startup.Application; - -public class StorageOptionChooser extends JPanel { - - public static final double DEFAULT_SAVE_TIME_SKIP = 0.20; - - private final OpenRocketDocument document; - - private JRadioButton allButton; - private JRadioButton someButton; - private JRadioButton noneButton; - - private JSpinner timeSpinner; - - private JCheckBox compressButton; - - private JLabel estimateLabel; - - - private boolean artificialEvent = false; - private static final Translator trans = Application.getTranslator(); - - public StorageOptionChooser(OpenRocketDocument doc, StorageOptions opts) { - super(new MigLayout()); - - this.document = doc; - - - ChangeListener changeUpdater = new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - updateEstimate(); - } - }; - ActionListener actionUpdater = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - updateEstimate(); - } - }; - - - ButtonGroup buttonGroup = new ButtonGroup(); - String tip; - - //// Simulated data to store: - this.add(new JLabel(trans.get("StorageOptChooser.lbl.Simdatatostore")), "spanx, wrap unrel"); - - //// All simulated data - allButton = new JRadioButton(trans.get("StorageOptChooser.rdbut.Allsimdata")); - //// <html>Store all simulated data.<br> - //// This can result in very large files! - allButton.setToolTipText(trans.get("StorageOptChooser.lbl.longA1") + - trans.get("StorageOptChooser.lbl.longA2")); - buttonGroup.add(allButton); - allButton.addActionListener(actionUpdater); - this.add(allButton, "spanx, wrap rel"); - - //// Every - someButton = new JRadioButton(trans.get("StorageOptChooser.rdbut.Every")); - //// <html>Store plottable values approximately this far apart.<br>" - //// Larger values result in smaller files. - tip = trans.get("StorageOptChooser.lbl.longB1") + - trans.get("StorageOptChooser.lbl.longB2"); - someButton.setToolTipText(tip); - buttonGroup.add(someButton); - someButton.addActionListener(actionUpdater); - this.add(someButton, ""); - - timeSpinner = new JSpinner(new SpinnerNumberModel(0.0, 0.0, 5.0, 0.1)); - timeSpinner.setToolTipText(tip); - timeSpinner.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - if (artificialEvent) - return; - someButton.setSelected(true); - } - }); - this.add(timeSpinner, "wmin 55lp"); - timeSpinner.addChangeListener(changeUpdater); - - //// seconds - JLabel label = new JLabel(trans.get("StorageOptChooser.lbl.seconds")); - label.setToolTipText(tip); - this.add(label, "wrap rel"); - - //// Only primary figures - noneButton = new JRadioButton(trans.get("StorageOptChooser.rdbut.Onlyprimfig")); - //// <html>Store only the values shown in the summary table.<br> - //// This results in the smallest files. - noneButton.setToolTipText(trans.get("StorageOptChooser.lbl.longC1") + - trans.get("StorageOptChooser.lbl.longC2")); - buttonGroup.add(noneButton); - noneButton.addActionListener(actionUpdater); - this.add(noneButton, "spanx, wrap 20lp"); - - - //// Compress file - compressButton = new JCheckBox(trans.get("StorageOptChooser.checkbox.Compfile")); - //// Using compression reduces the file size significantly. - compressButton.setToolTipText(trans.get("StorageOptChooser.lbl.UsingComp")); - compressButton.addActionListener(actionUpdater); - this.add(compressButton, "spanx, wrap para"); - - - // Estimate is updated in loadOptions(opts) - estimateLabel = new JLabel(""); - //// An estimate on how large the resulting file would - //// be with the present options. - estimateLabel.setToolTipText(trans.get("StorageOptChooser.lbl.longD1")); - this.add(estimateLabel, "spanx"); - - - this.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createEmptyBorder(0, 10, 0, 0), - //// Save options - BorderFactory.createTitledBorder(trans.get("StorageOptChooser.ttip.Saveopt")))); - - loadOptions(opts); - } - - - public void loadOptions(StorageOptions opts) { - double t; - - // Data storage radio button - t = opts.getSimulationTimeSkip(); - if (t == StorageOptions.SIMULATION_DATA_ALL) { - allButton.setSelected(true); - t = DEFAULT_SAVE_TIME_SKIP; - } else if (t == StorageOptions.SIMULATION_DATA_NONE) { - noneButton.setSelected(true); - t = DEFAULT_SAVE_TIME_SKIP; - } else { - someButton.setSelected(true); - } - - // Time skip spinner - artificialEvent = true; - timeSpinner.setValue(t); - artificialEvent = false; - - // Compression checkbox - compressButton.setSelected(opts.isCompressionEnabled()); - - updateEstimate(); - } - - - public void storeOptions(StorageOptions opts) { - double t; - - if (allButton.isSelected()) { - t = StorageOptions.SIMULATION_DATA_ALL; - } else if (noneButton.isSelected()) { - t = StorageOptions.SIMULATION_DATA_NONE; - } else { - t = (Double)timeSpinner.getValue(); - } - - opts.setSimulationTimeSkip(t); - - opts.setCompressionEnabled(compressButton.isSelected()); - - opts.setExplicitlySet(true); - } - - - - // TODO: MEDIUM: The estimation method always uses OpenRocketSaver! - private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver(); - - private void updateEstimate() { - StorageOptions opts = new StorageOptions(); - - storeOptions(opts); - long size = ROCKET_SAVER.estimateFileSize(document, opts); - size = Math.max((size+512)/1024, 1); - - String formatted; - - if (size >= 10000) { - formatted = (size/1000) + " MB"; - } else if (size >= 1000){ - formatted = (size/1000) + "." + ((size/100)%10) + " MB"; - } else if (size >= 100) { - formatted = ((size/10)*10) + " kB"; - } else { - formatted = size + " kB"; - } - - //// Estimated file size: - estimateLabel.setText(trans.get("StorageOptChooser.lbl.Estfilesize") + " " + formatted); - } - - - - /** - * Asks the user the storage options using a modal dialog window if the document - * contains simulated data and the user has not explicitly set how to store the data. - * - * @param document the document to check. - * @param parent the parent frame for the dialog. - * @return <code>true</code> to continue, <code>false</code> if the user cancelled. - */ - public static boolean verifyStorageOptions(OpenRocketDocument document, JFrame parent) { - StorageOptions options = document.getDefaultStorageOptions(); - - if (options.isExplicitlySet()) { - // User has explicitly set the values, save as is - return true; - } - - - boolean hasData = false; - - simulationLoop: - for (Simulation s: document.getSimulations()) { - if (s.getStatus() == Simulation.Status.NOT_SIMULATED || - s.getStatus() == Simulation.Status.EXTERNAL) - continue; - - FlightData data = s.getSimulatedData(); - if (data == null) - continue; - - for (int i=0; i < data.getBranchCount(); i++) { - FlightDataBranch branch = data.getBranch(i); - if (branch == null) - continue; - if (branch.getLength() > 0) { - hasData = true; - break simulationLoop; - } - } - } - - - if (!hasData) { - // No data to store, do not ask only about compression - return true; - } - - - StorageOptionChooser chooser = new StorageOptionChooser(document, options); - - //// Save options - if (JOptionPane.showConfirmDialog(parent, chooser, trans.get("StorageOptChooser.lbl.Saveopt"), - JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) != - JOptionPane.OK_OPTION) { - // User cancelled - return false; - } - - chooser.storeOptions(options); - return true; - } - -} diff --git a/src/net/sf/openrocket/gui/TextFieldListener.java b/src/net/sf/openrocket/gui/TextFieldListener.java deleted file mode 100644 index d0773004..00000000 --- a/src/net/sf/openrocket/gui/TextFieldListener.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.gui; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; - -import javax.swing.JTextField; - -public abstract class TextFieldListener implements ActionListener, FocusListener { - private JTextField field; - - public void listenTo(JTextField newField) { - if (field != null) { - field.removeActionListener(this); - field.removeFocusListener(this); - } - field = newField; - if (field != null) { - field.addActionListener(this); - field.addFocusListener(this); - } - } - - public abstract void setText(String text); - - public void actionPerformed(ActionEvent e) { - setText(field.getText()); - } - public void focusGained(FocusEvent e) { } - public void focusLost(FocusEvent e) { - setText(field.getText()); - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/adaptors/BooleanModel.java b/src/net/sf/openrocket/gui/adaptors/BooleanModel.java deleted file mode 100644 index a2f95ed3..00000000 --- a/src/net/sf/openrocket/gui/adaptors/BooleanModel.java +++ /dev/null @@ -1,331 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.beans.PropertyChangeListener; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.EventObject; -import java.util.List; - -import javax.swing.AbstractAction; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Invalidatable; -import net.sf.openrocket.util.Invalidator; -import net.sf.openrocket.util.MemoryManagement; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.StateChangeListener; - - -/** - * A class that adapts an isXXX/setXXX boolean variable. It functions as an Action suitable - * for usage in JCheckBox or JToggleButton. You can create a suitable button with - * <code> - * check = new JCheckBox(new BooleanModel(component,"Value")) - * check.setText("Label"); - * </code> - * This will produce a button that uses isValue() and setValue(boolean) of the corresponding - * component. - * <p> - * Additionally a number of component enabled states may be controlled by this class using - * the method {@link #addEnableComponent(Component, boolean)}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class BooleanModel extends AbstractAction implements StateChangeListener, Invalidatable { - private static final LogHelper log = Application.getLogger(); - - private final ChangeSource source; - private final String valueName; - - /* Only used when referencing a ChangeSource! */ - private final Method getMethod; - private final Method setMethod; - private final Method getEnabled; - - /* Only used with internal boolean value! */ - private boolean value; - - - private final List<Component> components = new ArrayList<Component>(); - private final List<Boolean> componentEnableState = new ArrayList<Boolean>(); - - private String toString = null; - - private int firing = 0; - - private boolean oldValue; - private boolean oldEnabled; - - private Invalidator invalidator = new Invalidator(this); - - - /** - * Construct a BooleanModel that holds the boolean value within itself. - * - * @param initialValue the initial value of the boolean - */ - public BooleanModel(boolean initialValue) { - this.valueName = null; - this.source = null; - this.getMethod = null; - this.setMethod = null; - this.getEnabled = null; - - this.value = initialValue; - - oldValue = getValue(); - oldEnabled = getIsEnabled(); - - this.setEnabled(oldEnabled); - this.putValue(SELECTED_KEY, oldValue); - - } - - /** - * Construct a BooleanModel that references the boolean from a ChangeSource method. - * - * @param source the boolean source. - * @param valueName the name of the getter/setter method (without the get/is/set prefix) - */ - public BooleanModel(ChangeSource source, String valueName) { - this.source = source; - this.valueName = valueName; - - Method getter = null, setter = null; - - - // Try get/is and set - try { - getter = source.getClass().getMethod("is" + valueName); - } catch (NoSuchMethodException ignore) { - } - if (getter == null) { - try { - getter = source.getClass().getMethod("get" + valueName); - } catch (NoSuchMethodException ignore) { - } - } - try { - setter = source.getClass().getMethod("set" + valueName, boolean.class); - } catch (NoSuchMethodException ignore) { - } - - if (getter == null || setter == null) { - throw new IllegalArgumentException("get/is methods for boolean '" + valueName + - "' not present in class " + source.getClass().getCanonicalName()); - } - - getMethod = getter; - setMethod = setter; - - Method e = null; - try { - e = source.getClass().getMethod("is" + valueName + "Enabled"); - } catch (NoSuchMethodException ignore) { - } - getEnabled = e; - - oldValue = getValue(); - oldEnabled = getIsEnabled(); - - this.setEnabled(oldEnabled); - this.putValue(SELECTED_KEY, oldValue); - - source.addChangeListener(this); - } - - public boolean getValue() { - - if (getMethod != null) { - - try { - return (Boolean) getMethod.invoke(source); - } catch (IllegalAccessException e) { - throw new BugException("getMethod execution error for source " + source, e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - - } else { - - // Use internal value - return value; - - } - } - - public void setValue(boolean b) { - checkState(true); - log.debug("Setting value of " + this + " to " + b); - - if (setMethod != null) { - try { - setMethod.invoke(source, new Object[] { b }); - } catch (IllegalAccessException e) { - throw new BugException("setMethod execution error for source " + source, e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } else { - // Manually fire state change - normally the ChangeSource fires it - value = b; - stateChanged(null); - } - } - - - /** - * Add a component the enabled status of which will be controlled by the value - * of this boolean. The <code>component</code> will be enabled exactly when - * the state of this model is equal to that of <code>enableState</code>. - * - * @param component the component to control. - * @param enableState the state in which the component should be enabled. - */ - public void addEnableComponent(Component component, boolean enableState) { - checkState(true); - components.add(component); - componentEnableState.add(enableState); - updateEnableStatus(); - } - - /** - * Add a component which will be enabled when this boolean is <code>true</code>. - * This is equivalent to <code>booleanModel.addEnableComponent(component, true)</code>. - * - * @param component the component to control. - * @see #addEnableComponent(Component, boolean) - */ - public void addEnableComponent(Component component) { - checkState(true); - addEnableComponent(component, true); - } - - private void updateEnableStatus() { - boolean state = getValue(); - - for (int i = 0; i < components.size(); i++) { - Component c = components.get(i); - boolean b = componentEnableState.get(i); - c.setEnabled(state == b); - } - } - - - - private boolean getIsEnabled() { - if (getEnabled == null) - return true; - try { - return (Boolean) getEnabled.invoke(source); - } catch (IllegalAccessException e) { - throw new BugException("getEnabled execution error for source " + source, e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - @Override - public void stateChanged(EventObject event) { - checkState(true); - - if (firing > 0) { - log.debug("Ignoring stateChanged of " + this + ", currently firing events"); - return; - } - - boolean v = getValue(); - boolean e = getIsEnabled(); - if (oldValue != v) { - log.debug("Value of " + this + " has changed to " + v + " oldValue=" + oldValue); - oldValue = v; - firing++; - this.putValue(SELECTED_KEY, getValue()); - // this.firePropertyChange(SELECTED_KEY, !v, v); - updateEnableStatus(); - firing--; - } - if (oldEnabled != e) { - log.debug("Enabled status of " + this + " has changed to " + e + " oldEnabled=" + oldEnabled); - oldEnabled = e; - setEnabled(e); - } - } - - - @Override - public void actionPerformed(ActionEvent e) { - if (firing > 0) { - log.debug("Ignoring actionPerformed of " + this + ", currently firing events"); - return; - } - - boolean v = (Boolean) this.getValue(SELECTED_KEY); - log.user("Value of " + this + " changed to " + v + " oldValue=" + oldValue); - if (v != oldValue) { - firing++; - setValue(v); - oldValue = getValue(); - // Update all states - this.putValue(SELECTED_KEY, oldValue); - this.setEnabled(getIsEnabled()); - updateEnableStatus(); - firing--; - } - } - - - @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - checkState(true); - super.addPropertyChangeListener(listener); - } - - - /** - * Invalidates this model by removing all listeners and removing this from - * listening to the source. After invalidation no listeners can be added to this - * model and the value cannot be set. - */ - @Override - public void invalidate() { - invalidator.invalidate(); - - PropertyChangeListener[] listeners = this.getPropertyChangeListeners(); - if (listeners.length > 0) { - log.warn("Invalidating " + this + " while still having listeners " + listeners); - for (PropertyChangeListener l : listeners) { - this.removePropertyChangeListener(l); - } - } - if (source != null) { - source.removeChangeListener(this); - } - MemoryManagement.collectable(this); - } - - - private void checkState(boolean error) { - invalidator.check(error); - } - - - - @Override - public String toString() { - if (toString == null) { - if (source != null) { - toString = "BooleanModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; - } else { - toString = "BooleanModel[internal value]"; - } - } - return toString; - } -} diff --git a/src/net/sf/openrocket/gui/adaptors/Column.java b/src/net/sf/openrocket/gui/adaptors/Column.java deleted file mode 100644 index 87997c48..00000000 --- a/src/net/sf/openrocket/gui/adaptors/Column.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - -import javax.swing.table.TableColumnModel; - -public abstract class Column { - private final String name; - - /** - * Create a new column with specified name. Additionally, the {@link #getValueAt(int)} - * method must be implemented. - * - * @param name the caption of the column. - */ - public Column(String name) { - this.name = name; - } - - /** - * Return the caption of the column. - */ - @Override - public String toString() { - return name; - } - - /** - * Return the default width of the column. This is used by the method - * {@link #ColumnTableModel.setColumnWidth(TableColumnModel)}. The default width is - * 100, the method may be overridden to return other values relative to this value. - * - * @return the relative width of the column (default 100). - */ - public int getDefaultWidth() { - return 100; - } - - - /** - * Returns the exact width of this column. If the return value is positive, - * both the minimum and maximum widths of this column are set to this value - * - * @return the absolute exact width of the column (default 0). - */ - public int getExactWidth() { - return 0; - } - - - /** - * Return the column type class. This is necessary for example for numerical - * sorting of Value objects, showing booleans as checkboxes etc. - * - * @return the object class of this column, by default <code>Object.class</code>. - */ - public Class<?> getColumnClass() { - return Object.class; - } - - /** - * Return the value in this column at the specified row. - * - * @param row the row of the data. - * @return the value at the specified position. - */ - public abstract Object getValueAt(int row); - -} diff --git a/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java b/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java deleted file mode 100644 index e50a8ea5..00000000 --- a/src/net/sf/openrocket/gui/adaptors/ColumnTableModel.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import net.sf.openrocket.startup.Application; - -public abstract class ColumnTableModel extends AbstractTableModel { - private final Column[] columns; - - public ColumnTableModel(Column... columns) { - this.columns = columns; - } - - public void setColumnWidths(TableColumnModel model) { - for (int i = 0; i < columns.length; i++) { - if (columns[i].getExactWidth() > 0) { - TableColumn col = model.getColumn(i); - int w = columns[i].getExactWidth(); - col.setResizable(false); - col.setMinWidth(w); - col.setMaxWidth(w); - col.setPreferredWidth(w); - } else { - model.getColumn(i).setPreferredWidth(columns[i].getDefaultWidth()); - } - } - } - - @Override - public int getColumnCount() { - return columns.length; - } - - @Override - public String getColumnName(int col) { - return columns[col].toString(); - } - - @Override - public Class<?> getColumnClass(int col) { - return columns[col].getColumnClass(); - } - - @Override - public Object getValueAt(int row, int col) { - if ((row < 0) || (row >= getRowCount()) || - (col < 0) || (col >= columns.length)) { - Application.getExceptionHandler().handleErrorCondition("Error: Requested illegal column/row, col=" + col + " row=" + row); - return null; - } - return columns[col].getValueAt(row); - } - -} diff --git a/src/net/sf/openrocket/gui/adaptors/DoubleModel.java b/src/net/sf/openrocket/gui/adaptors/DoubleModel.java deleted file mode 100644 index 820a54a6..00000000 --- a/src/net/sf/openrocket/gui/adaptors/DoubleModel.java +++ /dev/null @@ -1,941 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - -import java.awt.event.ActionEvent; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.EventListener; -import java.util.EventObject; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.BoundedRangeModel; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Invalidatable; -import net.sf.openrocket.util.Invalidator; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.MemoryManagement; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.StateChangeListener; - - -/** - * A model connector that can read and modify any value of any ChangeSource that - * has the appropriate get/set methods defined. - * - * The variable is defined in the constructor by providing the variable name as a string - * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a - * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2. - * - * Sub-models suitable for JSpinners and other components are available from the appropriate - * methods. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class DoubleModel implements StateChangeListener, ChangeSource, Invalidatable { - private static final LogHelper log = Application.getLogger(); - - - public static final DoubleModel ZERO = new DoubleModel(0); - - //////////// JSpinner Model //////////// - - /** - * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel - * to be compatible with the NumberEditor, but only has the necessary methods defined. - */ - private class ValueSpinnerModel extends SpinnerNumberModel implements Invalidatable { - - @Override - public Object getValue() { - return currentUnit.toUnit(DoubleModel.this.getValue()); - } - - @Override - public void setValue(Object value) { - if (firing > 0) { - // Ignore, if called when model is sending events - log.verbose("Ignoring call to SpinnerModel setValue for " + DoubleModel.this.toString() + - " value=" + value + ", currently firing events"); - return; - } - Number num = (Number) value; - double newValue = num.doubleValue(); - double converted = currentUnit.fromUnit(newValue); - - log.user("SpinnerModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue + - " converted=" + converted); - DoubleModel.this.setValue(converted); - - } - - @Override - public Object getNextValue() { - double d = currentUnit.toUnit(DoubleModel.this.getValue()); - double max = currentUnit.toUnit(maxValue); - if (MathUtil.equals(d, max)) - return null; - d = currentUnit.getNextValue(d); - if (d > max) - d = max; - return d; - } - - @Override - public Object getPreviousValue() { - double d = currentUnit.toUnit(DoubleModel.this.getValue()); - double min = currentUnit.toUnit(minValue); - if (MathUtil.equals(d, min)) - return null; - d = currentUnit.getPreviousValue(d); - if (d < min) - d = min; - return d; - } - - - @Override - public Comparable<Double> getMinimum() { - return currentUnit.toUnit(minValue); - } - - @Override - public Comparable<Double> getMaximum() { - return currentUnit.toUnit(maxValue); - } - - - @Override - public void addChangeListener(ChangeListener l) { - DoubleModel.this.addChangeListener(l); - } - - @Override - public void removeChangeListener(ChangeListener l) { - DoubleModel.this.removeChangeListener(l); - } - - @Override - public void invalidate() { - DoubleModel.this.invalidate(); - } - } - - /** - * Returns a new SpinnerModel with the same base as the DoubleModel. - * The values given to the JSpinner are in the currently selected units. - * - * @return A compatibility layer for a SpinnerModel. - */ - public SpinnerModel getSpinnerModel() { - return new ValueSpinnerModel(); - } - - - - - - //////////// JSlider model //////////// - - private class ValueSliderModel implements BoundedRangeModel, StateChangeListener, Invalidatable { - private static final int MAX = 1000; - - /* - * Use linear scale value = linear1 * x + linear0 when x < linearPosition - * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise - */ - - // Linear in range x <= linearPosition - private final double linearPosition; - - // May be changing DoubleModels when using linear model - private final DoubleModel min, mid, max; - - // Linear multiplier and constant - //private final double linear1; - //private final double linear0; - - // Non-linear multiplier, exponent and constant - private final double quad2, quad1, quad0; - - - - public ValueSliderModel(DoubleModel min, DoubleModel max) { - linearPosition = 1.0; - - this.min = min; - this.mid = max; // Never use exponential scale - this.max = max; - - min.addChangeListener(this); - max.addChangeListener(this); - - quad2 = quad1 = quad0 = 0; // Not used - } - - - - /** - * Generate a linear model from min to max. - */ - public ValueSliderModel(double min, double max) { - linearPosition = 1.0; - - this.min = new DoubleModel(min); - this.mid = new DoubleModel(max); // Never use exponential scale - this.max = new DoubleModel(max); - - quad2 = quad1 = quad0 = 0; // Not used - } - - public ValueSliderModel(double min, double mid, double max) { - this(min, 0.5, mid, max); - } - - /* - * v(x) = mul * x^exp + add - * - * v(pos) = mul * pos^exp + add = mid - * v(1) = mul + add = max - * v'(pos) = mul*exp * pos^(exp-1) = linearMul - */ - public ValueSliderModel(double min, double pos, double mid, double max) { - this.min = new DoubleModel(min); - this.mid = new DoubleModel(mid); - this.max = new DoubleModel(max); - - - linearPosition = pos; - //linear0 = min; - //linear1 = (mid-min)/pos; - - if (!(min < mid && mid <= max && 0 < pos && pos < 1)) { - throw new IllegalArgumentException("Bad arguments for ValueSliderModel " + - "min=" + min + " mid=" + mid + " max=" + max + " pos=" + pos); - } - - /* - * quad2..0 are calculated such that - * f(pos) = mid - continuity - * f(1) = max - end point - * f'(pos) = linear1 - continuity of derivative - */ - - double delta = (mid - min) / pos; - quad2 = (max - mid - delta + delta * pos) / pow2(pos - 1); - quad1 = (delta + 2 * (mid - max) * pos - delta * pos * pos) / pow2(pos - 1); - quad0 = (mid - (2 * mid + delta) * pos + (max + delta) * pos * pos) / pow2(pos - 1); - - } - - private double pow2(double x) { - return x * x; - } - - @Override - public int getValue() { - double value = DoubleModel.this.getValue(); - if (value <= min.getValue()) - return 0; - if (value >= max.getValue()) - return MAX; - - double x; - if (value <= mid.getValue()) { - // Use linear scale - //linear0 = min; - //linear1 = (mid-min)/pos; - - x = (value - min.getValue()) * linearPosition / (mid.getValue() - min.getValue()); - } else { - // Use quadratic scale - // Further solution of the quadratic equation - // a*x^2 + b*x + c-value == 0 - x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2); - } - return (int) (x * MAX); - } - - - @Override - public void setValue(int newValue) { - if (firing > 0) { - // Ignore loops - log.verbose("Ignoring call to SliderModel setValue for " + DoubleModel.this.toString() + - " value=" + newValue + ", currently firing events"); - return; - } - - double x = (double) newValue / MAX; - double scaledValue; - - if (x <= linearPosition) { - // Use linear scale - //linear0 = min; - //linear1 = (mid-min)/pos; - - scaledValue = (mid.getValue() - min.getValue()) / linearPosition * x + min.getValue(); - } else { - // Use quadratic scale - scaledValue = quad2 * x * x + quad1 * x + quad0; - } - - double converted = currentUnit.fromUnit(currentUnit.round(currentUnit.toUnit(scaledValue))); - log.user("SliderModel setValue called for " + DoubleModel.this.toString() + " newValue=" + newValue + - " scaledValue=" + scaledValue + " converted=" + converted); - DoubleModel.this.setValue(converted); - } - - - // Static get-methods - private boolean isAdjusting; - - @Override - public int getExtent() { - return 0; - } - - @Override - public int getMaximum() { - return MAX; - } - - @Override - public int getMinimum() { - return 0; - } - - @Override - public boolean getValueIsAdjusting() { - return isAdjusting; - } - - // Ignore set-values - @Override - public void setExtent(int newExtent) { - } - - @Override - public void setMaximum(int newMaximum) { - } - - @Override - public void setMinimum(int newMinimum) { - } - - @Override - public void setValueIsAdjusting(boolean b) { - isAdjusting = b; - } - - @Override - public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) { - setValueIsAdjusting(adjusting); - setValue(value); - } - - // Pass change listeners to the underlying model - @Override - public void addChangeListener(ChangeListener l) { - DoubleModel.this.addChangeListener(l); - } - - @Override - public void removeChangeListener(ChangeListener l) { - DoubleModel.this.removeChangeListener(l); - } - - @Override - public void invalidate() { - DoubleModel.this.invalidate(); - } - - @Override - public void stateChanged(EventObject e) { - // Min or max range has changed. - // Fire if not already firing - if (firing == 0) - fireStateChanged(); - } - } - - - public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) { - return new ValueSliderModel(min, max); - } - - public BoundedRangeModel getSliderModel(double min, double max) { - return new ValueSliderModel(min, max); - } - - public BoundedRangeModel getSliderModel(double min, double mid, double max) { - return new ValueSliderModel(min, mid, max); - } - - public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) { - return new ValueSliderModel(min, pos, mid, max); - } - - - - - - //////////// Action model //////////// - - private class AutomaticActionModel extends AbstractAction implements StateChangeListener, Invalidatable { - private boolean oldValue = false; - - public AutomaticActionModel() { - oldValue = isAutomatic(); - addChangeListener(this); - } - - - @Override - public boolean isEnabled() { - return isAutomaticAvailable(); - } - - @Override - public Object getValue(String key) { - if (key.equals(Action.SELECTED_KEY)) { - oldValue = isAutomatic(); - return oldValue; - } - return super.getValue(key); - } - - @Override - public void putValue(String key, Object value) { - if (firing > 0) { - log.verbose("Ignoring call to ActionModel putValue for " + DoubleModel.this.toString() + - " key=" + key + " value=" + value + ", currently firing events"); - return; - } - if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) { - log.user("ActionModel putValue called for " + DoubleModel.this.toString() + - " key=" + key + " value=" + value); - oldValue = (Boolean) value; - setAutomatic((Boolean) value); - } else { - log.debug("Passing ActionModel putValue call to supermethod for " + DoubleModel.this.toString() + - " key=" + key + " value=" + value); - super.putValue(key, value); - } - } - - // Implement a wrapper to the ChangeListeners - ArrayList<PropertyChangeListener> propertyChangeListeners = - new ArrayList<PropertyChangeListener>(); - - @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - propertyChangeListeners.add(listener); - DoubleModel.this.addChangeListener(this); - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - propertyChangeListeners.remove(listener); - if (propertyChangeListeners.isEmpty()) - DoubleModel.this.removeChangeListener(this); - } - - // If the value has changed, generate an event to the listeners - @Override - public void stateChanged(EventObject e) { - boolean newValue = isAutomatic(); - if (oldValue == newValue) - return; - PropertyChangeEvent event = new PropertyChangeEvent(this, Action.SELECTED_KEY, - oldValue, newValue); - oldValue = newValue; - Object[] l = propertyChangeListeners.toArray(); - for (int i = 0; i < l.length; i++) { - ((PropertyChangeListener) l[i]).propertyChange(event); - } - } - - @Override - public void actionPerformed(ActionEvent e) { - // Setting performed in putValue - } - - @Override - public void invalidate() { - DoubleModel.this.invalidate(); - } - } - - /** - * Returns a new Action corresponding to the changes of the automatic setting - * property of the value model. This may be used directly with e.g. check buttons. - * - * @return A compatibility layer for an Action. - */ - public Action getAutomaticAction() { - return new AutomaticActionModel(); - } - - - - - - //////////// Main model ///////////// - - /* - * The main model handles all values in SI units, i.e. no conversion is made within the model. - */ - - private final ChangeSource source; - private final String valueName; - private final double multiplier; - - private final Method getMethod; - private final Method setMethod; - - private final Method getAutoMethod; - private final Method setAutoMethod; - - private final ArrayList<EventListener> listeners = new ArrayList<EventListener>(); - - private final UnitGroup units; - private Unit currentUnit; - - private final double minValue; - private final double maxValue; - - private String toString = null; - - - private int firing = 0; // >0 when model itself is sending events - - - // Used to differentiate changes in valueName and other changes in the component: - private double lastValue = 0; - private boolean lastAutomatic = false; - - private Invalidator invalidator = new Invalidator(this); - - - /** - * Generate a DoubleModel that contains an internal double value. - * - * @param value the initial value. - */ - public DoubleModel(double value) { - this(value, UnitGroup.UNITS_NONE, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - } - - /** - * Generate a DoubleModel that contains an internal double value. - * - * @param value the initial value. - * @param unit the unit for the value. - */ - public DoubleModel(double value, UnitGroup unit) { - this(value, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - } - - /** - * Generate a DoubleModel that contains an internal double value. - * - * @param value the initial value. - * @param unit the unit for the value. - * @param min minimum value. - */ - public DoubleModel(double value, UnitGroup unit, double min) { - this(value, unit, min, Double.POSITIVE_INFINITY); - } - - /** - * Generate a DoubleModel that contains an internal double value. - * - * @param value the initial value. - * @param unit the unit for the value. - * @param min minimum value. - * @param max maximum value. - */ - public DoubleModel(double value, UnitGroup unit, double min, double max) { - this.lastValue = value; - this.minValue = min; - this.maxValue = max; - - source = null; - valueName = "Constant value"; - multiplier = 1; - - getMethod = setMethod = null; - getAutoMethod = setAutoMethod = null; - units = unit; - currentUnit = units.getDefaultUnit(); - } - - - /** - * Generates a new DoubleModel that changes the values of the specified component. - * The double value is read and written using the methods "get"/"set" + valueName. - * - * @param source Component whose parameter to use. - * @param valueName Name of methods used to get/set the parameter. - * @param multiplier Value shown by the model is the value from component.getXXX * multiplier - * @param min Minimum value allowed (in SI units) - * @param max Maximum value allowed (in SI units) - */ - public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit, - double min, double max) { - this.source = source; - this.valueName = valueName; - this.multiplier = multiplier; - - this.units = unit; - currentUnit = units.getDefaultUnit(); - - this.minValue = min; - this.maxValue = max; - - try { - getMethod = source.getClass().getMethod("get" + valueName); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("get method for value '" + valueName + - "' not present in class " + source.getClass().getCanonicalName()); - } - - Method s = null; - try { - s = source.getClass().getMethod("set" + valueName, double.class); - } catch (NoSuchMethodException e1) { - } // Ignore - setMethod = s; - - // Automatic selection methods - - Method set = null, get = null; - - try { - get = source.getClass().getMethod("is" + valueName + "Automatic"); - set = source.getClass().getMethod("set" + valueName + "Automatic", boolean.class); - } catch (NoSuchMethodException e) { - } // ignore - - if (set != null && get != null) { - getAutoMethod = get; - setAutoMethod = set; - } else { - getAutoMethod = null; - setAutoMethod = null; - } - - } - - public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit, - double min) { - this(source, valueName, multiplier, unit, min, Double.POSITIVE_INFINITY); - } - - public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) { - this(source, valueName, multiplier, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - } - - public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, - double min, double max) { - this(source, valueName, 1.0, unit, min, max); - } - - public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) { - this(source, valueName, 1.0, unit, min, Double.POSITIVE_INFINITY); - } - - public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) { - this(source, valueName, 1.0, unit, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - } - - public DoubleModel(ChangeSource source, String valueName) { - this(source, valueName, 1.0, UnitGroup.UNITS_NONE, - Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY); - } - - public DoubleModel(ChangeSource source, String valueName, double min) { - this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, Double.POSITIVE_INFINITY); - } - - public DoubleModel(ChangeSource source, String valueName, double min, double max) { - this(source, valueName, 1.0, UnitGroup.UNITS_NONE, min, max); - } - - - - /** - * Returns the value of the variable (in SI units). - */ - public double getValue() { - if (getMethod == null) // Constant value - return lastValue; - - try { - return (Double) getMethod.invoke(source) * multiplier; - } catch (IllegalArgumentException e) { - throw new BugException("Unable to invoke getMethod of " + this, e); - } catch (IllegalAccessException e) { - throw new BugException("Unable to invoke getMethod of " + this, e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - /** - * Sets the value of the variable. - * @param v New value for parameter in SI units. - */ - public void setValue(double v) { - checkState(true); - - log.debug("Setting value " + v + " for " + this); - if (setMethod == null) { - if (getMethod != null) { - throw new BugException("setMethod not available for variable '" + valueName + - "' in class " + source.getClass().getCanonicalName()); - } - lastValue = v; - fireStateChanged(); - return; - } - - try { - setMethod.invoke(source, v / multiplier); - } catch (IllegalArgumentException e) { - throw new BugException("Unable to invoke setMethod of " + this, e); - } catch (IllegalAccessException e) { - throw new BugException("Unable to invoke setMethod of " + this, e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - - /** - * Returns whether setting the value automatically is available. - */ - public boolean isAutomaticAvailable() { - return (getAutoMethod != null) && (setAutoMethod != null); - } - - /** - * Returns whether the value is currently being set automatically. - * Returns false if automatic setting is not available at all. - */ - public boolean isAutomatic() { - if (getAutoMethod == null) - return false; - - try { - return (Boolean) getAutoMethod.invoke(source); - } catch (IllegalArgumentException e) { - throw new BugException("Method call failed", e); - } catch (IllegalAccessException e) { - throw new BugException("Method call failed", e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - /** - * Sets whether the value should be set automatically. Simply fires a - * state change event if automatic setting is not available. - */ - public void setAutomatic(boolean auto) { - checkState(true); - - if (setAutoMethod == null) { - log.debug("Setting automatic to " + auto + " for " + this + ", automatic not available"); - fireStateChanged(); // in case something is out-of-sync - return; - } - - log.debug("Setting automatic to " + auto + " for " + this); - lastAutomatic = auto; - try { - setAutoMethod.invoke(source, auto); - } catch (IllegalArgumentException e) { - throw new BugException(e); - } catch (IllegalAccessException e) { - throw new BugException(e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - - /** - * Returns the current Unit. At the beginning it is the default unit of the UnitGroup. - * @return The most recently set unit. - */ - public Unit getCurrentUnit() { - return currentUnit; - } - - /** - * Sets the current Unit. The unit must be one of those included in the UnitGroup. - * @param u The unit to set active. - */ - public void setCurrentUnit(Unit u) { - checkState(true); - if (currentUnit == u) - return; - log.debug("Setting unit for " + this + " to '" + u + "'"); - currentUnit = u; - fireStateChanged(); - } - - - /** - * Returns the UnitGroup associated with the parameter value. - * - * @return The UnitGroup given to the constructor. - */ - public UnitGroup getUnitGroup() { - return units; - } - - - - /** - * Add a listener to the model. Adds the model as a listener to the value source if this - * is the first listener. - * @param l Listener to add. - */ - @Override - public void addChangeListener(EventListener l) { - checkState(true); - - if (listeners.isEmpty()) { - if (source != null) { - source.addChangeListener(this); - lastValue = getValue(); - lastAutomatic = isAutomatic(); - } - } - - listeners.add(l); - log.verbose(this + " adding listener (total " + listeners.size() + "): " + l); - } - - /** - * Remove a listener from the model. Removes the model from being a listener to the Component - * if this was the last listener of the model. - * @param l Listener to remove. - */ - @Override - public void removeChangeListener(EventListener l) { - checkState(false); - - listeners.remove(l); - if (listeners.isEmpty() && source != null) { - source.removeChangeListener(this); - } - log.verbose(this + " removing listener (total " + listeners.size() + "): " + l); - } - - - /** - * Invalidates this model by removing all listeners and removing this from - * listening to the source. After invalidation no listeners can be added to this - * model and the value cannot be set. - */ - @Override - public void invalidate() { - log.verbose("Invalidating " + this); - invalidator.invalidate(); - - if (!listeners.isEmpty()) { - log.warn("Invalidating " + this + " while still having listeners " + listeners); - } - listeners.clear(); - if (source != null) { - source.removeChangeListener(this); - } - MemoryManagement.collectable(this); - } - - - private void checkState(boolean error) { - invalidator.check(error); - } - - - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (!listeners.isEmpty()) { - log.warn(this + " being garbage-collected while having listeners " + listeners); - } - }; - - - /** - * Fire a ChangeEvent to all listeners. - */ - protected void fireStateChanged() { - checkState(true); - - EventObject event = new EventObject(this); - ChangeEvent cevent = new ChangeEvent(this); - firing++; - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] ls = listeners.toArray(new EventListener[0]); - for (EventListener l : ls) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(event); - } else if ( l instanceof ChangeListener ) { - ((ChangeListener)l).stateChanged(cevent); - } - } - firing--; - } - - /** - * Called when the component changes. Checks whether the modeled value has changed, and if - * it has, updates lastValue and generates ChangeEvents for all listeners of the model. - */ - @Override - public void stateChanged(EventObject e) { - checkState(true); - - double v = getValue(); - boolean b = isAutomatic(); - if (lastValue == v && lastAutomatic == b) - return; - lastValue = v; - lastAutomatic = b; - fireStateChanged(); - } - - - /** - * Explain the DoubleModel as a String. - */ - @Override - public String toString() { - if (toString == null) { - if (source == null) { - toString = "DoubleModel[constant=" + lastValue + "]"; - } else { - toString = "DoubleModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; - } - } - return toString; - } -} diff --git a/src/net/sf/openrocket/gui/adaptors/EnumModel.java b/src/net/sf/openrocket/gui/adaptors/EnumModel.java deleted file mode 100644 index a6757a38..00000000 --- a/src/net/sf/openrocket/gui/adaptors/EnumModel.java +++ /dev/null @@ -1,133 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - -import java.util.EventObject; - -import javax.swing.AbstractListModel; -import javax.swing.ComboBoxModel; - -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.StateChangeListener; - - -public class EnumModel<T extends Enum<T>> extends AbstractListModel - implements ComboBoxModel, StateChangeListener { - - private final ChangeSource source; - private final String valueName; - private final String nullText; - - private final Enum<T>[] values; - private Enum<T> currentValue = null; - - private final Reflection.Method getMethod; - private final Reflection.Method setMethod; - - - - public EnumModel(ChangeSource source, String valueName) { - this(source,valueName,null,null); - } - - public EnumModel(ChangeSource source, String valueName, Enum<T>[] values) { - this(source, valueName, values, null); - } - - @SuppressWarnings("unchecked") - public EnumModel(ChangeSource source, String valueName, Enum<T>[] values, String nullText) { - Class<? extends Enum<T>> enumClass; - this.source = source; - this.valueName = valueName; - - try { - java.lang.reflect.Method getM = source.getClass().getMethod("get" + valueName); - enumClass = (Class<? extends Enum<T>>) getM.getReturnType(); - if (!enumClass.isEnum()) { - throw new IllegalArgumentException("Return type of get" + valueName + - " not an enum type"); - } - - getMethod = new Reflection.Method(getM); - setMethod = new Reflection.Method(source.getClass().getMethod("set" + valueName, - enumClass)); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("get/is methods for enum '"+valueName+ - "' not present in class "+source.getClass().getCanonicalName()); - } - - if (values != null) - this.values = values; - else - this.values = enumClass.getEnumConstants(); - - this.nullText = nullText; - - stateChanged(null); // Update current value - source.addChangeListener(this); - } - - - - @Override - public Object getSelectedItem() { - if (currentValue==null) - return nullText; - return currentValue; - } - - @Override - public void setSelectedItem(Object item) { - if (item == null) { - // Clear selection - huh? - return; - } - if (item instanceof String) { - if (currentValue != null) - setMethod.invoke(source, (Object)null); - return; - } - - if (!(item instanceof Enum<?>)) { - throw new IllegalArgumentException("Not String or Enum, item="+item); - } - - // Comparison with == ok, since both are enums - if (currentValue == item) - return; - setMethod.invoke(source, item); - } - - @Override - public Object getElementAt(int index) { - if (values[index] == null) - return nullText; - return values[index]; - } - - @Override - public int getSize() { - return values.length; - } - - - - - - @SuppressWarnings("unchecked") - @Override - public void stateChanged(EventObject e) { - Enum<T> value = (Enum<T>) getMethod.invoke(source); - if (value != currentValue) { - currentValue = value; - this.fireContentsChanged(this, 0, values.length); - } - } - - - - @Override - public String toString() { - return "EnumModel["+source.getClass().getCanonicalName()+":"+valueName+"]"; - } - -} diff --git a/src/net/sf/openrocket/gui/adaptors/IntegerModel.java b/src/net/sf/openrocket/gui/adaptors/IntegerModel.java deleted file mode 100644 index e948918a..00000000 --- a/src/net/sf/openrocket/gui/adaptors/IntegerModel.java +++ /dev/null @@ -1,262 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.EventListener; -import java.util.EventObject; - -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.StateChangeListener; - - -public class IntegerModel implements StateChangeListener { - private static final LogHelper log = Application.getLogger(); - - - //////////// JSpinner Model //////////// - - private class IntegerSpinnerModel extends SpinnerNumberModel { - @Override - public Object getValue() { - return IntegerModel.this.getValue(); - } - - @Override - public void setValue(Object value) { - if (firing > 0) { - // Ignore, if called when model is sending events - log.verbose("Ignoring call to SpinnerModel setValue for " + IntegerModel.this.toString() + - " value=" + value + ", currently firing events"); - return; - - } - Number num = (Number) value; - int newValue = num.intValue(); - log.user("SpinnerModel setValue called for " + IntegerModel.this.toString() + " newValue=" + newValue); - IntegerModel.this.setValue(newValue); - } - - @Override - public Object getNextValue() { - int d = IntegerModel.this.getValue(); - if (d >= maxValue) - return null; - return (d + 1); - } - - @Override - public Object getPreviousValue() { - int d = IntegerModel.this.getValue(); - if (d <= minValue) - return null; - return (d - 1); - } - - @Override - public void addChangeListener(ChangeListener l) { - IntegerModel.this.addChangeListener(l); - } - - @Override - public void removeChangeListener(ChangeListener l) { - IntegerModel.this.removeChangeListener(l); - } - } - - /** - * Returns a new SpinnerModel with the same base as the DoubleModel. - * The values given to the JSpinner are in the currently selected units. - * - * @return A compatibility layer for a SpinnerModel. - */ - public SpinnerModel getSpinnerModel() { - return new IntegerSpinnerModel(); - } - - - - - //////////// Main model ///////////// - - /* - * The main model handles all values in SI units, i.e. no conversion is made within the model. - */ - - private final ChangeSource source; - private final String valueName; - - private final Method getMethod; - private final Method setMethod; - - private final ArrayList<EventListener> listeners = new ArrayList<EventListener>(); - - private final int minValue; - private final int maxValue; - - private String toString = null; - - - private int firing = 0; // >0 when model itself is sending events - - - // Used to differentiate changes in valueName and other changes in the source: - private int lastValue = 0; - - - - /** - * Generates a new DoubleModel that changes the values of the specified source. - * The double value is read and written using the methods "get"/"set" + valueName. - * - * @param source Component whose parameter to use. - * @param valueName Name of methods used to get/set the parameter. - * @param min Minimum value allowed (in SI units) - * @param max Maximum value allowed (in SI units) - */ - public IntegerModel(ChangeSource source, String valueName, int min, int max) { - this.source = source; - this.valueName = valueName; - - this.minValue = min; - this.maxValue = max; - - try { - getMethod = source.getClass().getMethod("get" + valueName); - setMethod = source.getClass().getMethod("set" + valueName, int.class); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("get/set methods for value '" + valueName + - "' not present in class " + source.getClass().getCanonicalName()); - } - } - - public IntegerModel(ChangeSource source, String valueName, int min) { - this(source, valueName, min, Integer.MAX_VALUE); - } - - public IntegerModel(ChangeSource source, String valueName) { - this(source, valueName, Integer.MIN_VALUE, Integer.MAX_VALUE); - } - - - - - /** - * Returns the value of the variable. - */ - public int getValue() { - try { - return (Integer) getMethod.invoke(source); - } catch (IllegalArgumentException e) { - throw new BugException(e); - } catch (IllegalAccessException e) { - throw new BugException(e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - /** - * Sets the value of the variable. - */ - public void setValue(int v) { - log.debug("Setting value " + v + " for " + this); - try { - setMethod.invoke(source, v); - } catch (IllegalArgumentException e) { - throw new BugException(e); - } catch (IllegalAccessException e) { - throw new BugException(e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - - /** - * Add a listener to the model. Adds the model as a listener to the Component if this - * is the first listener. - * @param l Listener to add. - */ - public void addChangeListener(EventListener l) { - if (listeners.isEmpty()) { - source.addChangeListener(this); - lastValue = getValue(); - } - - listeners.add(l); - log.verbose(this + " adding listener (total " + listeners.size() + "): " + l); - } - - /** - * Remove a listener from the model. Removes the model from being a listener to the Component - * if this was the last listener of the model. - * @param l Listener to remove. - */ - public void removeChangeListener(ChangeListener l) { - listeners.remove(l); - if (listeners.isEmpty()) { - source.removeChangeListener(this); - } - log.verbose(this + " removing listener (total " + listeners.size() + "): " + l); - } - - - @Override - protected void finalize() throws Throwable { - super.finalize(); - if (!listeners.isEmpty()) { - log.warn(this + " being garbage-collected while having listeners " + listeners); - } - }; - - - public void fireStateChanged() { - EventListener[] list = listeners.toArray(new EventListener[0] ); - EventObject event = new EventObject(this); - ChangeEvent cevent = new ChangeEvent(this); - firing++; - for( EventListener l : list ) { - if ( l instanceof ChangeListener) { - ((ChangeListener)l).stateChanged(cevent); - } else if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(event); - } - } - firing--; - } - - /** - * Called when the source changes. Checks whether the modeled value has changed, and if - * it has, updates lastValue and generates ChangeEvents for all listeners of the model. - */ - @Override - public void stateChanged(EventObject e) { - int v = getValue(); - if (lastValue == v) - return; - lastValue = v; - fireStateChanged(); - } - - /** - * Explain the DoubleModel as a String. - */ - @Override - public String toString() { - if (toString == null) { - toString = "IntegerModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; - } - return toString; - } - -} diff --git a/src/net/sf/openrocket/gui/adaptors/MaterialModel.java b/src/net/sf/openrocket/gui/adaptors/MaterialModel.java deleted file mode 100644 index a914df00..00000000 --- a/src/net/sf/openrocket/gui/adaptors/MaterialModel.java +++ /dev/null @@ -1,165 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - - -import java.awt.Component; - -import javax.swing.AbstractListModel; -import javax.swing.ComboBoxModel; -import javax.swing.SwingUtilities; - -import net.sf.openrocket.database.Database; -import net.sf.openrocket.database.DatabaseListener; -import net.sf.openrocket.database.Databases; -import net.sf.openrocket.gui.dialogs.CustomMaterialDialog; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Reflection; - -public class MaterialModel extends AbstractListModel implements - ComboBoxModel, ComponentChangeListener, DatabaseListener<Material> { - - private static final String CUSTOM = "Custom"; - - - private final Component parentComponent; - - private final RocketComponent component; - private final Material.Type type; - private final Database<Material> database; - - private final Reflection.Method getMethod; - private final Reflection.Method setMethod; - private static final Translator trans = Application.getTranslator(); - - - public MaterialModel(Component parent, RocketComponent component, Material.Type type) { - //// Material - //this(parent, component, type, trans.get("MaterialModel.title.Material")); - this(parent, component, type, "Material"); - } - - public MaterialModel(Component parent, RocketComponent component, Material.Type type, - String name) { - this.parentComponent = parent; - this.component = component; - this.type = type; - - switch (type) { - case LINE: - this.database = Databases.LINE_MATERIAL; - break; - - case BULK: - this.database = Databases.BULK_MATERIAL; - break; - - case SURFACE: - this.database = Databases.SURFACE_MATERIAL; - break; - - default: - throw new IllegalArgumentException("Unknown material type:"+type); - } - - try { - getMethod = new Reflection.Method(component.getClass().getMethod("get"+name)); - setMethod = new Reflection.Method(component.getClass().getMethod("set"+name, - Material.class)); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("get/is methods for material " + - "not present in class "+component.getClass().getCanonicalName()); - } - - component.addComponentChangeListener(this); - database.addDatabaseListener(this); - } - - @Override - public Object getSelectedItem() { - return getMethod.invoke(component); - } - - @Override - public void setSelectedItem(Object item) { - if (item == null) { - // Clear selection - huh? - return; - } - - if (item == CUSTOM) { - - // Open custom material dialog in the future, after combo box has closed - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - CustomMaterialDialog dialog = new CustomMaterialDialog( - SwingUtilities.getWindowAncestor(parentComponent), - (Material) getSelectedItem(), true, - //// Define custom material - trans.get("MaterialModel.title.Defcustmat")); - - dialog.setVisible(true); - - if (!dialog.getOkClicked()) - return; - - Material material = dialog.getMaterial(); - setMethod.invoke(component, material); - - if (dialog.isAddSelected()) { - database.add(material); - } - } - }); - - } else if (item instanceof Material) { - - setMethod.invoke(component, item); - - } else { - throw new IllegalArgumentException("Illegal item class " + item.getClass() + - " item=" + item); - } - } - - @Override - public Object getElementAt(int index) { - if (index == database.size()) { - return CUSTOM; - } else if (index >= database.size()+1) { - return null; - } - return database.get(index); - } - - @Override - public int getSize() { - return database.size() + 1; - } - - - - //////// Change listeners - - @Override - public void componentChanged(ComponentChangeEvent e) { - if (((ComponentChangeEvent)e).isMassChange()) { - this.fireContentsChanged(this, 0, 0); - } - } - - @Override - public void elementAdded(Material element, Database<Material> source) { - this.fireContentsChanged(this, 0, database.size()); - } - - @Override - public void elementRemoved(Material element, Database<Material> source) { - this.fireContentsChanged(this, 0, database.size()); - } - -} diff --git a/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java b/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java deleted file mode 100644 index 57852699..00000000 --- a/src/net/sf/openrocket/gui/adaptors/MotorConfigurationModel.java +++ /dev/null @@ -1,170 +0,0 @@ -package net.sf.openrocket.gui.adaptors; - - -import java.util.EventObject; -import java.util.HashMap; -import java.util.Map; - -import javax.swing.ComboBoxModel; -import javax.swing.SwingUtilities; -import javax.swing.event.EventListenerList; -import javax.swing.event.ListDataEvent; -import javax.swing.event.ListDataListener; - -import net.sf.openrocket.gui.dialogs.EditMotorConfigurationDialog; -import net.sf.openrocket.gui.main.BasicFrame; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.StateChangeListener; - -public class MotorConfigurationModel implements ComboBoxModel, StateChangeListener { - private static final Translator trans = Application.getTranslator(); - - private static final String EDIT = trans.get("MotorCfgModel.Editcfg"); - - - private EventListenerList listenerList = new EventListenerList(); - - private final Configuration config; - private final Rocket rocket; - - private Map<String, ID> map = new HashMap<String, ID>(); - - - public MotorConfigurationModel(Configuration config) { - this.config = config; - this.rocket = config.getRocket(); - config.addChangeListener(this); - } - - - - @Override - public Object getElementAt(int index) { - String[] ids = rocket.getMotorConfigurationIDs(); - if (index < 0 || index > ids.length) - return null; - - if (index == ids.length) - return EDIT; - - return get(ids[index]); - } - - @Override - public int getSize() { - return rocket.getMotorConfigurationIDs().length + 1; - } - - @Override - public Object getSelectedItem() { - return get(config.getMotorConfigurationID()); - } - - @Override - public void setSelectedItem(Object item) { - if (item == null) { - // Clear selection - huh? - return; - } - if (item == EDIT) { - - // Open edit dialog in the future, after combo box has closed - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - new EditMotorConfigurationDialog(rocket, BasicFrame.findFrame(rocket)) - .setVisible(true); - } - }); - - return; - } - if (!(item instanceof ID)) { - throw new IllegalArgumentException("MotorConfigurationModel item="+item); - } - - ID idObject = (ID) item; - config.setMotorConfigurationID(idObject.getID()); - } - - - - //////////////// Event/listener handling //////////////// - - - @Override - public void addListDataListener(ListDataListener l) { - listenerList.add(ListDataListener.class, l); - } - - @Override - public void removeListDataListener(ListDataListener l) { - listenerList.remove(ListDataListener.class, l); - } - - protected void fireListDataEvent() { - Object[] listeners = listenerList.getListenerList(); - ListDataEvent e = null; - - for (int i = listeners.length-2; i>=0; i-=2) { - if (listeners[i] == ListDataListener.class) { - if (e == null) - e = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize()); - ((ListDataListener) listeners[i+1]).contentsChanged(e); - } - } - } - - - @Override - public void stateChanged(EventObject e) { - if (e instanceof ComponentChangeEvent) { - // Ignore unnecessary changes - if (!((ComponentChangeEvent)e).isMotorChange()) - return; - } - fireListDataEvent(); - } - - - - /* - * The ID class is an adapter, that contains the actual configuration ID, - * but gives the configuration description as its String representation. - * The get(id) method retrieves ID objects and caches them for reuse. - */ - - private ID get(String id) { - ID idObject = map.get(id); - if (idObject != null) - return idObject; - - idObject = new ID(id); - map.put(id, idObject); - return idObject; - } - - - private class ID { - private final String id; - - public ID(String id) { - this.id = id; - } - - public String getID() { - return id; - } - - @Override - public String toString() { - return rocket.getMotorConfigurationNameOrDescription(id); - } - } - -} - diff --git a/src/net/sf/openrocket/gui/components/BasicSlider.java b/src/net/sf/openrocket/gui/components/BasicSlider.java deleted file mode 100644 index 0ac92e79..00000000 --- a/src/net/sf/openrocket/gui/components/BasicSlider.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.sf.openrocket.gui.components; - -import javax.swing.BoundedRangeModel; -import javax.swing.JSlider; -import javax.swing.plaf.basic.BasicSliderUI; - -/** - * A simple slider that does not show the current value. GTK l&f shows the value, and cannot - * be configured otherwise(!). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class BasicSlider extends JSlider { - - public BasicSlider(BoundedRangeModel brm) { - this(brm,JSlider.HORIZONTAL,false); - } - - public BasicSlider(BoundedRangeModel brm, int orientation) { - this(brm,orientation,false); - } - - public BasicSlider(BoundedRangeModel brm, int orientation, boolean inverted) { - super(brm); - setOrientation(orientation); - setInverted(inverted); - setUI(new BasicSliderUI(this)); - } - -} diff --git a/src/net/sf/openrocket/gui/components/BasicTree.java b/src/net/sf/openrocket/gui/components/BasicTree.java deleted file mode 100644 index 6ec8804e..00000000 --- a/src/net/sf/openrocket/gui/components/BasicTree.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Component; -import java.awt.Graphics; -import java.awt.Graphics2D; - -import javax.swing.Icon; -import javax.swing.JTree; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreeNode; -import javax.swing.tree.TreePath; - -public class BasicTree extends JTree { - - - public BasicTree() { - super(); - setDefaultOptions(); - } - - public BasicTree(TreeNode node) { - super(node); - setDefaultOptions(); - } - - - private void setDefaultOptions() { - this.setToggleClickCount(0); - - javax.swing.plaf.basic.BasicTreeUI plainUI = new javax.swing.plaf.basic.BasicTreeUI(); - this.setUI(plainUI); - plainUI.setExpandedIcon(TreeIcon.MINUS); - plainUI.setCollapsedIcon(TreeIcon.PLUS); - plainUI.setLeftChildIndent(15); - - - this.setBackground(Color.WHITE); - this.setShowsRootHandles(false); - } - - - /** - * Expand the entire tree structure. All nodes will be visible after the call. - */ - public void expandTree() { - for (int i = 0; i < getRowCount(); i++) - expandRow(i); - } - - - - - @Override - public void treeDidChange() { - super.treeDidChange(); - /* - * Expand the childless nodes to prevent leaf nodes from looking expandable. - */ - expandChildlessNodes(); - } - - /** - * Expand all nodes in the tree that are visible and have no children. This can be used - * to avoid the situation where a non-leaf node is marked as being expandable, but when - * expanding it it has no children. - */ - private void expandChildlessNodes() { - TreeModel model = this.getModel(); - if (model == null) { - return; - } - Object root = model.getRoot(); - expandChildlessNodes(model, new TreePath(root)); - } - - private void expandChildlessNodes(TreeModel model, TreePath path) { - Object object = path.getLastPathComponent(); - if (this.isVisible(path)) { - int count = model.getChildCount(object); - if (count == 0) { - this.expandPath(path); - } - for (int i = 0; i < count; i++) { - expandChildlessNodes(model, path.pathByAddingChild(model.getChild(object, i))); - } - } - } - - - - /** - * Plain-looking tree expand/collapse icons. - */ - private static class TreeIcon implements Icon { - public static final Icon PLUS = new TreeIcon(true); - public static final Icon MINUS = new TreeIcon(false); - - // Implementation: - - private final static int width = 9; - private final static int height = 9; - private final static BasicStroke stroke = new BasicStroke(2); - private boolean plus; - - private TreeIcon(boolean plus) { - this.plus = plus; - } - - @Override - public void paintIcon(Component c, Graphics g, int x, int y) { - Graphics2D g2 = (Graphics2D) g.create(); - - // Background - g2.setColor(Color.WHITE); - g2.fillRect(x, y, width, height); - - // Border - g2.setColor(Color.DARK_GRAY); - g2.drawRect(x, y, width, height); - - // Horizontal stroke - g2.setStroke(stroke); - g2.drawLine(x + 3, y + (height + 1) / 2, x + width - 2, y + (height + 1) / 2); - - // Vertical stroke - if (plus) { - g2.drawLine(x + (width + 1) / 2, y + 3, x + (width + 1) / 2, y + height - 2); - } - - g2.dispose(); - } - - @Override - public int getIconWidth() { - return width; - } - - @Override - public int getIconHeight() { - return height; - } - } -} diff --git a/src/net/sf/openrocket/gui/components/CollectionTable.java b/src/net/sf/openrocket/gui/components/CollectionTable.java deleted file mode 100644 index ef14dc61..00000000 --- a/src/net/sf/openrocket/gui/components/CollectionTable.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.sf.openrocket.gui.components; - -import javax.swing.JTable; -import javax.swing.table.AbstractTableModel; - - -/* - * TODO: LOW: This is currently unused. - */ -public abstract class CollectionTable<T> extends JTable { - - private final String[] columnNames; - private CollectionTableModel model; - - - protected CollectionTable(String[] columnNames) { - this.columnNames = columnNames.clone(); - } - - - protected void initializeTable() { - model = new CollectionTableModel(); - this.setModel(model); - } - - - /** - * Retrieve the object for the specified row number. - * - * @param row the row number being queried. - * @return the object at that row. - */ - protected abstract T getModelObjectAt(int row); - - protected abstract int getModelRowCount(); - - - - protected abstract Object getViewForModelObject(T object, int column); - - protected Class<?> getViewColumnClass(int column) { - return Object.class; - } - - - - private class CollectionTableModel extends AbstractTableModel { - @Override - public int getColumnCount() { - return columnNames.length; - } - - @Override - public String getColumnName(int column) { - return columnNames[column]; - } - - @Override - public Class<?> getColumnClass(int column) { - return getViewColumnClass(column); - } - - - @Override - public int getRowCount() { - return getModelRowCount(); - } - - @Override - public Object getValueAt(int row, int column) { - T value = getModelObjectAt(row); - return getViewForModelObject(value, column); - } - } -} diff --git a/src/net/sf/openrocket/gui/components/ColorChooser.java b/src/net/sf/openrocket/gui/components/ColorChooser.java deleted file mode 100644 index e149e3ca..00000000 --- a/src/net/sf/openrocket/gui/components/ColorChooser.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * ColorChooser.java - */ -package net.sf.openrocket.gui.components; - -import javax.swing.JButton; -import javax.swing.JColorChooser; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JTextField; -import javax.swing.SwingUtilities; -import javax.swing.colorchooser.ColorSelectionModel; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import java.awt.Color; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -/** - * A panel implementation of a color chooser. The panel has a label and a textfield. The label identifies - * what the color is to be used for (the purpose), and the textfield is uneditable but has its background set - * to the currently chosen color as a way of visualizing the color. - * - * The chosen color may be retrieved via a call to getCurrentColor. - */ -public class ColorChooser extends JPanel { - - private static final String COLOR_CHOOSER_BUTTON_LABEL = "Color"; - - // The currently selected color - private Color curColor; - - final JColorChooser chooser; - final JLabel label; - final JTextField field; - final JPanel p; - - /** - * Construct a color chooser as a panel. - * l - * @param parent the parent panel to which this component will be added - * @param theChooser the delegated color chooser; the initial color taken from this chooser - * @param theLabel the label used as the 'purpose' of the color; placed next to a textfield - */ - public ColorChooser (JPanel parent, JColorChooser theChooser, final String theLabel) { - p = parent; - chooser = theChooser; - chooser.setPreviewPanel(this); - // Initialize the currently selected color - curColor = chooser.getColor(); - label = new JLabel(theLabel + ":"); - - parent.add(label, "align right"); - field = new JTextField(); - field.setEditable(false); - field.setBackground(curColor); - parent.add(field, "width 50:100:100"); - - final JButton button = new JButton(COLOR_CHOOSER_BUTTON_LABEL); - - ActionListener actionListener = new ActionListener() { - public void actionPerformed (ActionEvent actionEvent) { - chooser.updateUI(); - - final JDialog dialog = JColorChooser.createDialog(null, - theLabel, true, - chooser, - null, null); - - // Wait until current event dispatching completes before showing - // dialog - Runnable showDialog = new Runnable() { - public void run () { - dialog.show(); - } - }; - SwingUtilities.invokeLater(showDialog); - } - }; - button.addActionListener(actionListener); - parent.add(button, "wrap"); - - // Add listener on model to detect changes to selected color - ColorSelectionModel model = chooser.getSelectionModel(); - model.addChangeListener(new ChangeListener() { - public void stateChanged (ChangeEvent evt) { - ColorSelectionModel model = (ColorSelectionModel) evt.getSource(); - // Get the new color value - curColor = model.getSelectedColor(); - field.setBackground(curColor); - } - }); - } - - /** - * Get the user-selected color. - * - * @return the current color - */ - public Color getCurrentColor () { - return curColor; - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/components/ColorChooserButton.java b/src/net/sf/openrocket/gui/components/ColorChooserButton.java deleted file mode 100644 index 15e4adc6..00000000 --- a/src/net/sf/openrocket/gui/components/ColorChooserButton.java +++ /dev/null @@ -1,71 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Color; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.beans.PropertyChangeListener; - -import javax.swing.JButton; -import javax.swing.JColorChooser; -import javax.swing.JDialog; -import javax.swing.JPanel; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * A color chooser button. The currently selected color can be queried or set using the - * {@link #getSelectedColor()} and {@link #setSelectedColor(Color)}, and changes listened - * to by listening to property events with property name {@link #COLOR_KEY}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ColorChooserButton extends JButton { - private static final LogHelper log = Application.getLogger(); - - public static final String COLOR_KEY = "selectedColor"; - - - public ColorChooserButton(Color initial) { - - setSelectedColor(initial); - - // Add action listener that opens color chooser dialog - this.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Activating color chooser"); - final JColorChooser chooser = new JColorChooser(getSelectedColor()); - chooser.setPreviewPanel(new JPanel()); - final JDialog dialog = JColorChooser.createDialog(ColorChooserButton.this, "Select color", true, - chooser, new ActionListener() { - @Override - public void actionPerformed(ActionEvent e2) { - Color c = chooser.getColor(); - log.user("User selected color " + c); - setSelectedColor(chooser.getColor()); - } - }, null); - log.info("Closing color chooser"); - dialog.setVisible(true); - } - }); - - } - - - public void addColorPropertyChangeListener(PropertyChangeListener listener) { - this.addPropertyChangeListener(COLOR_KEY, listener); - } - - public void setSelectedColor(Color c) { - log.debug("Selecting color " + c); - this.setIcon(new ColorIcon(c)); - this.putClientProperty(COLOR_KEY, c); - } - - public Color getSelectedColor() { - return (Color) this.getClientProperty(COLOR_KEY); - } - -} diff --git a/src/net/sf/openrocket/gui/components/ColorIcon.java b/src/net/sf/openrocket/gui/components/ColorIcon.java deleted file mode 100644 index 747a4268..00000000 --- a/src/net/sf/openrocket/gui/components/ColorIcon.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Color; - -import javax.swing.Icon; - -/** - * An Icon that displays a specific color, suitable for drawing into a button. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ColorIcon implements Icon { - private final Color color; - - public ColorIcon(Color c) { - this.color = c; - } - - @Override - public int getIconHeight() { - return 15; - } - - @Override - public int getIconWidth() { - return 25; - } - - @Override - public void paintIcon(java.awt.Component c, java.awt.Graphics g, int x, int y) { - g.setColor(color); - g.fill3DRect(x, y, getIconWidth(), getIconHeight(), false); - } - -} diff --git a/src/net/sf/openrocket/gui/components/CsvOptionPanel.java b/src/net/sf/openrocket/gui/components/CsvOptionPanel.java deleted file mode 100644 index cd979e16..00000000 --- a/src/net/sf/openrocket/gui/components/CsvOptionPanel.java +++ /dev/null @@ -1,126 +0,0 @@ -package net.sf.openrocket.gui.components; - -import javax.swing.BorderFactory; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.Preferences; - -/** - * A panel that shows options for saving CSV files. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class CsvOptionPanel extends JPanel { - - private static final Translator trans = Application.getTranslator(); - - private static final String SPACE = trans.get("CsvOptionPanel.separator.space"); - private static final String TAB = trans.get("CsvOptionPanel.separator.tab"); - - private final String baseClassName; - - private final JComboBox fieldSeparator; - private final JCheckBox[] options; - private final JComboBox commentCharacter; - - /** - * Sole constructor. - * - * @param includeComments a list of comment inclusion options to provide; - * every second item is the option name and every second the tooltip - */ - public CsvOptionPanel(Class<?> baseClass, String... includeComments) { - super(new MigLayout("fill, insets 0")); - - this.baseClassName = baseClass.getSimpleName(); - - JPanel panel; - JLabel label; - String tip; - - - // TODO: HIGH: Rename the translation keys - - // Field separator panel - panel = new JPanel(new MigLayout("fill")); - panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Fieldsep"))); - - label = new JLabel(trans.get("SimExpPan.lbl.Fieldsepstr")); - tip = trans.get("SimExpPan.lbl.longA1") + - trans.get("SimExpPan.lbl.longA2"); - label.setToolTipText(tip); - panel.add(label, "gapright unrel"); - - fieldSeparator = new JComboBox(new String[] { ",", ";", SPACE, TAB }); - fieldSeparator.setEditable(true); - fieldSeparator.setSelectedItem(Application.getPreferences().getString(Preferences.EXPORT_FIELD_SEPARATOR, ",")); - fieldSeparator.setToolTipText(tip); - panel.add(fieldSeparator, "growx"); - - this.add(panel, "growx, wrap unrel"); - - - - // Comments separator panel - panel = new JPanel(new MigLayout("fill")); - panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Comments"))); - - - // List of include comments options - if (includeComments.length % 2 == 1) { - throw new IllegalArgumentException("Invalid argument length, must be even, length=" + includeComments.length); - } - options = new JCheckBox[includeComments.length / 2]; - for (int i = 0; i < includeComments.length / 2; i++) { - options[i] = new JCheckBox(includeComments[i * 2]); - options[i].setToolTipText(includeComments[i * 2 + 1]); - options[i].setSelected(Application.getPreferences().getBoolean("csvOptions." + baseClassName + "." + i, true)); - panel.add(options[i], "wrap"); - } - - - label = new JLabel(trans.get("SimExpPan.lbl.Commentchar")); - tip = trans.get("SimExpPan.lbl.ttip.Commentchar"); - label.setToolTipText(tip); - panel.add(label, "split 2, gapright unrel"); - - commentCharacter = new JComboBox(new String[] { "#", "%", ";" }); - commentCharacter.setEditable(true); - commentCharacter.setSelectedItem(Application.getPreferences().getString(Preferences.EXPORT_COMMENT_CHARACTER, "#")); - commentCharacter.setToolTipText(tip); - panel.add(commentCharacter, "growx"); - - this.add(panel, "growx, wrap"); - } - - - public String getFieldSeparator() { - return fieldSeparator.getSelectedItem().toString(); - } - - public String getCommentCharacter() { - return commentCharacter.getSelectedItem().toString(); - } - - public boolean getSelectionOption(int index) { - return options[index].isSelected(); - } - - /** - * Store the selected options to the user preferences. - */ - public void storePreferences() { - Application.getPreferences().putString(Preferences.EXPORT_FIELD_SEPARATOR, getFieldSeparator()); - Application.getPreferences().putString(Preferences.EXPORT_COMMENT_CHARACTER, getCommentCharacter()); - for (int i = 0; i < options.length; i++) { - Application.getPreferences().putBoolean("csvOptions." + baseClassName + "." + i, options[i].isSelected()); - } - } - -} diff --git a/src/net/sf/openrocket/gui/components/DescriptionArea.java b/src/net/sf/openrocket/gui/components/DescriptionArea.java deleted file mode 100644 index 1e37bb6f..00000000 --- a/src/net/sf/openrocket/gui/components/DescriptionArea.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Rectangle; - -import javax.swing.JEditorPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.ScrollPaneConstants; -import javax.swing.SwingUtilities; - -public class DescriptionArea extends JScrollPane { - - private final JEditorPane editorPane; - - - /** - * Construct a description area with the specified number of rows, default description font size, - * being opaque. - * - * @param rows the number of rows - */ - public DescriptionArea(int rows) { - this("", rows, -1); - } - - /** - * Construct a description area with the specified number of rows and size, being opaque. - * - * @param rows the number of rows. - * @param size the font size difference compared to the default font size. - */ - public DescriptionArea(int rows, float size) { - this("", rows, size); - } - - /** - * Construct an opaque description area with the specified number of rows, size and text, being opaque. - * - * @param text the initial text. - * @param rows the number of rows. - * @param size the font size difference compared to the default font size. - */ - public DescriptionArea(String text, int rows, float size) { - this(text, rows, size, true); - } - - /** - * Constructor with all options. - * - * @param text the text for the description area. - * @param rows the number of rows to set - * @param size the relative font size in points (positive or negative) - * @param opaque if <code>false</code> the background color will be set to the background color - * of a default JPanel (simulation non-opaque) - */ - public DescriptionArea(String text, int rows, float size, boolean opaque) { - super(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - - editorPane = new JEditorPane("text/html", ""); - Font font = editorPane.getFont(); - editorPane.setFont(font.deriveFont(font.getSize2D() + size)); - editorPane.setEditable(false); - - if (!opaque) { - Color bg = new JPanel().getBackground(); - editorPane.setBackground(new Color(bg.getRed(), bg.getGreen(), bg.getBlue())); - this.setOpaque(true); - } - - // Calculate correct height - editorPane.setText("abc"); - Dimension oneline = editorPane.getPreferredSize(); - editorPane.setText("abc<br>def"); - Dimension twolines = editorPane.getPreferredSize(); - editorPane.setText(""); - - int lineheight = twolines.height - oneline.height; - int extraheight = oneline.height - lineheight; - - Dimension dim = editorPane.getPreferredSize(); - dim.height = lineheight * rows + extraheight + 2; - this.setPreferredSize(dim); - - this.setViewportView(editorPane); - this.setText(text); - } - - public void setText(String txt) { - editorPane.setText(txt); - editorPane.revalidate(); - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - editorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1)); - } - - }); - editorPane.scrollRectToVisible(new Rectangle(0, 0, 1, 1)); - } - -} diff --git a/src/net/sf/openrocket/gui/components/DoubleCellEditor.java b/src/net/sf/openrocket/gui/components/DoubleCellEditor.java deleted file mode 100644 index 1602350a..00000000 --- a/src/net/sf/openrocket/gui/components/DoubleCellEditor.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Component; -import java.text.ParseException; - -import javax.swing.AbstractCellEditor; -import javax.swing.JSpinner; -import javax.swing.JTable; -import javax.swing.table.TableCellEditor; - -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; - -public class DoubleCellEditor extends AbstractCellEditor implements TableCellEditor { - - private final JSpinner editor; - private final DoubleModel model; - - public DoubleCellEditor() { - model = new DoubleModel(0); - editor = new JSpinner(model.getSpinnerModel()); - editor.setEditor(new SpinnerEditor(editor)); - // editor.addChangeListener(this); - } - - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, - boolean isSelected, int row, int column) { - - double val = (Double) value; - model.setValue(val); - - return editor; - } - - - @Override - public boolean stopCellEditing() { - try { - editor.commitEdit(); - } catch (ParseException e) { - // Ignore - } - return super.stopCellEditing(); - } - - - @Override - public Object getCellEditorValue() { - return model.getValue(); - } - -} diff --git a/src/net/sf/openrocket/gui/components/FlatButton.java b/src/net/sf/openrocket/gui/components/FlatButton.java deleted file mode 100644 index d4c39ef4..00000000 --- a/src/net/sf/openrocket/gui/components/FlatButton.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - -import javax.swing.Action; -import javax.swing.Icon; -import javax.swing.JButton; - -/** - * A JButton that appears flat until you roll over it. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FlatButton extends JButton { - - public FlatButton() { - super(); - initialize(); - } - - public FlatButton(Icon icon) { - super(icon); - initialize(); - } - - public FlatButton(String text) { - super(text); - initialize(); - } - - public FlatButton(Action a) { - super(a); - initialize(); - } - - public FlatButton(String text, Icon icon) { - super(text, icon); - initialize(); - } - - - private void initialize() { - this.addMouseListener(new MouseAdapter() { - @Override - public void mouseExited(MouseEvent e) { - flatten(); - } - - @Override - public void mouseEntered(MouseEvent e) { - raise(); - } - }); - flatten(); - } - - - private void flatten() { - this.setContentAreaFilled(false); - this.setBorderPainted(false); - } - - private void raise() { - this.setContentAreaFilled(true); - this.setBorderPainted(true); - } - -} diff --git a/src/net/sf/openrocket/gui/components/HtmlLabel.java b/src/net/sf/openrocket/gui/components/HtmlLabel.java deleted file mode 100644 index 59fdbfa7..00000000 --- a/src/net/sf/openrocket/gui/components/HtmlLabel.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Dimension; - -import javax.swing.JLabel; - -/** - * A JLabel that limits the minimum and maximum height of the label to the - * initial preferred height of the label. This is required in labels that use HTML - * since these often cause the panels to expand too much in height. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class HtmlLabel extends JLabel { - - public HtmlLabel() { - super(); - limitSize(); - } - - public HtmlLabel(String text) { - super(text); - limitSize(); - } - - public HtmlLabel(String text, int horizontalAlignment) { - super(text, horizontalAlignment); - limitSize(); - } - - - private void limitSize() { - Dimension dim = this.getPreferredSize(); - this.setMinimumSize(new Dimension(0, dim.height)); - this.setMaximumSize(new Dimension(Integer.MAX_VALUE, dim.height)); - } - -} diff --git a/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java b/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java deleted file mode 100644 index aaf82928..00000000 --- a/src/net/sf/openrocket/gui/components/ImageDisplayComponent.java +++ /dev/null @@ -1,124 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Color; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.image.BufferedImage; -import java.io.File; - -import javax.imageio.ImageIO; -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.SwingUtilities; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.util.MathUtil; - -/** - * Draws a BufferedImage centered and scaled to fit to the component. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ImageDisplayComponent extends JPanel { - - private BufferedImage image; - - public ImageDisplayComponent() { - this(null); - } - - public ImageDisplayComponent(BufferedImage image) { - this.image = image; - } - - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - - if (image == null) { - return; - } - - final int width = Math.max(this.getWidth(), 1); - final int height = Math.max(this.getHeight(), 1); - - final int origWidth = Math.max(image.getWidth(), 1); - final int origHeight = Math.max(image.getHeight(), 1); - - - // Determine scaling factor - double scaleX = ((double) width) / origWidth; - double scaleY = ((double) height) / origHeight; - - double scale = MathUtil.min(scaleX, scaleY); - - if (scale >= 1) { - scale = 1.0; - } - - - // Center in the middle of the component - int finalWidth = (int) Math.round(origWidth * scale); - int finalHeight = (int) Math.round(origHeight * scale); - - int posX = (width - finalWidth) / 2; - int posY = (height - finalHeight) / 2; - - - // Draw the image - int dx1 = posX; - int dy1 = posY; - int dx2 = posX + finalWidth; - int dy2 = posY + finalHeight; - int sx1 = 0; - int sy1 = 0; - int sx2 = origWidth; - int sy2 = origHeight; - - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2.drawImage(image, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, null); - - } - - - public BufferedImage getImage() { - return image; - } - - - public void setImage(BufferedImage image) { - this.image = image; - this.repaint(); - } - - - public static void main(String[] args) throws Exception { - final BufferedImage image = ImageIO.read(new File("test.png")); - - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - - JFrame frame = new JFrame(); - - JPanel panel = new JPanel(new MigLayout("fill")); - panel.setBackground(Color.red); - frame.add(panel); - - ImageDisplayComponent c = new ImageDisplayComponent(image); - panel.add(c, "grow"); - - frame.setSize(500, 500); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setVisible(true); - - } - }); - } - -} diff --git a/src/net/sf/openrocket/gui/components/SelectableLabel.java b/src/net/sf/openrocket/gui/components/SelectableLabel.java deleted file mode 100644 index d4fd622e..00000000 --- a/src/net/sf/openrocket/gui/components/SelectableLabel.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Cursor; -import java.awt.Dimension; - -import javax.swing.JTextField; -import javax.swing.UIManager; -import javax.swing.plaf.basic.BasicTextFieldUI; - -public class SelectableLabel extends JTextField { - - public SelectableLabel() { - this(""); - } - - public SelectableLabel(String text) { - super(text); - - // Set basic UI since GTK l&f doesn't support null border - this.setUI(new BasicTextFieldUI()); - - this.setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR)); - - this.setEditable(false); - this.setBorder(null); - this.setOpaque(true); - if (UIManager.getColor("Label.foreground") != null) - this.setForeground(UIManager.getColor("Label.foreground")); - if (UIManager.getColor("Label.background") != null) - this.setBackground(UIManager.getColor("Label.background")); - if (UIManager.getFont("Label.font") != null) - this.setFont(UIManager.getFont("Label.font")); - - } - - // The default preferred size is slightly too short, causing it to scroll - @Override - public Dimension getPreferredSize() { - Dimension dim = super.getPreferredSize(); - dim.width += 5; - return dim; - } - -} diff --git a/src/net/sf/openrocket/gui/components/SimulationExportPanel.java b/src/net/sf/openrocket/gui/components/SimulationExportPanel.java deleted file mode 100644 index 69bc02f0..00000000 --- a/src/net/sf/openrocket/gui/components/SimulationExportPanel.java +++ /dev/null @@ -1,432 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.util.Arrays; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.SwingUtilities; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.util.FileHelper; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.gui.util.SaveCSVWorker; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; - -public class SimulationExportPanel extends JPanel { - - private static final String SPACE = "SPACE"; - private static final String TAB = "TAB"; - private static final Translator trans = Application.getTranslator(); - - private static final int OPTION_SIMULATION_COMMENTS = 0; - private static final int OPTION_FIELD_DESCRIPTIONS = 1; - private static final int OPTION_FLIGHT_EVENTS = 2; - - private final JTable table; - private final SelectionTableModel tableModel; - private final JLabel selectedCountLabel; - - private final Simulation simulation; - private final FlightDataBranch branch; - - private final boolean[] selected; - private final FlightDataType[] types; - private final Unit[] units; - - private final CsvOptionPanel csvOptions; - - - public SimulationExportPanel(Simulation sim) { - super(new MigLayout("fill, flowy")); - - JPanel panel; - JButton button; - - - this.simulation = sim; - - // TODO: MEDIUM: Only exports primary branch - - final FlightData data = simulation.getSimulatedData(); - - // Check that data exists - if (data == null || data.getBranchCount() == 0 || - data.getBranch(0).getTypes().length == 0) { - throw new IllegalArgumentException("No data for panel"); - } - - - // Create the data model - branch = data.getBranch(0); - - types = branch.getTypes(); - Arrays.sort(types); - - selected = new boolean[types.length]; - units = new Unit[types.length]; - for (int i = 0; i < types.length; i++) { - selected[i] = ((SwingPreferences) Application.getPreferences()).isExportSelected(types[i]); - units[i] = types[i].getUnitGroup().getDefaultUnit(); - } - - - //// Create the panel - - - // Set up the variable selection table - tableModel = new SelectionTableModel(); - table = new JTable(tableModel); - table.setDefaultRenderer(Object.class, - new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Object.class))); - table.setDefaultRenderer(Boolean.class, - new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Boolean.class))); - table.setRowSelectionAllowed(false); - table.setColumnSelectionAllowed(false); - - table.setDefaultEditor(Unit.class, new UnitCellEditor() { - @Override - protected UnitGroup getUnitGroup(Unit value, int row, int column) { - return types[row].getUnitGroup(); - } - }); - - // Set column widths - TableColumnModel columnModel = table.getColumnModel(); - TableColumn col = columnModel.getColumn(0); - int w = table.getRowHeight(); - col.setMinWidth(w); - col.setPreferredWidth(w); - col.setMaxWidth(w); - - col = columnModel.getColumn(1); - col.setPreferredWidth(200); - - col = columnModel.getColumn(2); - col.setPreferredWidth(100); - - table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); - - // Add table - panel = new JPanel(new MigLayout("fill")); - panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Vartoexport"))); - - panel.add(new JScrollPane(table), "wmin 300lp, width 300lp, height 1, grow 100, wrap"); - - // Select all/none buttons - button = new JButton(trans.get("SimExpPan.but.Selectall")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - tableModel.selectAll(); - } - }); - panel.add(button, "split 2, growx 1, sizegroup selectbutton"); - - button = new JButton(trans.get("SimExpPan.but.Selectnone")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - tableModel.selectNone(); - } - }); - panel.add(button, "growx 1, sizegroup selectbutton, wrap"); - - - selectedCountLabel = new JLabel(); - updateSelectedCount(); - panel.add(selectedCountLabel); - - this.add(panel, "grow 100, wrap"); - - - // These need to be in the order of the OPTIONS_XXX indices - csvOptions = new CsvOptionPanel(SimulationExportPanel.class, - trans.get("SimExpPan.checkbox.Includesimudesc"), - trans.get("SimExpPan.checkbox.ttip.Includesimudesc"), - trans.get("SimExpPan.checkbox.Includefielddesc"), - trans.get("SimExpPan.checkbox.ttip.Includefielddesc"), - trans.get("SimExpPan.checkbox.Incflightevents"), - trans.get("SimExpPan.checkbox.ttip.Incflightevents")); - - this.add(csvOptions, "spany, split, growx 1"); - - - // Space-filling panel - panel = new JPanel(); - this.add(panel, "width 1, height 1, grow 1"); - - - // Export button - button = new JButton(trans.get("SimExpPan.but.Exporttofile")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - doExport(); - } - }); - this.add(button, "gapbottom para, gapright para, right"); - - } - - - private void doExport() { - JFileChooser chooser = new JFileChooser(); - chooser.setFileFilter(FileHelper.CSV_FILE_FILTER); - chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); - - if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) - return; - - File file = chooser.getSelectedFile(); - if (file == null) - return; - - file = FileHelper.ensureExtension(file, "csv"); - if (!FileHelper.confirmWrite(file, this)) { - return; - } - - - String commentChar = csvOptions.getCommentCharacter(); - String fieldSep = csvOptions.getFieldSeparator(); - boolean simulationComment = csvOptions.getSelectionOption(OPTION_SIMULATION_COMMENTS); - boolean fieldComment = csvOptions.getSelectionOption(OPTION_FIELD_DESCRIPTIONS); - boolean eventComment = csvOptions.getSelectionOption(OPTION_FLIGHT_EVENTS); - csvOptions.storePreferences(); - - // Store preferences and export - int n = 0; - ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); - for (int i = 0; i < selected.length; i++) { - ((SwingPreferences) Application.getPreferences()).setExportSelected(types[i], selected[i]); - if (selected[i]) - n++; - } - - - FlightDataType[] fieldTypes = new FlightDataType[n]; - Unit[] fieldUnits = new Unit[n]; - int pos = 0; - for (int i = 0; i < selected.length; i++) { - if (selected[i]) { - fieldTypes[pos] = types[i]; - fieldUnits[pos] = units[i]; - pos++; - } - } - - if (fieldSep.equals(SPACE)) { - fieldSep = " "; - } else if (fieldSep.equals(TAB)) { - fieldSep = "\t"; - } - - - SaveCSVWorker.export(file, simulation, branch, fieldTypes, fieldUnits, fieldSep, - commentChar, simulationComment, fieldComment, eventComment, - SwingUtilities.getWindowAncestor(this)); - } - - - private void updateSelectedCount() { - int total = selected.length; - int n = 0; - String str; - - for (int i = 0; i < selected.length; i++) { - if (selected[i]) - n++; - } - - if (n == 1) { - //// Exporting 1 variable out of - str = trans.get("SimExpPan.ExportingVar.desc1") + " " + total + "."; - } else { - //// Exporting - //// variables out of - str = trans.get("SimExpPan.ExportingVar.desc2") + " " + n + " " + - trans.get("SimExpPan.ExportingVar.desc3") + " " + total + "."; - } - - selectedCountLabel.setText(str); - } - - - - /** - * A table cell renderer that uses another renderer and sets the background and - * foreground of the returned component based on the selection of the variable. - */ - private class SelectionBackgroundCellRenderer implements TableCellRenderer { - - private final TableCellRenderer renderer; - - public SelectionBackgroundCellRenderer(TableCellRenderer renderer) { - this.renderer = renderer; - } - - @Override - public Component getTableCellRendererComponent(JTable table, Object value, - boolean isSelected, boolean hasFocus, int row, int column) { - - Component component = renderer.getTableCellRendererComponent(table, - value, isSelected, hasFocus, row, column); - - if (selected[row]) { - component.setBackground(table.getSelectionBackground()); - component.setForeground(table.getSelectionForeground()); - } else { - component.setBackground(table.getBackground()); - component.setForeground(table.getForeground()); - } - - return component; - } - - } - - - /** - * The table model for the variable selection. - */ - private class SelectionTableModel extends AbstractTableModel { - private static final int SELECTED = 0; - private static final int NAME = 1; - private static final int UNIT = 2; - - @Override - public int getColumnCount() { - return 3; - } - - @Override - public int getRowCount() { - return types.length; - } - - @Override - public String getColumnName(int column) { - switch (column) { - case SELECTED: - return ""; - case NAME: - //// Variable - return trans.get("SimExpPan.Col.Variable"); - case UNIT: - //// Unit - return trans.get("SimExpPan.Col.Unit"); - default: - throw new IndexOutOfBoundsException("column=" + column); - } - - } - - @Override - public Class<?> getColumnClass(int column) { - switch (column) { - case SELECTED: - return Boolean.class; - case NAME: - return FlightDataType.class; - case UNIT: - return Unit.class; - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - @Override - public Object getValueAt(int row, int column) { - - switch (column) { - case SELECTED: - return selected[row]; - - case NAME: - return types[row]; - - case UNIT: - return units[row]; - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - - } - - @Override - public void setValueAt(Object value, int row, int column) { - - switch (column) { - case SELECTED: - selected[row] = (Boolean) value; - this.fireTableRowsUpdated(row, row); - updateSelectedCount(); - break; - - case NAME: - break; - - case UNIT: - units[row] = (Unit) value; - break; - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - - } - - @Override - public boolean isCellEditable(int row, int column) { - switch (column) { - case SELECTED: - return true; - - case NAME: - return false; - - case UNIT: - return types[row].getUnitGroup().getUnitCount() > 1; - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - public void selectAll() { - Arrays.fill(selected, true); - updateSelectedCount(); - this.fireTableDataChanged(); - } - - public void selectNone() { - Arrays.fill(selected, false); - updateSelectedCount(); - this.fireTableDataChanged(); - } - - } - -} diff --git a/src/net/sf/openrocket/gui/components/StageSelector.java b/src/net/sf/openrocket/gui/components/StageSelector.java deleted file mode 100644 index 94792791..00000000 --- a/src/net/sf/openrocket/gui/components/StageSelector.java +++ /dev/null @@ -1,111 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.event.ActionEvent; -import java.util.ArrayList; -import java.util.EventObject; -import java.util.List; - -import javax.swing.AbstractAction; -import javax.swing.JPanel; -import javax.swing.JToggleButton; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.StateChangeListener; - - -public class StageSelector extends JPanel implements StateChangeListener { - private static final Translator trans = Application.getTranslator(); - - private final Configuration configuration; - - private List<JToggleButton> buttons = new ArrayList<JToggleButton>(); - - public StageSelector(Configuration configuration) { - super(new MigLayout("gap 0!")); - this.configuration = configuration; - - JToggleButton button = new JToggleButton(new StageAction(0)); - this.add(button); - buttons.add(button); - - updateButtons(); - configuration.addChangeListener(this); - } - - private void updateButtons() { - int stages = configuration.getStageCount(); - if (buttons.size() == stages) - return; - - while (buttons.size() > stages) { - JToggleButton button = buttons.remove(buttons.size() - 1); - this.remove(button); - } - - while (buttons.size() < stages) { - JToggleButton button = new JToggleButton(new StageAction(buttons.size())); - this.add(button); - buttons.add(button); - } - - this.revalidate(); - } - - - - - @Override - public void stateChanged(EventObject e) { - updateButtons(); - } - - - private class StageAction extends AbstractAction implements StateChangeListener { - private final int stage; - - public StageAction(final int stage) { - this.stage = stage; - configuration.addChangeListener(this); - stateChanged(null); - } - - @Override - public Object getValue(String key) { - if (key.equals(NAME)) { - //// Stage - return trans.get("StageAction.Stage") + " " + (stage + 1); - } - return super.getValue(key); - } - - @Override - public void actionPerformed(ActionEvent e) { - configuration.setToStage(stage); - - // boolean state = (Boolean)getValue(SELECTED_KEY); - // if (state == true) { - // // Was disabled, now enabled - // configuration.setToStage(stage); - // } else { - // // Was enabled, check what to do - // if (configuration.isStageActive(stage + 1)) { - // configuration.setToStage(stage); - // } else { - // if (stage == 0) - // configuration.setAllStages(); - // else - // configuration.setToStage(stage-1); - // } - // } - // stateChanged(null); - } - - @Override - public void stateChanged(EventObject e) { - this.putValue(SELECTED_KEY, configuration.isStageActive(stage)); - } - } -} diff --git a/src/net/sf/openrocket/gui/components/StyledLabel.java b/src/net/sf/openrocket/gui/components/StyledLabel.java deleted file mode 100644 index cad8a25a..00000000 --- a/src/net/sf/openrocket/gui/components/StyledLabel.java +++ /dev/null @@ -1,111 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Font; - -import javax.swing.JLabel; -import javax.swing.SwingConstants; - -/** - * A resizeable and styleable JLabel. The method {@link #resizeFont(float)} changes the - * current font size by the given (positive or negative) amount. The change is relative - * to the current font size. The method {@link #setFontStyle(Style)} sets the style - * (bold/italic) of the font. - * <p> - * A nice small text is achievable by <code>new ResizeLabel("My text", -2);</code> - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class StyledLabel extends JLabel { - - public enum Style { - PLAIN(Font.PLAIN), - BOLD(Font.BOLD), - ITALIC(Font.ITALIC), - BOLD_ITALIC(Font.BOLD | Font.ITALIC); - - private int style; - Style(int fontStyle) { - this.style = fontStyle; - } - public int getFontStyle() { - return style; - } - } - - - - public StyledLabel() { - this("", SwingConstants.LEADING, 0f); - } - - public StyledLabel(String text) { - this(text, SwingConstants.LEADING, 0f); - } - - public StyledLabel(float size) { - this("", SwingConstants.LEADING, size); - } - - public StyledLabel(String text, float size) { - this(text, SwingConstants.LEADING, size); - } - - public StyledLabel(String text, int horizontalAlignment, float size) { - super(text, horizontalAlignment); - resizeFont(size); - checkPreferredSize(size, Style.PLAIN); - } - - - - public StyledLabel(Style style) { - this("", SwingConstants.LEADING, 0f, style); - } - - public StyledLabel(String text, Style style) { - this(text, SwingConstants.LEADING, 0f, style); - } - - public StyledLabel(float size, Style style) { - this("", SwingConstants.LEADING, size, style); - } - - public StyledLabel(String text, float size, Style style) { - this(text, SwingConstants.LEADING, size, style); - } - - public StyledLabel(String text, int horizontalAlignment, float size, Style style) { - super(text, horizontalAlignment); - resizeFont(size); - setFontStyle(style); - checkPreferredSize(size, style); - } - - - - - private void checkPreferredSize(float size, Style style) { - String str = this.getText(); - if (str.startsWith("<html>") && str.indexOf("<br") < 0) { - StyledLabel label = new StyledLabel("plaintext", size, style); - label.validate(); - System.out.println("Plain-text label: " + label.getPreferredSize()); - System.out.println("HTML label: " + this.getPreferredSize()); - } - } - - - - public void resizeFont(float size) { - Font font = this.getFont(); - font = font.deriveFont(font.getSize2D()+size); - this.setFont(font); - } - - public void setFontStyle(Style style) { - Font font = this.getFont(); - font = font.deriveFont(style.getFontStyle()); - this.setFont(font); - } -} diff --git a/src/net/sf/openrocket/gui/components/URLLabel.java b/src/net/sf/openrocket/gui/components/URLLabel.java deleted file mode 100644 index b70cd4b1..00000000 --- a/src/net/sf/openrocket/gui/components/URLLabel.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Color; -import java.awt.Cursor; -import java.awt.Desktop; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.font.TextAttribute; -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.Map; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; - -/** - * A label of a URL that is clickable. Clicking the URL will launch the URL in - * the default browser if the Desktop class is supported. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class URLLabel extends SelectableLabel { - private static final LogHelper log = Application.getLogger(); - - /** - * Create a label showing the url it will direct to. - * - * @param url the URL. - */ - public URLLabel(String url) { - this(url, url); - } - - /** - * Create a label with separate URL and label. - * - * @param url the URL clicking will open. - * @param label the label. - */ - public URLLabel(final String url, String label) { - super(); - - setText(label); - - if (Desktop.isDesktopSupported()) { - - // Blue, underlined font - Map<TextAttribute, Object> map = new HashMap<TextAttribute, Object>(); - map.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); - this.setFont(this.getFont().deriveFont(map)); - this.setForeground(Color.BLUE); - - this.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - - - this.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - Desktop d = Desktop.getDesktop(); - try { - d.browse(new URI(url)); - } catch (URISyntaxException e1) { - throw new BugException("Illegal URL: " + url, e1); - } catch (IOException e1) { - log.error("Unable to launch browser: " + e1.getMessage(), e1); - } - } - }); - - } - } -} diff --git a/src/net/sf/openrocket/gui/components/UnitCellEditor.java b/src/net/sf/openrocket/gui/components/UnitCellEditor.java deleted file mode 100644 index 2ff47abc..00000000 --- a/src/net/sf/openrocket/gui/components/UnitCellEditor.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.openrocket.gui.components; - -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.AbstractCellEditor; -import javax.swing.JComboBox; -import javax.swing.JTable; -import javax.swing.table.TableCellEditor; - -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; - - -/** - * A cell editor that returns a combo box containing a selection of units. - * Using classes must implement the {@link #getUnitGroup(Unit, int, int)} method - * to return the appropriate unit group for the selection. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class UnitCellEditor extends AbstractCellEditor - implements TableCellEditor, ActionListener { - - private final JComboBox editor; - - public UnitCellEditor() { - editor = new JComboBox(); - editor.setEditable(false); - editor.addActionListener(this); - } - - - @Override - public Component getTableCellEditorComponent(JTable table, Object value, - boolean isSelected, int row, int column) { - - Unit unit = (Unit) value; - UnitGroup group = getUnitGroup(unit, row, column); - - editor.removeAllItems(); - for (Unit u : group.getUnits()) { - editor.addItem(u); - } - - editor.setSelectedItem(unit); - - return editor; - } - - - @Override - public Object getCellEditorValue() { - return editor.getSelectedItem(); - } - - - - @Override - public void actionPerformed(ActionEvent e) { - // End editing when a value has been selected - this.fireEditingStopped(); - } - - - /** - * Return the unit group corresponding to the specified cell. - * - * @param value the cell's value. - * @param row the cell's row. - * @param column the cell's column. - * @return the unit group of this cell. - */ - protected abstract UnitGroup getUnitGroup(Unit value, int row, int column); - -} diff --git a/src/net/sf/openrocket/gui/components/UnitSelector.java b/src/net/sf/openrocket/gui/components/UnitSelector.java deleted file mode 100644 index 1bdef68e..00000000 --- a/src/net/sf/openrocket/gui/components/UnitSelector.java +++ /dev/null @@ -1,339 +0,0 @@ -package net.sf.openrocket.gui.components; - - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.ItemSelectable; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.util.ArrayList; -import java.util.EventObject; -import java.util.List; - -import javax.swing.Action; -import javax.swing.JMenuItem; -import javax.swing.JPopupMenu; -import javax.swing.border.Border; -import javax.swing.border.CompoundBorder; -import javax.swing.border.EmptyBorder; -import javax.swing.border.LineBorder; - -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.StateChangeListener; - - -/** - * A Swing component that allows one to choose a unit from a UnitGroup within - * a DoubleModel model. The current unit of the model is shown as a JLabel, and - * the unit can be changed by clicking on the label. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class UnitSelector extends StyledLabel implements StateChangeListener, MouseListener, - ItemSelectable { - - private DoubleModel model; - private final Action[] extraActions; - - private UnitGroup unitGroup; - private Unit currentUnit; - - private final boolean showValue; - - private final Border normalBorder; - private final Border withinBorder; - - - private final List<ItemListener> itemListeners = new ArrayList<ItemListener>(); - - - /** - * Common private constructor that sets the values and sets up the borders. - * Either model or group must be null. - * - * @param model - * @param showValue - * @param group - * @param actions - */ - private UnitSelector(DoubleModel model, boolean showValue, UnitGroup group, - Action[] actions) { - super(); - - this.model = model; - this.showValue = showValue; - - if (model != null) { - this.unitGroup = model.getUnitGroup(); - this.currentUnit = model.getCurrentUnit(); - } else if (group != null) { - this.unitGroup = group; - this.currentUnit = group.getDefaultUnit(); - } else { - this.unitGroup = UnitGroup.UNITS_NONE; - this.currentUnit = UnitGroup.UNITS_NONE.getDefaultUnit(); - } - - this.extraActions = actions; - - addMouseListener(this); - - // Define borders to use: - - normalBorder = new CompoundBorder( - new LineBorder(new Color(0f, 0f, 0f, 0.08f), 1), new EmptyBorder(1, 1, 1, - 1)); - withinBorder = new CompoundBorder(new LineBorder(new Color(0f, 0f, 0f, 0.6f)), - new EmptyBorder(1, 1, 1, 1)); - - // Add model listener if showing value - if (showValue) - this.model.addChangeListener(this); - - setBorder(normalBorder); - updateText(); - } - - - - public UnitSelector(DoubleModel model, Action... actions) { - this(model, false, actions); - } - - public UnitSelector(DoubleModel model, boolean showValue, Action... actions) { - this(model, showValue, null, actions); - } - - - public UnitSelector(UnitGroup group, Action... actions) { - this(null, false, group, actions); - } - - - - - /** - * Return the DoubleModel that is backing this selector up, or <code>null</code>. - * Either this method or {@link #getUnitGroup()} always returns <code>null</code>. - * - * @return the DoubleModel being used, or <code>null</code>. - */ - public DoubleModel getModel() { - return model; - } - - - /** - * Set the current double model. - * - * @param model the model to set, <code>null</code> is NOT allowed. - */ - public void setModel(DoubleModel model) { - if (this.model != null && showValue) { - this.model.removeChangeListener(this); - } - this.model = model; - this.unitGroup = model.getUnitGroup(); - this.currentUnit = model.getCurrentUnit(); - if (showValue) { - this.model.addChangeListener(this); - } - updateText(); - } - - - - /** - * Return the unit group that is being shown, or <code>null</code>. Either this method - * or {@link #getModel()} always returns <code>null</code>. - * - * @return the UnitGroup being used, or <code>null</code>. - */ - public UnitGroup getUnitGroup() { - return unitGroup; - } - - - public void setUnitGroup(UnitGroup group) { - if (model != null) { - throw new IllegalStateException( - "UnitGroup cannot be set when backed up with model."); - } - - if (this.unitGroup == group) - return; - - this.unitGroup = group; - this.currentUnit = group.getDefaultUnit(); - updateText(); - } - - - /** - * Return the currently selected unit. Works both when backup up with a DoubleModel - * and UnitGroup. - * - * @return the currently selected unit. - */ - public Unit getSelectedUnit() { - return currentUnit; - } - - - /** - * Set the currently selected unit. Sets it to the DoubleModel if it is backed up - * by it. - * - * @param unit the unit to select. - */ - public void setSelectedUnit(Unit unit) { - if (!unitGroup.contains(unit)) { - throw new IllegalArgumentException("unit " + unit - + " not contained in group " + unitGroup); - } - - this.currentUnit = unit; - if (model != null) { - model.setCurrentUnit(unit); - } - updateText(); - fireItemEvent(); - } - - - - /** - * Updates the text of the label - */ - private void updateText() { - if (model != null) { - - Unit unit = model.getCurrentUnit(); - if (showValue) { - setText(unit.toStringUnit(model.getValue())); - } else { - setText(unit.getUnit()); - } - - } else if (unitGroup != null) { - - setText(currentUnit.getUnit()); - - } else { - throw new IllegalStateException("Both model and unitGroup are null."); - } - } - - - /** - * Update the component when the DoubleModel changes. - */ - @Override - public void stateChanged(EventObject e) { - updateText(); - } - - - - //////// ItemListener handling //////// - - public void addItemListener(ItemListener listener) { - itemListeners.add(listener); - } - - public void removeItemListener(ItemListener listener) { - itemListeners.remove(listener); - } - - protected void fireItemEvent() { - ItemEvent event = null; - ItemListener[] listeners = itemListeners.toArray(new ItemListener[0]); - for (ItemListener l: listeners) { - if (event == null) { - event = new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, getSelectedUnit(), - ItemEvent.SELECTED); - } - l.itemStateChanged(event); - } - } - - - - //////// Popup //////// - - private void popup() { - JPopupMenu popup = new JPopupMenu(); - - for (int i = 0; i < unitGroup.getUnitCount(); i++) { - Unit unit = unitGroup.getUnit(i); - JMenuItem item = new JMenuItem(unit.getUnit()); - item.addActionListener(new UnitSelectorItem(unit)); - popup.add(item); - } - - for (int i = 0; i < extraActions.length; i++) { - if (extraActions[i] == null && i < extraActions.length - 1) { - popup.addSeparator(); - } else { - popup.add(new JMenuItem(extraActions[i])); - } - } - - Dimension d = getSize(); - popup.show(this, 0, d.height); - } - - - /** - * ActionListener class that sets the currently selected unit. - */ - private class UnitSelectorItem implements ActionListener { - private final Unit unit; - - public UnitSelectorItem(Unit u) { - unit = u; - } - - public void actionPerformed(ActionEvent e) { - setSelectedUnit(unit); - } - } - - - @Override - public Object[] getSelectedObjects() { - return new Object[]{ getSelectedUnit() }; - } - - - - //////// Mouse handling //////// - - public void mouseClicked(MouseEvent e) { - if (unitGroup.getUnitCount() > 1) - popup(); - } - - public void mouseEntered(MouseEvent e) { - if (unitGroup.getUnitCount() > 1) - setBorder(withinBorder); - } - - public void mouseExited(MouseEvent e) { - setBorder(normalBorder); - } - - public void mousePressed(MouseEvent e) { - } // Ignore - - public void mouseReleased(MouseEvent e) { - } // Ignore - -} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassPointer.java b/src/net/sf/openrocket/gui/components/compass/CompassPointer.java deleted file mode 100644 index 37fdad42..00000000 --- a/src/net/sf/openrocket/gui/components/compass/CompassPointer.java +++ /dev/null @@ -1,218 +0,0 @@ -package net.sf.openrocket.gui.components.compass; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; - -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.sf.openrocket.gui.Resettable; -import net.sf.openrocket.gui.adaptors.DoubleModel; - -/** - * A component that draws a pointer onto a compass rose. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class CompassPointer extends CompassRose implements Resettable { - - private static final Color PRIMARY_POINTER_COLOR = new Color(1.0f, 0.2f, 0.2f); - private static final Color SECONDARY_POINTER_COLOR = new Color(0.2f, 0.2f, 0.2f, 0.2f); - - private final DoubleModel model; - private final ChangeListener listener; - - protected int width = -1; - protected int mid = -1; - - private DoubleModel secondaryModel; - - private float pointerLength = 0.95f; - private float pointerWidth = 0.1f; - private float pointerArrowWidth = 0.2f; - private boolean pointerArrow = true; - - - - public CompassPointer(DoubleModel model) { - super(); - this.model = model; - listener = new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - CompassPointer.this.repaint(); - } - }; - model.addChangeListener(listener); - } - - - @Override - public void paintComponent(Graphics g) { - super.paintComponent(g); - - Graphics2D g2 = (Graphics2D) g; - - - Dimension dimension = this.getSize(); - - width = Math.min(dimension.width, dimension.height); - mid = width / 2; - width = (int) (getScaler() * width); - - - g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - - if (secondaryModel != null) { - drawArrow(secondaryModel.getValue(), SECONDARY_POINTER_COLOR, g2); - } - drawArrow(model.getValue(), PRIMARY_POINTER_COLOR, g2); - - - } - - - private void drawArrow(double angle, Color color, Graphics2D g2) { - - int pLength = (int) (width * pointerLength / 2); - int pWidth = (int) (width * pointerWidth / 2); - int pArrowWidth = (int) (width * pointerArrowWidth / 2); - - int[] x = new int[8]; - int[] y = new int[8]; - - g2.setColor(color); - - - double sin = Math.sin(angle); - double cos = Math.cos(angle); - - int n = 0; - - // Top part - x[n] = 0; - y[n] = -pLength; - n++; - if (pointerArrow) { - x[n] = -pArrowWidth; - y[n] = -pLength + 2 * pArrowWidth; - n++; - x[n] = -pWidth; - y[n] = -pLength + 2 * pArrowWidth; - n++; - } - - // Bottom part - x[n] = -pWidth; - y[n] = pLength; - n++; - x[n] = 0; - y[n] = pLength - pWidth; - n++; - x[n] = pWidth; - y[n] = pLength; - n++; - - // Top part - if (pointerArrow) { - x[n] = pWidth; - y[n] = -pLength + 2 * pArrowWidth; - n++; - x[n] = pArrowWidth; - y[n] = -pLength + 2 * pArrowWidth; - n++; - } - - // Rotate and shift - for (int i = 0; i < n; i++) { - double x2, y2; - x2 = cos * x[i] - sin * y[i]; - y2 = sin * x[i] + cos * y[i]; - - x[i] = (int) (x2 + mid); - y[i] = (int) (y2 + mid); - } - - g2.fillPolygon(x, y, n); - - g2.setColor(color.darker()); - g2.drawPolygon(x, y, n); - - } - - - public boolean isPointerArrow() { - return pointerArrow; - } - - - public void setPointerArrow(boolean useArrow) { - this.pointerArrow = useArrow; - repaint(); - } - - - public float getPointerLength() { - return pointerLength; - } - - - public void setPointerLength(float pointerLength) { - this.pointerLength = pointerLength; - repaint(); - } - - - public float getPointerWidth() { - return pointerWidth; - } - - - public void setPointerWidth(float pointerWidth) { - this.pointerWidth = pointerWidth; - repaint(); - } - - - public float getPointerArrowWidth() { - return pointerArrowWidth; - } - - - public void setPointerArrowWidth(float pointerArrowWidth) { - this.pointerArrowWidth = pointerArrowWidth; - repaint(); - } - - - - public DoubleModel getSecondaryModel() { - return secondaryModel; - } - - - public void setSecondaryModel(DoubleModel secondaryModel) { - if (this.secondaryModel != null) { - this.secondaryModel.removeChangeListener(listener); - } - this.secondaryModel = secondaryModel; - if (this.secondaryModel != null) { - this.secondaryModel.addChangeListener(listener); - } - } - - - @Override - public void resetModel() { - model.removeChangeListener(listener); - setSecondaryModel(null); - } - - - -} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassRose.java b/src/net/sf/openrocket/gui/components/compass/CompassRose.java deleted file mode 100644 index 49f1d059..00000000 --- a/src/net/sf/openrocket/gui/components/compass/CompassRose.java +++ /dev/null @@ -1,221 +0,0 @@ -package net.sf.openrocket.gui.components.compass; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.font.GlyphVector; -import java.awt.geom.Rectangle2D; - -import javax.swing.JComponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -/** - * A component that draws a compass rose. This class has no other functionality, but superclasses - * may add functionality to it. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class CompassRose extends JComponent { - private static final Translator trans = Application.getTranslator(); - - - private static final Color MAIN_COLOR = new Color(0.4f, 0.4f, 1.0f); - private static final float MAIN_LENGTH = 0.95f; - private static final float MAIN_WIDTH = 0.15f; - - private static final int CIRCLE_BORDER = 2; - private static final Color CIRCLE_HIGHLIGHT = new Color(1.0f, 1.0f, 1.0f, 0.7f); - private static final Color CIRCLE_SHADE = new Color(0.0f, 0.0f, 0.0f, 0.2f); - - private static final Color MARKER_COLOR = Color.BLACK; - - - private double scaler; - - private double markerRadius; - private Font markerFont; - - - /** - * Construct a compass rose with the default settings. - */ - public CompassRose() { - this(0.8, 1.1, Font.decode("Serif-PLAIN-16")); - } - - - /** - * Construct a compass rose with the specified settings. - * - * @param scaler The scaler of the rose. The bordering circle will we this portion of the component dimensions. - * @param markerRadius The radius for the marker positions (N/E/S/W), or NaN for no markers. A value greater than one - * will position the markers outside of the bordering circle. - * @param markerFont The font used for the markers. - */ - public CompassRose(double scaler, double markerRadius, Font markerFont) { - this.scaler = scaler; - this.markerRadius = markerRadius; - this.markerFont = markerFont; - } - - - - @Override - public void paintComponent(Graphics g) { - - Graphics2D g2 = (Graphics2D) g; - - int[] x = new int[3]; - int[] y = new int[3]; - Dimension dimension = this.getSize(); - - int width = Math.min(dimension.width, dimension.height); - int mid = width / 2; - width = (int) (scaler * width); - - int mainLength = (int) (width * MAIN_LENGTH / 2); - int mainWidth = (int) (width * MAIN_WIDTH / 2); - - - g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - g2.setColor(MAIN_COLOR); - - // North - x[0] = mid; - y[0] = mid; - x[1] = mid; - y[1] = mid - mainLength; - x[2] = mid - mainWidth; - y[2] = mid - mainWidth; - g2.fillPolygon(x, y, 3); - - x[2] = mid + mainWidth; - g2.drawPolygon(x, y, 3); - - // East - x[0] = mid; - y[0] = mid; - x[1] = mid + mainLength; - y[1] = mid; - x[2] = mid + mainWidth; - y[2] = mid - mainWidth; - g2.fillPolygon(x, y, 3); - - y[2] = mid + mainWidth; - g2.drawPolygon(x, y, 3); - - // South - x[0] = mid; - y[0] = mid; - x[1] = mid; - y[1] = mid + mainLength; - x[2] = mid + mainWidth; - y[2] = mid + mainWidth; - g2.fillPolygon(x, y, 3); - - x[2] = mid - mainWidth; - g2.drawPolygon(x, y, 3); - - // West - x[0] = mid; - y[0] = mid; - x[1] = mid - mainLength; - y[1] = mid; - x[2] = mid - mainWidth; - y[2] = mid + mainWidth; - g2.fillPolygon(x, y, 3); - - y[2] = mid - mainWidth; - g2.drawPolygon(x, y, 3); - - - // Border circle - g2.setColor(CIRCLE_SHADE); - g2.drawArc(mid - width / 2 + CIRCLE_BORDER, mid - width / 2 + CIRCLE_BORDER, - width - 2 * CIRCLE_BORDER, width - 2 * CIRCLE_BORDER, 45, 180); - g2.setColor(CIRCLE_HIGHLIGHT); - g2.drawArc(mid - width / 2 + CIRCLE_BORDER, mid - width / 2 + CIRCLE_BORDER, - width - 2 * CIRCLE_BORDER, width - 2 * CIRCLE_BORDER, 180 + 45, 180); - - - // Draw direction markers - if (!Double.isNaN(markerRadius) && markerFont != null) { - - int pos = (int) (width * markerRadius / 2); - - g2.setColor(MARKER_COLOR); - drawMarker(g2, mid, mid - pos, trans.get("lbl.north")); - drawMarker(g2, mid + pos, mid, trans.get("lbl.east")); - drawMarker(g2, mid, mid + pos, trans.get("lbl.south")); - drawMarker(g2, mid - pos, mid, trans.get("lbl.west")); - - } - - } - - - - private void drawMarker(Graphics2D g2, float x, float y, String str) { - GlyphVector gv = markerFont.createGlyphVector(g2.getFontRenderContext(), str); - Rectangle2D rect = gv.getVisualBounds(); - - x -= rect.getWidth() / 2; - y += rect.getHeight() / 2; - - g2.drawGlyphVector(gv, x, y); - - } - - - - - - public double getScaler() { - return scaler; - } - - - public void setScaler(double scaler) { - this.scaler = scaler; - repaint(); - } - - - public double getMarkerRadius() { - return markerRadius; - } - - - public void setMarkerRadius(double markerRadius) { - this.markerRadius = markerRadius; - repaint(); - } - - - public Font getMarkerFont() { - return markerFont; - } - - - public void setMarkerFont(Font markerFont) { - this.markerFont = markerFont; - repaint(); - } - - @Override - public Dimension getPreferredSize() { - Dimension dim = super.getPreferredSize(); - int min = Math.min(dim.width, dim.height); - dim.setSize(min, min); - return dim; - } - - -} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java b/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java deleted file mode 100644 index c8b0fee2..00000000 --- a/src/net/sf/openrocket/gui/components/compass/CompassSelectionButton.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.sf.openrocket.gui.components.compass; - -import java.awt.Dimension; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JSpinner; -import javax.swing.border.BevelBorder; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.Resettable; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.FlatButton; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.MathUtil; - - -/** - * A button that displays a current compass direction and opens a popup to edit - * the value when clicked. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class CompassSelectionButton extends FlatButton implements Resettable { - - private static final Translator trans = Application.getTranslator(); - - private static final int POPUP_COMPASS_SIZE = 200; - private static final double SECTOR = 45; - - private static int minWidth = -1; - - - private final DoubleModel model; - private final ChangeListener listener; - - private JPopupMenu popup; - - - public CompassSelectionButton(final DoubleModel model) { - this.model = model; - - JPanel panel = new JPanel(new MigLayout("fill, ins 0")); - panel.setOpaque(false); - - CompassPointer pointer = new CompassPointer(model); - pointer.setPreferredSize(new Dimension(24, 24)); - pointer.setMarkerFont(null); - pointer.setPointerArrow(false); - pointer.setPointerWidth(0.45f); - pointer.setScaler(1.0f); - panel.add(pointer, "gapright rel"); - - - final JLabel label = new JLabel(); - label.setText(getLabel(model.getValue())); - panel.add(label); - - listener = new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - label.setText(getLabel(model.getValue())); - } - }; - model.addChangeListener(listener); - - - if (minWidth < 0) { - calculateMinWidth(); - label.setMinimumSize(new Dimension(minWidth, 0)); - } - - - this.add(panel); - - this.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - openPopup(); - } - }); - } - - - - - private String getLabel(double value) { - String str; - - value = MathUtil.reduce360(value); - value = Math.toDegrees(value); - str = "" + Math.round(value) + Chars.DEGREE + " ("; - - if (value <= 0.5 * SECTOR || value >= 7.5 * SECTOR) { - str += trans.get("lbl.N"); - } else if (value <= 1.5 * SECTOR) { - str += trans.get("lbl.NE"); - } else if (value <= 2.5 * SECTOR) { - str += trans.get("lbl.E"); - } else if (value <= 3.5 * SECTOR) { - str += trans.get("lbl.SE"); - } else if (value <= 4.5 * SECTOR) { - str += trans.get("lbl.S"); - } else if (value <= 5.5 * SECTOR) { - str += trans.get("lbl.SW"); - } else if (value <= 6.5 * SECTOR) { - str += trans.get("lbl.W"); - } else { - str += trans.get("lbl.NW"); - } - - str += ")"; - return str; - } - - - private void openPopup() { - if (popup == null) { - popup = new JPopupMenu(); - - - final JPanel panel = new JPanel(new MigLayout("fill")); - - final CompassPointer rose = new CompassSelector(model); - rose.setPreferredSize(new Dimension(POPUP_COMPASS_SIZE, POPUP_COMPASS_SIZE)); - panel.add(rose, "spany, gapright unrel"); - - panel.add(new JPanel(), "growy, wrap"); - - JSpinner spin = new JSpinner(model.getSpinnerModel()); - panel.add(spin, "wmin 50lp, growx, gapright 0, aligny bottom"); - - panel.add(new JLabel("" + Chars.DEGREE), "wrap para"); - - JButton close = new JButton("OK"); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - popup.setVisible(false); - } - }); - panel.add(close, "span 2, growx, wrap"); - - panel.add(new JPanel(), "growy, wrap"); - - popup.add(panel); - popup.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); - } - - popup.pack(); - - Dimension popupSize = popup.getPreferredSize(); - Dimension buttonSize = this.getSize(); - - int posX = buttonSize.width / 2 - popupSize.width / 2; - int posY = buttonSize.height / 2 - popupSize.height / 2; - popup.show(this, posX, posY); - } - - private void calculateMinWidth() { - JLabel label = new JLabel(); - int max = 0; - for (double deg = 0; deg < 360; deg += 0.99999999999) { - label.setText(getLabel(Math.toRadians(deg))); - int w = label.getPreferredSize().width; - if (w > max) { - max = w; - } - } - minWidth = max + 1; - } - - - - - @Override - public void resetModel() { - model.removeChangeListener(listener); - } - -} diff --git a/src/net/sf/openrocket/gui/components/compass/CompassSelector.java b/src/net/sf/openrocket/gui/components/compass/CompassSelector.java deleted file mode 100644 index deed9cf7..00000000 --- a/src/net/sf/openrocket/gui/components/compass/CompassSelector.java +++ /dev/null @@ -1,97 +0,0 @@ -package net.sf.openrocket.gui.components.compass; - -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.util.MathUtil; - -/** - * Component that allows selecting a compass direction on a CompassSelector. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class CompassSelector extends CompassPointer { - - private final DoubleModel model; - - public CompassSelector(DoubleModel model) { - super(model); - this.model = model; - - MouseAdapter mouse = new MouseAdapter() { - private boolean dragging = false; - - @Override - public void mousePressed(MouseEvent e) { - if (!isWithinCircle(e)) - return; - if (e.getButton() != MouseEvent.BUTTON1) - return; - dragging = true; - clicked(e); - } - - @Override - public void mouseReleased(MouseEvent e) { - if (e.getButton() != MouseEvent.BUTTON1) - return; - dragging = false; - } - - - @Override - public void mouseDragged(MouseEvent e) { - if (!dragging) - return; - clicked(e); - } - }; - this.addMouseListener(mouse); - this.addMouseMotionListener(mouse); - - } - - private boolean isWithinCircle(MouseEvent e) { - if (mid < 0 || width < 0) { - return false; - } - - int x = e.getX() - mid; - int y = e.getY() - mid; - - double distance = Math.hypot(x, y); - return distance < width / 2; - } - - private void clicked(MouseEvent e) { - - if (mid < 0 || width < 0) { - return; - } - - int x = e.getX() - mid; - int y = e.getY() - mid; - - double distance = Math.hypot(x, y); - - double theta = Math.atan2(y, x); - theta = MathUtil.reduce360(theta + Math.PI / 2); - - // Round the value appropriately - theta = Math.toDegrees(theta); - - if (distance > 50) { - theta = Math.round(theta); - } else if (distance > 10) { - theta = 5 * Math.round(theta / 5); - } else { - // Do nothing if too close to center - return; - } - theta = Math.toRadians(theta); - - model.setValue(theta); - } - -} diff --git a/src/net/sf/openrocket/gui/components/compass/Tester.java b/src/net/sf/openrocket/gui/components/compass/Tester.java deleted file mode 100644 index 86a82a49..00000000 --- a/src/net/sf/openrocket/gui/components/compass/Tester.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.sf.openrocket.gui.components.compass; - -import java.awt.Dimension; -import java.lang.reflect.InvocationTargetException; - -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.SwingUtilities; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.ResourceBundleTranslator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class Tester { - - - public static void main(String[] args) throws InterruptedException, InvocationTargetException { - - Application.setBaseTranslator(new ResourceBundleTranslator("l10n.messages")); - - GUIUtil.setBestLAF(); - - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - JFrame frame = new JFrame(); - - JPanel panel = new JPanel(new MigLayout("fill")); - DoubleModel model = new DoubleModel(Math.toRadians(45), UnitGroup.UNITS_ANGLE); - DoubleModel second = new DoubleModel(Math.toRadians(30), UnitGroup.UNITS_ANGLE); - - - CompassPointer rose = new CompassSelector(model); - rose.setPreferredSize(new Dimension(300, 300)); - rose.setSecondaryModel(second); - panel.add(rose); - - rose = new CompassPointer(model); - rose.setPreferredSize(new Dimension(24, 24)); - panel.add(rose); - rose.setMarkerFont(null); - rose.setPointerArrow(false); - rose.setPointerWidth(0.45f); - rose.setScaler(1.0f); - - JSpinner spin = new JSpinner(model.getSpinnerModel()); - spin.setPreferredSize(new Dimension(50, 20)); - panel.add(spin, "wrap para"); - - - CompassSelectionButton button = new CompassSelectionButton(model); - panel.add(button); - - - frame.add(panel); - frame.pack(); - frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - frame.setVisible(true); - } - }); - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java b/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java deleted file mode 100644 index af272a0d..00000000 --- a/src/net/sf/openrocket/gui/configdialog/BodyTubeConfig.java +++ /dev/null @@ -1,119 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JCheckBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.BooleanModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class BodyTubeConfig extends RocketComponentConfig { - - private MotorConfig motorConfigPane = null; - private static final Translator trans = Application.getTranslator(); - - public BodyTubeConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - //// Body tube length - panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Bodytubelength"))); - - DoubleModel m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.5, 2.0)), "w 100lp, wrap"); - - - //// Body tube diameter - panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Outerdiameter"))); - - DoubleModel od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); - - JCheckBox check = new JCheckBox(od.getAutomaticAction()); - //// Automatic - check.setText(trans.get("BodyTubecfg.checkbox.Automatic")); - panel.add(check, "skip, span 2, wrap"); - - - //// Inner diameter - panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Innerdiameter"))); - - // Diameter = 2*Radius - m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); - - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), "w 100lp, wrap"); - - - //// Wall thickness - panel.add(new JLabel(trans.get("BodyTubecfg.lbl.Wallthickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 0px"); - - //// Filled - check = new JCheckBox(new BooleanModel(component, "Filled")); - check.setText(trans.get("BodyTubecfg.checkbox.Filled")); - panel.add(check, "skip, span 2, wrap"); - - - //// Material - panel.add(materialPanel(new JPanel(new MigLayout()), Material.Type.BULK), - "cell 4 0, gapleft paragraph, aligny 0%, spany"); - - //// General and General properties - tabbedPane.insertTab(trans.get("BodyTubecfg.tab.General"), null, panel, - trans.get("BodyTubecfg.tab.Generalproperties"), 0); - motorConfigPane = new MotorConfig((BodyTube) c); - //// Motor and Motor mount configuration - tabbedPane.insertTab(trans.get("BodyTubecfg.tab.Motor"), null, motorConfigPane, - trans.get("BodyTubecfg.tab.Motormountconf"), 1); - tabbedPane.setSelectedIndex(0); - } - - @Override - public void updateFields() { - super.updateFields(); - if (motorConfigPane != null) - motorConfigPane.updateFields(); - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java b/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java deleted file mode 100644 index 135ec94e..00000000 --- a/src/net/sf/openrocket/gui/configdialog/BulkheadConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JPanel; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - - - -public class BulkheadConfig extends RingComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public BulkheadConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel tab; - - tab = generalTab(trans.get("BulkheadCfg.tab.Diameter"), null, null, - trans.get("BulkheadCfg.tab.Thickness")); - //// General and General properties - tabbedPane.insertTab(trans.get("BulkheadCfg.tab.General"), null, tab, - trans.get("BulkheadCfg.tab.Generalproperties"), 0); - tabbedPane.setSelectedIndex(0); - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java b/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java deleted file mode 100644 index dfe6a352..00000000 --- a/src/net/sf/openrocket/gui/configdialog/CenteringRingConfig.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JPanel; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - - - -public class CenteringRingConfig extends RingComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public CenteringRingConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel tab; - - //// Outer diameter: and Inner diameter: and Thickness: - tab = generalTab(trans.get("CenteringRingCfg.tab.Outerdiam"), - trans.get("CenteringRingCfg.tab.Innerdiam"), null, - trans.get("CenteringRingCfg.tab.Thickness")); - //// General and General properties - tabbedPane.insertTab(trans.get("CenteringRingCfg.tab.General"), null, tab, - trans.get("CenteringRingCfg.tab.Generalproperties"), 0); - tabbedPane.setSelectedIndex(0); - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java b/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java deleted file mode 100644 index a669b2dc..00000000 --- a/src/net/sf/openrocket/gui/configdialog/ComponentConfigDialog.java +++ /dev/null @@ -1,219 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.Window; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -import javax.swing.JDialog; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Reflection; - -/** - * A dialog that contains the configuration elements of one component. - * The contents of the dialog are instantiated from CONFIGDIALOGPACKAGE according - * to the current component. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class ComponentConfigDialog extends JDialog implements ComponentChangeListener { - private static final long serialVersionUID = 1L; - private static final String CONFIGDIALOGPACKAGE = "net.sf.openrocket.gui.configdialog"; - private static final String CONFIGDIALOGPOSTFIX = "Config"; - - - private static ComponentConfigDialog dialog = null; - - - private OpenRocketDocument document = null; - private RocketComponent component = null; - private RocketComponentConfig configurator = null; - - private final Window parent; - private static final Translator trans = Application.getTranslator(); - - private ComponentConfigDialog(Window parent, OpenRocketDocument document, - RocketComponent component) { - super(parent); - this.parent = parent; - - setComponent(document, component); - - GUIUtil.setDisposableDialogOptions(this, null); - GUIUtil.rememberWindowPosition(this); - } - - - /** - * Set the component being configured. The listening connections of the old configurator - * will be removed and the new ones created. - * - * @param component Component to configure. - */ - private void setComponent(OpenRocketDocument document, RocketComponent component) { - if (this.document != null) { - this.document.getRocket().removeComponentChangeListener(this); - } - - if (configurator != null) { - // Remove listeners by setting all applicable models to null - GUIUtil.setNullModels(configurator); // null-safe - } - - this.document = document; - this.component = component; - this.document.getRocket().addComponentChangeListener(this); - - configurator = getDialogContents(); - this.setContentPane(configurator); - configurator.updateFields(); - - //// configuration - setTitle(trans.get("ComponentCfgDlg.configuration1") + " " + component.getComponentName() + " " + trans.get("ComponentCfgDlg.configuration")); - - this.pack(); - } - - /** - * Return the configurator panel of the current component. - */ - private RocketComponentConfig getDialogContents() { - Constructor<? extends RocketComponentConfig> c = - findDialogContentsConstructor(component); - if (c != null) { - try { - return c.newInstance(document, component); - } catch (InstantiationException e) { - throw new BugException("BUG in constructor reflection", e); - } catch (IllegalAccessException e) { - throw new BugException("BUG in constructor reflection", e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - // Should never be reached, since RocketComponentConfig should catch all - // components without their own configurator. - throw new BugException("Unable to find any configurator for " + component); - } - - - private void closeDialog() { - this.setVisible(false); - this.dispose(); - this.configurator.invalidateModels(); - } - - - @Override - public void componentChanged(ComponentChangeEvent e) { - if (e.isTreeChange() || e.isUndoChange()) { - - // Hide dialog in case of tree or undo change - dialog.closeDialog(); - - } else { - /* - * TODO: HIGH: The line below has caused a NullPointerException (without null check) - * How is this possible? The null check was added to avoid this, but the - * root cause should be analyzed. - * [Openrocket-bugs] 2009-12-12 19:23:22 Automatic bug report for OpenRocket 0.9.5 - */ - if (configurator != null) - configurator.updateFields(); - } - } - - - /** - * Finds the Constructor of the given component's config dialog panel in - * CONFIGDIALOGPACKAGE. - */ - @SuppressWarnings("unchecked") - private static Constructor<? extends RocketComponentConfig> findDialogContentsConstructor(RocketComponent component) { - Class<?> currentclass; - String currentclassname; - String configclassname; - - Class<?> configclass; - Constructor<? extends RocketComponentConfig> c; - - currentclass = component.getClass(); - while ((currentclass != null) && (currentclass != Object.class)) { - currentclassname = currentclass.getCanonicalName(); - int index = currentclassname.lastIndexOf('.'); - if (index >= 0) - currentclassname = currentclassname.substring(index + 1); - configclassname = CONFIGDIALOGPACKAGE + "." + currentclassname + - CONFIGDIALOGPOSTFIX; - - try { - configclass = Class.forName(configclassname); - c = (Constructor<? extends RocketComponentConfig>) - configclass.getConstructor(OpenRocketDocument.class, RocketComponent.class); - return c; - } catch (Exception ignore) { - } - - currentclass = currentclass.getSuperclass(); - } - return null; - } - - - - - ////////// Static dialog ///////// - - /** - * A singleton configuration dialog. Will create and show a new dialog if one has not - * previously been used, or update the dialog and show it if a previous one exists. - * - * @param document the document to configure. - * @param component the component to configure. - */ - public static void showDialog(Window parent, OpenRocketDocument document, - RocketComponent component) { - if (dialog != null) - dialog.dispose(); - - dialog = new ComponentConfigDialog(parent, document, component); - dialog.setVisible(true); - - ////Modify - document.addUndoPosition(trans.get("ComponentCfgDlg.Modify") + " " + component.getComponentName()); - } - - - /* package */ - static void showDialog(RocketComponent component) { - showDialog(dialog.parent, dialog.document, component); - } - - /** - * Hides the configuration dialog. May be used even if not currently visible. - */ - public static void hideDialog() { - if (dialog != null) { - dialog.closeDialog(); - } - } - - - /** - * Returns whether the singleton configuration dialog is currently visible or not. - */ - public static boolean isDialogVisible() { - return (dialog != null) && (dialog.isVisible()); - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java deleted file mode 100644 index 321ef6c4..00000000 --- a/src/net/sf/openrocket/gui/configdialog/EllipticalFinSetConfig.java +++ /dev/null @@ -1,189 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.SwingConstants; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.IntegerModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class EllipticalFinSetConfig extends FinSetConfig { - private static final Translator trans = Application.getTranslator(); - - public EllipticalFinSetConfig(OpenRocketDocument d, final RocketComponent component) { - super(d, component); - - DoubleModel m; - JSpinner spin; - JComboBox combo; - - JPanel mainPanel = new JPanel(new MigLayout()); - - - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Number of fins - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Nbroffins"))); - - IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); - - spin = new JSpinner(im.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx, wrap"); - - - //// Base rotation - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Rotation"))); - - m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - //// Fin cant - JLabel label = new JLabel(trans.get("EllipticalFinSetCfg.Fincant")); - //// "The angle that the fins are canted with respect to the rocket - label.setToolTipText(trans.get("EllipticalFinSetCfg.ttip.Fincant")); - panel.add(label); - - m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE, - -FinSet.MAX_CANT, FinSet.MAX_CANT); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT, FinSet.MAX_CANT)), - "w 100lp, wrap"); - - - - //// Root chord - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Rootchord"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); - - - //// Height - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Height"))); - - m = new DoubleModel(component, "Height", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); - - - //// Position - //// Position relative to: - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Positionrelativeto"))); - - combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx, growx, wrap"); - - //// plus - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - - - //// Right portion - mainPanel.add(panel, "aligny 20%"); - - mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy"); - - - - panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - - //// Cross section - //// Fin cross section: - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.FincrossSection")), "span, split"); - combo = new JComboBox( - new EnumModel<FinSet.CrossSection>(component, "CrossSection")); - panel.add(combo, "growx, wrap unrel"); - - - //// Thickness: - panel.add(new JLabel(trans.get("EllipticalFinSetCfg.Thickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 30lp"); - - - - //// Material - materialPanel(panel, Material.Type.BULK); - - - - - - mainPanel.add(panel, "aligny 20%"); - - addFinSetButtons(); - - //// General and General properties - tabbedPane.insertTab(trans.get("EllipticalFinSetCfg.General"), null, mainPanel, - trans.get("EllipticalFinSetCfg.Generalproperties"), 0); - tabbedPane.setSelectedIndex(0); - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java deleted file mode 100644 index 67eb4074..00000000 --- a/src/net/sf/openrocket/gui/configdialog/FinSetConfig.java +++ /dev/null @@ -1,468 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.Coaxial; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -import javax.swing.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; - - -public abstract class FinSetConfig extends RocketComponentConfig { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private JButton split = null; - - public FinSetConfig(OpenRocketDocument d, RocketComponent component) { - super(d, component); - - //// Fin tabs and Through-the-wall fin tabs - tabbedPane.insertTab(trans.get("FinSetConfig.tab.Fintabs"), null, finTabPanel(), - trans.get("FinSetConfig.tab.Through-the-wall"), 0); - } - - - protected void addFinSetButtons() { - JButton convert = null; - - //// Convert buttons - if (!(component instanceof FreeformFinSet)) { - //// Convert to freeform - convert = new JButton(trans.get("FinSetConfig.but.Converttofreeform")); - //// Convert this fin set into a freeform fin set - convert.setToolTipText(trans.get("FinSetConfig.but.Converttofreeform.ttip")); - convert.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Converting " + component.getComponentName() + " into freeform fin set"); - - // Do change in future for overall safety - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - //// Convert fin set - document.addUndoPosition(trans.get("FinSetConfig.Convertfinset")); - RocketComponent freeform = - FreeformFinSet.convertFinSet((FinSet) component); - ComponentConfigDialog.showDialog(freeform); - } - }); - - ComponentConfigDialog.hideDialog(); - } - }); - } - - //// Split fins - split = new JButton(trans.get("FinSetConfig.but.Splitfins")); - //// Split the fin set into separate fins - split.setToolTipText(trans.get("FinSetConfig.but.Splitfins.ttip")); - split.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Splitting " + component.getComponentName() + " into separate fins, fin count=" + - ((FinSet) component).getFinCount()); - - // Do change in future for overall safety - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - RocketComponent parent = component.getParent(); - int index = parent.getChildPosition(component); - int count = ((FinSet) component).getFinCount(); - double base = ((FinSet) component).getBaseRotation(); - if (count <= 1) - return; - - document.addUndoPosition("Split fin set"); - parent.removeChild(index); - for (int i = 0; i < count; i++) { - FinSet copy = (FinSet) component.copy(); - copy.setFinCount(1); - copy.setBaseRotation(base + i * 2 * Math.PI / count); - copy.setName(copy.getName() + " #" + (i + 1)); - parent.addChild(copy, index + i); - } - } - }); - - ComponentConfigDialog.hideDialog(); - } - }); - split.setEnabled(((FinSet) component).getFinCount() > 1); - - if (convert == null) - addButtons(split); - else - addButtons(split, convert); - - } - - public JPanel finTabPanel() { - JPanel panel = new JPanel( - new MigLayout("align 50% 20%, fillx, gap rel unrel, ins 20lp 10% 20lp 10%", - "[150lp::][65lp::][30lp::][200lp::]", "")); - // JPanel panel = new JPanel(new MigLayout("fillx, align 20% 20%, gap rel unrel", - // "[40lp][80lp::][30lp::][100lp::]","")); - - //// Through-the-wall fin tabs: - panel.add(new StyledLabel(trans.get("FinSetConfig.lbl.Through-the-wall"), Style.BOLD), - "spanx, wrap 30lp"); - - JLabel label; - DoubleModel length; - DoubleModel length2; - DoubleModel length_2; - JSpinner spin; - JButton autoCalc; - - length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - length2 = new DoubleModel(component, "Length", 0.5, UnitGroup.UNITS_LENGTH, 0); - length_2 = new DoubleModel(component, "Length", -0.5, UnitGroup.UNITS_LENGTH, 0); - - register(length); - register(length2); - register(length_2); - - //// Tab length - //// Tab length: - label = new JLabel(trans.get("FinSetConfig.lbl.Tablength")); - //// The length of the fin tab. - label.setToolTipText(trans.get("FinSetConfig.ttip.Tablength")); - panel.add(label, "gapleft para, gapright 40lp, growx 1"); - - final DoubleModel mtl = new DoubleModel(component, "TabLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(mtl.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx 1"); - - panel.add(new UnitSelector(mtl), "growx 1"); - panel.add(new BasicSlider(mtl.getSliderModel(DoubleModel.ZERO, length)), - "w 100lp, growx 5, wrap"); - - - //// Tab length - //// Tab height: - label = new JLabel(trans.get("FinSetConfig.lbl.Tabheight")); - //// The spanwise height of the fin tab. - label.setToolTipText(trans.get("FinSetConfig.ttip.Tabheight")); - panel.add(label, "gapleft para"); - - final DoubleModel mth = new DoubleModel(component, "TabHeight", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(mth.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(mth), "growx"); - panel.add(new BasicSlider(mth.getSliderModel(DoubleModel.ZERO, length2)), - "w 100lp, growx 5, wrap"); - - //// Tab position: - label = new JLabel(trans.get("FinSetConfig.lbl.Tabposition")); - //// The position of the fin tab. - label.setToolTipText(trans.get("FinSetConfig.ttip.Tabposition")); - panel.add(label, "gapleft para"); - - final DoubleModel mts = new DoubleModel(component, "TabShift", UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(mts.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(mts), "growx"); - panel.add(new BasicSlider(mts.getSliderModel(length_2, length2)), "w 100lp, growx 5, wrap"); - - - //// relative to - label = new JLabel(trans.get("FinSetConfig.lbl.relativeto")); - panel.add(label, "right, gapright unrel"); - - final EnumModel<FinSet.TabRelativePosition> em = - new EnumModel<FinSet.TabRelativePosition>(component, "TabRelativePosition"); - - panel.add(new JComboBox(em), "spanx 3, growx, wrap para"); - - - // Calculate fin tab height, length, and position - autoCalc = new JButton(trans.get("FinSetConfig.but.AutoCalc")); - - autoCalc.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Computing " + component.getComponentName() + " tab height."); - - RocketComponent parent = component.getParent(); - if (parent instanceof Coaxial) { - try { - document.startUndo("Compute fin tabs"); - - List<CenteringRing> rings = new ArrayList<CenteringRing>(); - //Do deep recursive iteration - Iterator<RocketComponent> iter = parent.iterator(false); - while (iter.hasNext()) { - RocketComponent rocketComponent = iter.next(); - if (rocketComponent instanceof InnerTube) { - InnerTube it = (InnerTube) rocketComponent; - if (it.isMotorMount()) { - double depth = ((Coaxial) parent).getOuterRadius() - it.getOuterRadius(); - //Set fin tab depth - if (depth >= 0.0d) { - mth.setValue(depth); - mth.setCurrentUnit(UnitGroup.UNITS_LENGTH.getDefaultUnit()); - } - } - } else if (rocketComponent instanceof CenteringRing) { - rings.add((CenteringRing) rocketComponent); - } - } - //Figure out position and length of the fin tab - if (!rings.isEmpty()) { - FinSet.TabRelativePosition temp = (FinSet.TabRelativePosition) em.getSelectedItem(); - em.setSelectedItem(FinSet.TabRelativePosition.FRONT); - double len = computeFinTabLength(rings, component.asPositionValue(RocketComponent.Position.TOP, parent), - component.getLength(), mts, parent); - mtl.setValue(len); - //Be nice to the user and set the tab relative position enum back the way they had it. - em.setSelectedItem(temp); - } - - } finally { - document.stopUndo(); - } - } - } - }); - panel.add(autoCalc, "skip 1, spanx"); - - return panel; - } - - /** - * Scenarios: - * <p/> - * 1. All rings ahead of start of fin. - * 2. First ring ahead of start of fin. Second ring ahead of end of fin. - * 3. First ring ahead of start of fin. Second ring behind end of fin. - * 4. First ring equal or behind start of fin. Second ring ahead of, or equal to, end of fin. - * 5. First ring equal or behind start of fin. Second ring behind end of fin. - * 6. All rings behind end of fin. - * - * @param rings an unordered list of centering rings attached to the parent of the fin set - * @param finPositionFromTop the position from the top of the parent of the start of the fin set root - * @param finLength the length of the root chord - * @param mts the model for the tab shift (position); the model's value is modified as a result of this method call - * @param relativeTo the parent component of the finset - * - * @return the length of the fin tab - */ - private static double computeFinTabLength(List<CenteringRing> rings, Double finPositionFromTop, Double finLength, DoubleModel mts, - final RocketComponent relativeTo) { - List<SortableRing> positionsFromTop = new ArrayList<SortableRing>(); - - //Fin tabs will be computed between the last two rings that meet the criteria, represented by top and bottom here. - SortableRing top = null; - SortableRing bottom = null; - - if (rings != null) { - //Sort rings from top of parent to bottom - Collections.sort(rings, new Comparator<CenteringRing>() { - @Override - public int compare(CenteringRing centeringRing, CenteringRing centeringRing1) { - return (int) (1000d * (centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo) - - centeringRing1.asPositionValue(RocketComponent.Position.TOP, relativeTo))); - } - }); - - for (int i = 0; i < rings.size(); i++) { - CenteringRing centeringRing = rings.get(i); - //Handle centering rings that overlap or are adjacent by synthetically merging them into one virtual ring. - if (!positionsFromTop.isEmpty() && - positionsFromTop.get(positionsFromTop.size() - 1).bottomSidePositionFromTop() >= - centeringRing.asPositionValue(RocketComponent.Position.TOP, relativeTo)) { - SortableRing adjacent = positionsFromTop.get(positionsFromTop.size() - 1); - adjacent.merge(centeringRing, relativeTo); - } else { - positionsFromTop.add(new SortableRing(centeringRing, relativeTo)); - } - } - - for (int i = 0; i < positionsFromTop.size(); i++) { - SortableRing sortableRing = positionsFromTop.get(i); - if (top == null) { - top = sortableRing; - } else if (sortableRing.bottomSidePositionFromTop() <= finPositionFromTop) { - top = sortableRing; - bottom = null; - } else if (top.bottomSidePositionFromTop() <= finPositionFromTop) { - if (bottom == null) { - //If the current ring is in the upper half of the root chord, make it the top ring - if (sortableRing.bottomSidePositionFromTop() < finPositionFromTop + finLength / 2d) { - top = sortableRing; - } else { - bottom = sortableRing; - } - } - //Is the ring even with or above the end of the root chord? If so, make the existing bottom the top ring, - //and the current ring the bottom - else if (sortableRing.positionFromTop() <= finPositionFromTop + finLength) { - top = bottom; - bottom = sortableRing; - } - } else { - if (bottom == null) { - bottom = sortableRing; - } - } - } - } - - double resultFinTabLength = 0d; - - // Edge case where there are no centering rings or for some odd reason top and bottom are identical. - if (top == null || top == bottom) { - mts.setValue(0); - resultFinTabLength = finLength; - } else if (bottom == null) { - // If there is no bottom ring and the top ring's bottom edge is within the span of the root chord, then - // set the position of the fin tab starting at the bottom side of the top ring. - if (top.bottomSidePositionFromTop() >= finPositionFromTop) { - mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop); - resultFinTabLength = (finPositionFromTop + finLength - top.bottomSidePositionFromTop()); - } else { - mts.setValue(0); - double diffLen = top.positionFromTop() - finPositionFromTop; - if (diffLen < 0) { - // Otherwise the top ring is outside the span of the root chord so set the tab length to be the entire - // root chord. - resultFinTabLength = finLength; - } - else { - // Otherwise there is one ring within the span. Return the length from the start of the fin to the top - // side of the ring. - resultFinTabLength = diffLen; - } - } - } - // If the bottom edge of the top centering ring is above the start of the fin's root chord, then make the - // fin tab align with the start of the root chord. - else if (top.bottomSidePositionFromTop() < finPositionFromTop) { - mts.setValue(0); - - double lenToBottomRing = bottom.positionFromTop - finPositionFromTop; - // If the bottom ring lies farther back (down) than the trailing edge of the fin, then the tab should - // only be as long as the fin. - if (lenToBottomRing > finLength) { - resultFinTabLength = finLength; - } - else { - resultFinTabLength = lenToBottomRing; - } - } else { - mts.setValue(top.bottomSidePositionFromTop() - finPositionFromTop); - // The bottom ring is beyond the trailing edge of the fin. - if (bottom.positionFromTop() > finLength + finPositionFromTop) { - resultFinTabLength = (finLength + finPositionFromTop - top.bottomSidePositionFromTop()); - } - // The rings are within the span of the root chord. Place the tab between them. - else { - resultFinTabLength = (bottom.positionFromTop() - top.bottomSidePositionFromTop()); - } - } - if (resultFinTabLength < 0) { - resultFinTabLength = 0d; - } - return resultFinTabLength; - } - - @Override - public void updateFields() { - super.updateFields(); - if (split != null) - split.setEnabled(((FinSet) component).getFinCount() > 1); - } - - /** - * A container class to store pertinent info about centering rings. This is used in the computation to figure - * out tab length and position. - */ - static class SortableRing { - - /** - * The length of the ring (more commonly called the thickness). - */ - private double thickness; - /** - * The position of the ring from the top of the parent. - */ - private double positionFromTop; - - /** - * Constructor. - * - * @param r the source centering ring - */ - SortableRing(CenteringRing r, RocketComponent relativeTo) { - thickness = r.getLength(); - positionFromTop = r.asPositionValue(RocketComponent.Position.TOP, relativeTo); - } - - /** - * Merge an adjacent ring. - * - * @param adjacent the adjacent ring - */ - public void merge(CenteringRing adjacent, RocketComponent relativeTo) { - double v = adjacent.asPositionValue(RocketComponent.Position.TOP, relativeTo); - if (positionFromTop < v) { - thickness = (v + adjacent.getLength()) - positionFromTop; - } else { - double tmp = positionFromTop + thickness; - positionFromTop = v; - thickness = tmp - v; - } - } - - /** - * Compute the position of the bottom edge of the ring, relative to the top of the parent. - * - * @return the distance from the top of the parent to the bottom edge of the ring - */ - public double bottomSidePositionFromTop() { - return positionFromTop + thickness; - } - - /** - * Compute the position of the top edge of the ring, relative to the top of the parent. - * - * @return the distance from the top of the parent to the top edge of the ring - */ - public double positionFromTop() { - return positionFromTop; - } - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java deleted file mode 100644 index 56609bcf..00000000 --- a/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java +++ /dev/null @@ -1,494 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.Point; -import java.awt.event.MouseEvent; -import java.awt.geom.Point2D; - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.JTable; -import javax.swing.ListSelectionModel; -import javax.swing.SwingConstants; -import javax.swing.table.AbstractTableModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.IntegerModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.scalefigure.FinPointFigure; -import net.sf.openrocket.gui.scalefigure.ScaleScrollPane; -import net.sf.openrocket.gui.scalefigure.ScaleSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; - -public class FreeformFinSetConfig extends FinSetConfig { - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private final FreeformFinSet finset; - private JTable table = null; - private FinPointTableModel tableModel = null; - - private FinPointFigure figure = null; - - - public FreeformFinSetConfig(OpenRocketDocument d, RocketComponent component) { - super(d, component); - this.finset = (FreeformFinSet) component; - - //// General and General properties - tabbedPane.insertTab(trans.get("FreeformFinSetCfg.tab.General"), null, generalPane(), - trans.get("FreeformFinSetCfg.tab.ttip.General"), 0); - //// Shape and Fin shape - tabbedPane.insertTab(trans.get("FreeformFinSetCfg.tab.Shape"), null, shapePane(), - trans.get("FreeformFinSetCfg.tab.ttip.Finshape"), 1); - tabbedPane.setSelectedIndex(0); - - addFinSetButtons(); - } - - - - private JPanel generalPane() { - - DoubleModel m; - JSpinner spin; - JComboBox combo; - - JPanel mainPanel = new JPanel(new MigLayout("fill")); - - JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel", "[][65lp::][30lp::]", "")); - - - - //// Number of fins: - panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Numberoffins"))); - - IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); - - spin = new JSpinner(im.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx, wrap"); - - - //// Base rotation - panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Finrotation"))); - - m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - - //// Fin cant - JLabel label = new JLabel(trans.get("FreeformFinSetCfg.lbl.Fincant")); - //// The angle that the fins are canted with respect to the rocket body. - label.setToolTipText(trans.get("FreeformFinSetCfg.lbl.ttip.Fincant")); - panel.add(label); - - m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE, - -FinSet.MAX_CANT, FinSet.MAX_CANT); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT, FinSet.MAX_CANT)), - "w 100lp, wrap 40lp"); - - - - //// Position - //// Position relative to: - panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Posrelativeto"))); - - combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx 3, growx, wrap"); - //// plus - panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - - - - - mainPanel.add(panel, "aligny 20%"); - mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy, height 150lp"); - - - panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - - - - //// Cross section - //// Fin cross section: - panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.FincrossSection")), "span, split"); - combo = new JComboBox( - new EnumModel<FinSet.CrossSection>(component, "CrossSection")); - panel.add(combo, "growx, wrap unrel"); - - - //// Thickness: - panel.add(new JLabel(trans.get("FreeformFinSetCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 30lp"); - - - //// Material - materialPanel(panel, Material.Type.BULK); - - - - mainPanel.add(panel, "aligny 20%"); - - return mainPanel; - } - - - - private JPanel shapePane() { - JPanel panel = new JPanel(new MigLayout("fill")); - - - // Create the figure - figure = new FinPointFigure(finset); - ScaleScrollPane figurePane = new FinPointScrollPane(); - figurePane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); - figurePane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); - - // Create the table - tableModel = new FinPointTableModel(); - table = new JTable(tableModel); - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - for (int i = 0; i < Columns.values().length; i++) { - table.getColumnModel().getColumn(i). - setPreferredWidth(Columns.values()[i].getWidth()); - } - JScrollPane tablePane = new JScrollPane(table); - - - // panel.add(new JLabel("Coordinates:"), "aligny bottom, alignx 50%"); - // panel.add(new JLabel(" View:"), "wrap, aligny bottom"); - - - panel.add(tablePane, "growy, width 100lp:100lp:, height 100lp:250lp:"); - panel.add(figurePane, "gap unrel, spanx, growx, growy 1000, height 100lp:250lp:, wrap"); - - panel.add(new StyledLabel(trans.get("lbl.doubleClick1"), -2), "alignx 50%"); - - panel.add(new ScaleSelector(figurePane), "spany 2"); - panel.add(new StyledLabel(trans.get("FreeformFinSetConfig.lbl.clickDrag") + " " + - trans.get("FreeformFinSetConfig.lbl.ctrlClick"), -2), "spany 2, right, wrap"); - - - panel.add(new StyledLabel(trans.get("FreeformFinSetConfig.lbl.doubleClick2"), -2), "alignx 50%"); - - return panel; - } - - - - - - @Override - public void updateFields() { - super.updateFields(); - - if (tableModel != null) { - tableModel.fireTableDataChanged(); - } - if (figure != null) { - figure.updateFigure(); - } - } - - - - - private class FinPointScrollPane extends ScaleScrollPane { - private static final int ANY_MASK = - (MouseEvent.ALT_DOWN_MASK | MouseEvent.ALT_GRAPH_DOWN_MASK | - MouseEvent.META_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK | - MouseEvent.SHIFT_DOWN_MASK); - - private int dragIndex = -1; - - public FinPointScrollPane() { - super(figure, false); // Disallow fitting as it's buggy - } - - @Override - public void mousePressed(MouseEvent event) { - int mods = event.getModifiersEx(); - - if (event.getButton() != MouseEvent.BUTTON1 || - (mods & ANY_MASK) != 0) { - super.mousePressed(event); - return; - } - - int index = getPoint(event); - if (index >= 0) { - dragIndex = index; - return; - } - index = getSegment(event); - if (index >= 0) { - Point2D.Double point = getCoordinates(event); - finset.addPoint(index); - try { - finset.setPoint(index, point.x, point.y); - } catch (IllegalFinPointException ignore) { - } - dragIndex = index; - - return; - } - - super.mousePressed(event); - return; - } - - - @Override - public void mouseDragged(MouseEvent event) { - int mods = event.getModifiersEx(); - if (dragIndex < 0 || - (mods & (ANY_MASK | MouseEvent.BUTTON1_DOWN_MASK)) != - MouseEvent.BUTTON1_DOWN_MASK) { - super.mouseDragged(event); - return; - } - Point2D.Double point = getCoordinates(event); - - try { - finset.setPoint(dragIndex, point.x, point.y); - } catch (IllegalFinPointException ignore) { - log.debug("Ignoring IllegalFinPointException while dragging, dragIndex=" + dragIndex + - " x=" + point.x + " y=" + point.y); - } - } - - - @Override - public void mouseReleased(MouseEvent event) { - dragIndex = -1; - super.mouseReleased(event); - } - - @Override - public void mouseClicked(MouseEvent event) { - int mods = event.getModifiersEx(); - if (event.getButton() != MouseEvent.BUTTON1 || - (mods & ANY_MASK) != MouseEvent.CTRL_DOWN_MASK) { - super.mouseClicked(event); - return; - } - - int index = getPoint(event); - if (index < 0) { - super.mouseClicked(event); - return; - } - - try { - finset.removePoint(index); - } catch (IllegalFinPointException ignore) { - } - } - - - private int getPoint(MouseEvent event) { - Point p0 = event.getPoint(); - Point p1 = this.getViewport().getViewPosition(); - int x = p0.x + p1.x; - int y = p0.y + p1.y; - - return figure.getIndexByPoint(x, y); - } - - private int getSegment(MouseEvent event) { - Point p0 = event.getPoint(); - Point p1 = this.getViewport().getViewPosition(); - int x = p0.x + p1.x; - int y = p0.y + p1.y; - - return figure.getSegmentByPoint(x, y); - } - - private Point2D.Double getCoordinates(MouseEvent event) { - Point p0 = event.getPoint(); - Point p1 = this.getViewport().getViewPosition(); - int x = p0.x + p1.x; - int y = p0.y + p1.y; - - return figure.convertPoint(x, y); - } - - - } - - - - - - private enum Columns { - // NUMBER { - // @Override - // public String toString() { - // return "#"; - // } - // @Override - // public String getValue(FreeformFinSet finset, int row) { - // return "" + (row+1) + "."; - // } - // @Override - // public int getWidth() { - // return 10; - // } - // }, - X { - @Override - public String toString() { - return "X / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString(); - } - - @Override - public String getValue(FreeformFinSet finset, int row) { - return UnitGroup.UNITS_LENGTH.getDefaultUnit() - .toString(finset.getFinPoints()[row].x); - } - }, - Y { - @Override - public String toString() { - return "Y / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString(); - } - - @Override - public String getValue(FreeformFinSet finset, int row) { - return UnitGroup.UNITS_LENGTH.getDefaultUnit() - .toString(finset.getFinPoints()[row].y); - } - }; - - public abstract String getValue(FreeformFinSet finset, int row); - - @Override - public abstract String toString(); - - public int getWidth() { - return 20; - } - } - - private class FinPointTableModel extends AbstractTableModel { - - @Override - public int getColumnCount() { - return Columns.values().length; - } - - @Override - public int getRowCount() { - return finset.getPointCount(); - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - return Columns.values()[columnIndex].getValue(finset, rowIndex); - } - - @Override - public String getColumnName(int columnIndex) { - return Columns.values()[columnIndex].toString(); - } - - @Override - public boolean isCellEditable(int rowIndex, int columnIndex) { - if (rowIndex == 0 || rowIndex == getRowCount() - 1) { - return (columnIndex == Columns.X.ordinal()); - } - - return (columnIndex == Columns.X.ordinal() || columnIndex == Columns.Y.ordinal()); - } - - @Override - public void setValueAt(Object o, int rowIndex, int columnIndex) { - if (!(o instanceof String)) - return; - - if (rowIndex < 0 || rowIndex >= finset.getFinPoints().length || - columnIndex < 0 || columnIndex >= Columns.values().length) { - throw new IllegalArgumentException("Index out of bounds, row=" + rowIndex + - " column=" + columnIndex + " fin point count=" + finset.getFinPoints().length); - } - - String str = (String) o; - try { - - double value = UnitGroup.UNITS_LENGTH.fromString(str); - Coordinate c = finset.getFinPoints()[rowIndex]; - if (columnIndex == Columns.X.ordinal()) - c = c.setX(value); - else - c = c.setY(value); - - finset.setPoint(rowIndex, c.x, c.y); - - } catch (NumberFormatException ignore) { - } catch (IllegalFinPointException ignore) { - } - } - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java b/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java deleted file mode 100644 index b7c0afea..00000000 --- a/src/net/sf/openrocket/gui/configdialog/InnerTubeConfig.java +++ /dev/null @@ -1,324 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.geom.Ellipse2D; -import java.util.EventObject; -import java.util.List; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.SwingUtilities; -import javax.swing.border.BevelBorder; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.Resettable; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.ClusterConfiguration; -import net.sf.openrocket.rocketcomponent.Clusterable; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.StateChangeListener; - - -public class InnerTubeConfig extends ThicknessRingComponentConfig { - private static final Translator trans = Application.getTranslator(); - - - public InnerTubeConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel tab; - - tab = new MotorConfig((MotorMount) c); - //// Motor and Motor mount configuration - tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Motor"), null, tab, - trans.get("InnerTubeCfg.tab.ttip.Motor"), 1); - - tab = clusterTab(); - //// Cluster and Cluster configuration - tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Cluster"), null, tab, - trans.get("InnerTubeCfg.tab.ttip.Cluster"), 2); - - tab = positionTab(); - //// Radial position - tabbedPane.insertTab(trans.get("InnerTubeCfg.tab.Radialpos"), null, tab, - trans.get("InnerTubeCfg.tab.ttip.Radialpos"), 3); - - tabbedPane.setSelectedIndex(0); - } - - - private JPanel clusterTab() { - JPanel panel = new JPanel(new MigLayout()); - - JPanel subPanel = new JPanel(new MigLayout()); - - // Cluster type selection - //// Select cluster configuration: - subPanel.add(new JLabel(trans.get("InnerTubeCfg.lbl.Selectclustercfg")), "spanx, wrap"); - subPanel.add(new ClusterSelectionPanel((InnerTube) component), "spanx, wrap"); - // JPanel clusterSelection = new ClusterSelectionPanel((InnerTube)component); - // clusterSelection.setBackground(Color.blue); - // subPanel.add(clusterSelection); - - panel.add(subPanel); - - - subPanel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]")); - - // Tube separation scale - //// Tube separation: - JLabel l = new JLabel(trans.get("InnerTubeCfg.lbl.TubeSep")); - //// The separation of the tubes, 1.0 = touching each other - l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); - subPanel.add(l); - DoubleModel dm = new DoubleModel(component, "ClusterScale", 1, UnitGroup.UNITS_NONE, 0); - - JSpinner spin = new JSpinner(dm.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - //// The separation of the tubes, 1.0 = touching each other - spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); - subPanel.add(spin, "growx"); - - BasicSlider bs = new BasicSlider(dm.getSliderModel(0, 1, 4)); - //// The separation of the tubes, 1.0 = touching each other - bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.TubeSep")); - subPanel.add(bs, "skip,w 100lp, wrap"); - - // Rotation: - l = new JLabel(trans.get("InnerTubeCfg.lbl.Rotation")); - //// Rotation angle of the cluster configuration - l.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); - subPanel.add(l); - dm = new DoubleModel(component, "ClusterRotation", 1, UnitGroup.UNITS_ANGLE, - -Math.PI, Math.PI); - - spin = new JSpinner(dm.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - //// Rotation angle of the cluster configuration - spin.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); - subPanel.add(spin, "growx"); - - subPanel.add(new UnitSelector(dm), "growx"); - bs = new BasicSlider(dm.getSliderModel(-Math.PI, 0, Math.PI)); - //// Rotation angle of the cluster configuration - bs.setToolTipText(trans.get("InnerTubeCfg.lbl.ttip.Rotation")); - subPanel.add(bs, "w 100lp, wrap para"); - - - - // Split button - //// Split cluster - JButton split = new JButton(trans.get("InnerTubeCfg.but.Splitcluster")); - //// <html>Split the cluster into separate components.<br> - //// This also duplicates all components attached to this inner tube. - split.setToolTipText(trans.get("InnerTubeCfg.lbl.longA1") + - trans.get("InnerTubeCfg.lbl.longA2")); - split.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - // Do change in future for overall safety - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - RocketComponent parent = component.getParent(); - int index = parent.getChildPosition(component); - if (index < 0) { - throw new BugException("Inconsistent state: component=" + component + - " parent=" + parent + " parent.children=" + parent.getChildren()); - } - - InnerTube tube = (InnerTube) component; - if (tube.getClusterCount() <= 1) - return; - - document.addUndoPosition("Split cluster"); - - Coordinate[] coords = { Coordinate.NUL }; - coords = component.shiftCoordinates(coords); - parent.removeChild(index); - for (int i = 0; i < coords.length; i++) { - InnerTube copy = (InnerTube) component.copy(); - copy.setClusterConfiguration(ClusterConfiguration.SINGLE); - copy.setClusterRotation(0.0); - copy.setClusterScale(1.0); - copy.setRadialShift(coords[i].y, coords[i].z); - copy.setName(copy.getName() + " #" + (i + 1)); - - parent.addChild(copy, index + i); - } - } - }); - } - }); - subPanel.add(split, "spanx, split 2, gapright para, sizegroup buttons, right"); - - - // Reset button - ///// Reset settings - JButton reset = new JButton(trans.get("InnerTubeCfg.but.Resetsettings")); - //// Reset the separation and rotation to the default values - reset.setToolTipText(trans.get("InnerTubeCfg.but.ttip.Resetsettings")); - reset.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent arg0) { - ((InnerTube) component).setClusterScale(1.0); - ((InnerTube) component).setClusterRotation(0.0); - } - }); - subPanel.add(reset, "sizegroup buttons, right"); - - panel.add(subPanel, "grow"); - - - return panel; - } -} - - -class ClusterSelectionPanel extends JPanel { - private static final int BUTTON_SIZE = 50; - private static final int MOTOR_DIAMETER = 10; - - private static final Color SELECTED_COLOR = Color.RED; - private static final Color UNSELECTED_COLOR = Color.WHITE; - private static final Color MOTOR_FILL_COLOR = Color.GREEN; - private static final Color MOTOR_BORDER_COLOR = Color.BLACK; - - public ClusterSelectionPanel(Clusterable component) { - super(new MigLayout("gap 0 0", - "[" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!]", - "[" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!][" + BUTTON_SIZE + "!]")); - - for (int i = 0; i < ClusterConfiguration.CONFIGURATIONS.length; i++) { - ClusterConfiguration config = ClusterConfiguration.CONFIGURATIONS[i]; - - JComponent button = new ClusterButton(component, config); - if (i % 4 == 3) - add(button, "wrap"); - else - add(button); - } - - } - - - private class ClusterButton extends JPanel implements StateChangeListener, MouseListener, - Resettable { - private Clusterable component; - private ClusterConfiguration config; - - public ClusterButton(Clusterable c, ClusterConfiguration config) { - component = c; - this.config = config; - setMinimumSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); - setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); - setMaximumSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE)); - setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); - // setBorder(BorderFactory.createLineBorder(Color.BLACK, 1)); - component.addChangeListener(this); - addMouseListener(this); - } - - - @Override - public void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - Rectangle area = g2.getClipBounds(); - - if (component.getClusterConfiguration() == config) - g2.setColor(SELECTED_COLOR); - else - g2.setColor(UNSELECTED_COLOR); - - g2.fillRect(area.x, area.y, area.width, area.height); - - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - List<Double> points = config.getPoints(); - Ellipse2D.Float circle = new Ellipse2D.Float(); - for (int i = 0; i < points.size() / 2; i++) { - double x = points.get(i * 2); - double y = points.get(i * 2 + 1); - - double px = BUTTON_SIZE / 2 + x * MOTOR_DIAMETER; - double py = BUTTON_SIZE / 2 - y * MOTOR_DIAMETER; - circle.setFrameFromCenter(px, py, px + MOTOR_DIAMETER / 2, py + MOTOR_DIAMETER / 2); - - g2.setColor(MOTOR_FILL_COLOR); - g2.fill(circle); - g2.setColor(MOTOR_BORDER_COLOR); - g2.draw(circle); - } - } - - - @Override - public void stateChanged(EventObject e) { - repaint(); - } - - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) { - component.setClusterConfiguration(config); - } - } - - @Override - public void mouseEntered(MouseEvent e) { - } - - @Override - public void mouseExited(MouseEvent e) { - } - - @Override - public void mousePressed(MouseEvent e) { - } - - @Override - public void mouseReleased(MouseEvent e) { - } - - - @Override - public void resetModel() { - component.removeChangeListener(this); - removeMouseListener(this); - } - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java b/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java deleted file mode 100644 index 947b205a..00000000 --- a/src/net/sf/openrocket/gui/configdialog/LaunchLugConfig.java +++ /dev/null @@ -1,162 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class LaunchLugConfig extends RocketComponentConfig { - - private MotorConfig motorConfigPane = null; - private static final Translator trans = Application.getTranslator(); - - public LaunchLugConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel primary = new JPanel(new MigLayout("fill")); - - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - //// Body tube length - //// Length: - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Length"))); - - DoubleModel m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.1)), "w 100lp, wrap para"); - - - //// Body tube diameter - //// Outer diameter: - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Outerdiam"))); - - DoubleModel od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap rel"); - - - //// Inner diameter: - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Innerdiam"))); - - // Diameter = 2*Radius - m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); - - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), "w 100lp, wrap rel"); - - - //// Wall thickness - //// Thickness: - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 20lp"); - - - //// Radial position: - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Radialpos"))); - - m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, - -Math.PI, Math.PI); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - - - primary.add(panel, "grow, gapright 20lp"); - panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - - - //// Position relative to: - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.Posrelativeto"))); - - JComboBox combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx, growx, wrap"); - - //// plus - panel.add(new JLabel(trans.get("LaunchLugCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap para"); - - - - //// Material - materialPanel(panel, Material.Type.BULK); - - - primary.add(panel, "grow"); - - //// General and General properties - tabbedPane.insertTab(trans.get("LaunchLugCfg.tab.General"), null, primary, - trans.get("LaunchLugCfg.tab.Generalprop"), 0); - tabbedPane.setSelectedIndex(0); - } - - @Override - public void updateFields() { - super.updateFields(); - if (motorConfigPane != null) - motorConfigPane.updateFields(); - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java deleted file mode 100644 index b199d209..00000000 --- a/src/net/sf/openrocket/gui/configdialog/MassComponentConfig.java +++ /dev/null @@ -1,160 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - - -public class MassComponentConfig extends RocketComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public MassComponentConfig(OpenRocketDocument d, RocketComponent component) { - super(d, component); - - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - - - //// Mass - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Mass"))); - - DoubleModel m = new DoubleModel(component, "ComponentMass", UnitGroup.UNITS_MASS, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.5)), "w 100lp, wrap"); - - - - //// Mass length - //// Length - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Length"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); - - - //// Tube diameter - //// Diameter: - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Diameter"))); - - DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); - - - //// Position - //// Position relative to: - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.PosRelativeto"))); - - JComboBox combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx, growx, wrap"); - //// plus - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - //// General and General properties - tabbedPane.insertTab(trans.get("MassComponentCfg.tab.General"), null, panel, - trans.get("MassComponentCfg.tab.ttip.General"), 0); - //// Radial position and Radial position configuration - tabbedPane.insertTab(trans.get("MassComponentCfg.tab.Radialpos"), null, positionTab(), - trans.get("MassComponentCfg.tab.ttip.Radialpos"), 1); - tabbedPane.setSelectedIndex(0); - } - - - protected JPanel positionTab() { - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Radial position - //// Radial distance: - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Radialdistance"))); - - DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); - - - //// Radial direction: - panel.add(new JLabel(trans.get("MassComponentCfg.lbl.Radialdirection"))); - - m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - //// Reset button - JButton button = new JButton(trans.get("MassComponentCfg.but.Reset")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ((MassComponent) component).setRadialDirection(0.0); - ((MassComponent) component).setRadialPosition(0.0); - } - }); - panel.add(button, "spanx, right"); - - return panel; - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/MotorConfig.java b/src/net/sf/openrocket/gui/configdialog/MotorConfig.java deleted file mode 100644 index 227a475c..00000000 --- a/src/net/sf/openrocket/gui/configdialog/MotorConfig.java +++ /dev/null @@ -1,243 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.Component; -import java.awt.Container; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.SwingUtilities; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.BooleanModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.dialogs.motor.MotorChooserDialog; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class MotorConfig extends JPanel { - - private final Rocket rocket; - private final MotorMount mount; - private final Configuration configuration; - private JPanel panel; - private JLabel motorLabel; - private static final Translator trans = Application.getTranslator(); - - public MotorConfig(MotorMount motorMount) { - super(new MigLayout("fill")); - - this.rocket = ((RocketComponent) motorMount).getRocket(); - this.mount = motorMount; - this.configuration = ((RocketComponent) motorMount).getRocket().getDefaultConfiguration(); - - BooleanModel model; - - model = new BooleanModel(motorMount, "MotorMount"); - JCheckBox check = new JCheckBox(model); - ////This component is a motor mount - check.setText(trans.get("MotorCfg.checkbox.compmotormount")); - this.add(check, "wrap"); - - - panel = new JPanel(new MigLayout("fill")); - this.add(panel, "grow, wrap"); - - - // Motor configuration selector - //// Motor configuration: - panel.add(new JLabel(trans.get("MotorCfg.lbl.Motorcfg")), "shrink"); - - JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration)); - panel.add(combo, "growx"); - - configuration.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - updateFields(); - } - }); - - //// New button - JButton button = new JButton(trans.get("MotorCfg.but.New")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - String id = rocket.newMotorConfigurationID(); - configuration.setMotorConfigurationID(id); - } - }); - panel.add(button, "wrap unrel"); - - - // Current motor: - panel.add(new JLabel(trans.get("MotorCfg.lbl.Currentmotor")), "shrink"); - - motorLabel = new JLabel(); - motorLabel.setFont(motorLabel.getFont().deriveFont(Font.BOLD)); - updateFields(); - panel.add(motorLabel, "wrap unrel"); - - - - // Overhang - //// Motor overhang: - panel.add(new JLabel(trans.get("MotorCfg.lbl.Motoroverhang"))); - - DoubleModel dm = new DoubleModel(motorMount, "MotorOverhang", UnitGroup.UNITS_LENGTH); - - JSpinner spin = new JSpinner(dm.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "span, split, width :65lp:"); - - panel.add(new UnitSelector(dm), "width :30lp:"); - panel.add(new BasicSlider(dm.getSliderModel(-0.02, 0.06)), "w 100lp, wrap unrel"); - - - - // Select ignition event - //// Ignition at: - panel.add(new JLabel(trans.get("MotorCfg.lbl.Ignitionat")), ""); - - combo = new JComboBox(new EnumModel<IgnitionEvent>(mount, "IgnitionEvent")); - panel.add(combo, "growx, wrap"); - - // ... and delay - //// plus - panel.add(new JLabel(trans.get("MotorCfg.lbl.plus")), "gap indent, skip 1, span, split"); - - dm = new DoubleModel(mount, "IgnitionDelay", 0); - spin = new JSpinner(dm.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "gap rel rel"); - - //// seconds - panel.add(new JLabel(trans.get("MotorCfg.lbl.seconds")), "wrap unrel"); - - - - // Check stage count - RocketComponent c = (RocketComponent) mount; - c = c.getRocket(); - int stages = c.getChildCount(); - - if (stages == 1) { - //// The current design has only one stage. - //// Stages can be added by clicking \"New stage\". - - panel.add(new StyledLabel(trans.get("MotorCfg.lbl.longA1") + " " + - trans.get("MotorCfg.lbl.longA2"), -1), - "spanx, right, wrap para"); - } else { - //// The current design has - //// stages. - panel.add(new StyledLabel(trans.get("MotorCfg.lbl.longB1") + " " + stages + " " + - trans.get("MotorCfg.lbl.longB2"), -1), - "skip 1, spanx, wrap para"); - } - - - // Select etc. buttons - //// Select motor - button = new JButton(trans.get("MotorCfg.but.Selectmotor")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - String id = configuration.getMotorConfigurationID(); - - MotorChooserDialog dialog = new MotorChooserDialog(mount.getMotor(id), - mount.getMotorDelay(id), mount.getMotorMountDiameter(), - SwingUtilities.getWindowAncestor(MotorConfig.this)); - dialog.setVisible(true); - Motor m = dialog.getSelectedMotor(); - double d = dialog.getSelectedDelay(); - - if (m != null) { - if (id == null) { - id = rocket.newMotorConfigurationID(); - configuration.setMotorConfigurationID(id); - } - mount.setMotor(id, m); - mount.setMotorDelay(id, d); - } - updateFields(); - } - }); - panel.add(button, "span, split, growx"); - - //// Remove motor - button = new JButton(trans.get("MotorCfg.but.Removemotor")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - mount.setMotor(configuration.getMotorConfigurationID(), null); - updateFields(); - } - }); - panel.add(button, "growx, wrap"); - - - - - - // Set enabled status - - setDeepEnabled(panel, motorMount.isMotorMount()); - check.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - setDeepEnabled(panel, mount.isMotorMount()); - } - }); - - } - - public void updateFields() { - String id = configuration.getMotorConfigurationID(); - Motor m = mount.getMotor(id); - if (m == null) { - //// None - motorLabel.setText(trans.get("MotorCfg.lbl.motorLabel")); - } else { - String str = ""; - if (m instanceof ThrustCurveMotor) - str = ((ThrustCurveMotor) m).getManufacturer() + " "; - str += m.getDesignation(mount.getMotorDelay(id)); - motorLabel.setText(str); - } - } - - - private static void setDeepEnabled(Component component, boolean enabled) { - component.setEnabled(enabled); - if (component instanceof Container) { - for (Component c : ((Container) component).getComponents()) { - setDeepEnabled(c, enabled); - } - } - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java b/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java deleted file mode 100644 index 682f6d35..00000000 --- a/src/net/sf/openrocket/gui/configdialog/NoseConeConfig.java +++ /dev/null @@ -1,183 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.BooleanModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class NoseConeConfig extends RocketComponentConfig { - - private JComboBox typeBox; - - private DescriptionArea description; - - private JLabel shapeLabel; - private JSpinner shapeSpinner; - private JSlider shapeSlider; - private static final Translator trans = Application.getTranslator(); - - // Prepended to the description from NoseCone.DESCRIPTIONS - private static final String PREDESC = "<html>"; - - public NoseConeConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - DoubleModel m; - JPanel panel = new JPanel(new MigLayout("", "[][65lp::][30lp::]")); - - - - - //// Shape selection - //// Nose cone shape: - panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Noseconeshape"))); - - Transition.Shape selected = ((NoseCone) component).getType(); - Transition.Shape[] typeList = Transition.Shape.values(); - - typeBox = new JComboBox(typeList); - typeBox.setEditable(false); - typeBox.setSelectedItem(selected); - typeBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Transition.Shape s = (Transition.Shape) typeBox.getSelectedItem(); - ((NoseCone) component).setType(s); - description.setText(PREDESC + s.getNoseConeDescription()); - updateEnabled(); - } - }); - panel.add(typeBox, "span, wrap rel"); - - - - - //// Shape parameter - //// Shape parameter: - shapeLabel = new JLabel(trans.get("NoseConeCfg.lbl.Shapeparam")); - panel.add(shapeLabel); - - m = new DoubleModel(component, "ShapeParameter"); - - shapeSpinner = new JSpinner(m.getSpinnerModel()); - shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner)); - panel.add(shapeSpinner, "growx"); - - DoubleModel min = new DoubleModel(component, "ShapeParameterMin"); - DoubleModel max = new DoubleModel(component, "ShapeParameterMax"); - shapeSlider = new BasicSlider(m.getSliderModel(min, max)); - panel.add(shapeSlider, "skip, w 100lp, wrap para"); - - updateEnabled(); - - - //// Length - //// Nose cone length: - panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Noseconelength"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.7)), "w 100lp, wrap"); - - //// Diameter - //// Base diameter: - panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Basediam"))); - - m = new DoubleModel(component, "AftRadius", 2.0, UnitGroup.UNITS_LENGTH, 0); // Diameter = 2*Radius - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); - - JCheckBox check = new JCheckBox(m.getAutomaticAction()); - //// Automatic - check.setText(trans.get("NoseConeCfg.checkbox.Automatic")); - panel.add(check, "skip, span 2, wrap"); - - - //// Wall thickness: - panel.add(new JLabel(trans.get("NoseConeCfg.lbl.Wallthickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 0px"); - - - check = new JCheckBox(new BooleanModel(component, "Filled")); - //// Filled - check.setText(trans.get("NoseConeCfg.checkbox.Filled")); - panel.add(check, "skip, span 2, wrap"); - - - panel.add(new JLabel(""), "growy"); - - - - //// Description - - JPanel panel2 = new JPanel(new MigLayout("ins 0")); - - description = new DescriptionArea(5); - description.setText(PREDESC + ((NoseCone) component).getType().getNoseConeDescription()); - panel2.add(description, "wmin 250lp, spanx, growx, wrap para"); - - - //// Material - - - materialPanel(panel2, Material.Type.BULK); - panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany"); - - - //// General and General properties - tabbedPane.insertTab(trans.get("NoseConeCfg.tab.General"), null, panel, - trans.get("NoseConeCfg.tab.ttip.General"), 0); - //// Shoulder and Shoulder properties - tabbedPane.insertTab(trans.get("NoseConeCfg.tab.Shoulder"), null, shoulderTab(), - trans.get("NoseConeCfg.tab.ttip.Shoulder"), 1); - tabbedPane.setSelectedIndex(0); - } - - - private void updateEnabled() { - boolean e = ((NoseCone) component).getType().usesParameter(); - shapeLabel.setEnabled(e); - shapeSpinner.setEnabled(e); - shapeSlider.setEnabled(e); - } - - -} diff --git a/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java b/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java deleted file mode 100644 index 311b42af..00000000 --- a/src/net/sf/openrocket/gui/configdialog/ParachuteConfig.java +++ /dev/null @@ -1,293 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.IntegerModel; -import net.sf.openrocket.gui.adaptors.MaterialModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.HtmlLabel; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class ParachuteConfig extends RecoveryDeviceConfig { - private static final Translator trans = Application.getTranslator(); - - public ParachuteConfig(OpenRocketDocument d, final RocketComponent component) { - super(d, component); - - JPanel primary = new JPanel(new MigLayout()); - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - - //// Canopy - panel.add(new StyledLabel(trans.get("ParachuteCfg.lbl.Canopy"), Style.BOLD), "wrap unrel"); - - //// Diameter: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Diameter"))); - - DoubleModel m = new DoubleModel(component, "Diameter", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)), "w 100lp, wrap"); - - //// Material: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Material"))); - - JComboBox combo = new JComboBox(new MaterialModel(panel, component, - Material.Type.SURFACE)); - //// The component material affects the weight of the component. - combo.setToolTipText(trans.get("ParachuteCfg.combo.MaterialModel")); - panel.add(combo, "spanx 3, growx, wrap paragraph"); - - // materialPanel(panel, Material.Type.SURFACE, "Material:", null); - - - - // CD - //// <html>Drag coefficient C<sub>D</sub>: - JLabel label = new HtmlLabel(trans.get("ParachuteCfg.lbl.longA1")); - String tip = trans.get("ParachuteCfg.lbl.longB1") + - trans.get("ParachuteCfg.lbl.longB2") + " " + - trans.get("ParachuteCfg.lbl.longB3"); - label.setToolTipText(tip); - panel.add(label); - - m = new DoubleModel(component, "CD", UnitGroup.UNITS_COEFFICIENT, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setToolTipText(tip); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - //// Reset button - JButton button = new JButton(trans.get("ParachuteCfg.but.Reset")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Parachute p = (Parachute) component; - p.setCD(Parachute.DEFAULT_CD); - } - }); - panel.add(button, "spanx, wrap 30lp"); - - - - //// Shroud lines - panel.add(new StyledLabel(trans.get("ParachuteCfg.lbl.Shroudlines"), Style.BOLD), "wrap unrel"); - - //// Number of lines: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Numberoflines"))); - IntegerModel im = new IntegerModel(component, "LineCount", 0); - - spin = new JSpinner(im.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx, wrap"); - - //// Line length: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Linelength"))); - - m = new DoubleModel(component, "LineLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)), "w 100lp, wrap"); - - //// Material: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Material"))); - - combo = new JComboBox(new MaterialModel(panel, component, Material.Type.LINE, - "LineMaterial")); - panel.add(combo, "spanx 3, growx, wrap"); - - - - primary.add(panel, "grow, gapright 20lp"); - panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - - - - //// Position - //// Position relative to: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Posrelativeto"))); - - combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx, growx, wrap"); - - //// plus - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - - //// Spatial length - //// Packed length: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Packedlength"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); - - - //// Tube diameter - //// Packed diameter: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Packeddiam"))); - - DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 30lp"); - - - //// Deployment - //// Deploys at: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Deploysat")), ""); - - combo = new JComboBox(new EnumModel<IgnitionEvent>(component, "DeployEvent")); - panel.add(combo, "spanx 3, growx, wrap"); - - // ... and delay - //// plus - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.plusdelay")), "right"); - - m = new DoubleModel(component, "DeployDelay", 0); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "spanx, split"); - - //// seconds - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.seconds")), "wrap paragraph"); - - // Altitude: - label = new JLabel(trans.get("ParachuteCfg.lbl.Altitude")); - altitudeComponents.add(label); - panel.add(label); - - m = new DoubleModel(component, "DeployAltitude", UnitGroup.UNITS_DISTANCE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - altitudeComponents.add(spin); - panel.add(spin, "growx"); - UnitSelector unit = new UnitSelector(m); - altitudeComponents.add(unit); - panel.add(unit, "growx"); - BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000)); - altitudeComponents.add(slider); - panel.add(slider, "w 100lp, wrap"); - - - primary.add(panel, "grow"); - - updateFields(); - - //// General and General properties - tabbedPane.insertTab(trans.get("ParachuteCfg.tab.General"), null, primary, trans.get("ParachuteCfg.tab.ttip.General"), 0); - //// Radial position and Radial position configuration - tabbedPane.insertTab(trans.get("ParachuteCfg.tab.Radialpos"), null, positionTab(), - trans.get("ParachuteCfg.tab.ttip.Radialpos"), 1); - tabbedPane.setSelectedIndex(0); - } - - - - - - protected JPanel positionTab() { - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Radial position - //// Radial distance: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Radialdistance"))); - - DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); - - - //// Radial direction: - panel.add(new JLabel(trans.get("ParachuteCfg.lbl.Radialdirection"))); - - m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - //// Reset button - JButton button = new JButton(trans.get("ParachuteCfg.but.Reset")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ((MassObject) component).setRadialDirection(0.0); - ((MassObject) component).setRadialPosition(0.0); - } - }); - panel.add(button, "spanx, right"); - - return panel; - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java b/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java deleted file mode 100644 index f93613c9..00000000 --- a/src/net/sf/openrocket/gui/configdialog/RecoveryDeviceConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.JComponent; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.RocketComponent; - - -public abstract class RecoveryDeviceConfig extends RocketComponentConfig { - - protected final List<JComponent> altitudeComponents = new ArrayList<JComponent>(); - - public RecoveryDeviceConfig(OpenRocketDocument d, RocketComponent component) { - super(d, component); - } - - - - @Override - public void updateFields() { - super.updateFields(); - - if (altitudeComponents == null) - return; - - boolean enabled = (((RecoveryDevice) component).getDeployEvent() - == RecoveryDevice.DeployEvent.ALTITUDE); - - for (JComponent c : altitudeComponents) { - c.setEnabled(enabled); - } - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java deleted file mode 100644 index 23906d6f..00000000 --- a/src/net/sf/openrocket/gui/configdialog/RingComponentConfig.java +++ /dev/null @@ -1,243 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.RingComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class RingComponentConfig extends RocketComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public RingComponentConfig(OpenRocketDocument d, RocketComponent component) { - super(d, component); - } - - - protected JPanel generalTab(String outer, String inner, String thickness, String length) { - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - DoubleModel m; - JSpinner spin; - DoubleModel od = null; - - - //// Outer diameter - if (outer != null) { - panel.add(new JLabel(outer)); - - //// OuterRadius - od = new DoubleModel(component, "OuterRadius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); - - if (od.isAutomaticAvailable()) { - JCheckBox check = new JCheckBox(od.getAutomaticAction()); - //// Automatic - check.setText(trans.get("ringcompcfg.Automatic")); - panel.add(check, "skip, span 2, wrap"); - } - } - - - //// Inner diameter - if (inner != null) { - panel.add(new JLabel(inner)); - - //// InnerRadius - m = new DoubleModel(component, "InnerRadius", 2, UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - if (od == null) - panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); - else - panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)), - "w 100lp, wrap"); - - if (m.isAutomaticAvailable()) { - JCheckBox check = new JCheckBox(m.getAutomaticAction()); - //// Automatic - check.setText(trans.get("ringcompcfg.Automatic")); - panel.add(check, "skip, span 2, wrap"); - } - } - - - //// Wall thickness - if (thickness != null) { - panel.add(new JLabel(thickness)); - - //// Thickness - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap"); - } - - - //// Inner tube length - if (length != null) { - panel.add(new JLabel(length)); - - //// Length - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); - } - - - //// Position - - //// Position relative to: - panel.add(new JLabel(trans.get("ringcompcfg.Positionrelativeto"))); - - JComboBox combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx 3, growx, wrap"); - - //// plus - panel.add(new JLabel(trans.get("ringcompcfg.plus")), "right"); - - //// PositionValue - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - - //// Material - JPanel sub = materialPanel(new JPanel(new MigLayout()), Material.Type.BULK); - - if (component instanceof EngineBlock) { - final DescriptionArea desc = new DescriptionArea(6); - //// <html>An <b>engine block</b> stops the motor from moving forwards in the motor mount tube.<br><br>In order to add a motor, create a <b>body tube</b> or <b>inner tube</b> and mark it as a motor mount in the <em>Motor</em> tab. - desc.setText(trans.get("ringcompcfg.EngineBlock.desc")); - sub.add(desc, "width 1px, growx, wrap"); - } - panel.add(sub, "cell 4 0, gapleft paragraph, aligny 0%, spany"); - - return panel; - } - - - protected JPanel positionTab() { - JPanel panel = new JPanel(new MigLayout("align 20% 20%, gap rel unrel", - "[][65lp::][30lp::]", "")); - - //// Radial position - JLabel l = new JLabel(trans.get("ringcompcfg.Radialdistance")); - //// Distance from the rocket centerline - l.setToolTipText(trans.get("ringcompcfg.Distancefrom")); - panel.add(l); - - DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - //// Distance from the rocket centerline - spin.setToolTipText(trans.get("ringcompcfg.Distancefrom")); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - BasicSlider bs = new BasicSlider(m.getSliderModel(0, 0.1, 1.0)); - //// Distance from the rocket centerline - bs.setToolTipText(trans.get("ringcompcfg.Distancefrom")); - panel.add(bs, "w 100lp, wrap"); - - - //// Radial direction - l = new JLabel(trans.get("ringcompcfg.Radialdirection")); - //// The radial direction from the rocket centerline - l.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); - panel.add(l); - - m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - //// The radial direction from the rocket centerline - spin.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - bs = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)); - //// The radial direction from the rocket centerline - bs.setToolTipText(trans.get("ringcompcfg.radialdirectionfrom")); - panel.add(bs, "w 100lp, wrap"); - - - //// Reset button - JButton button = new JButton(trans.get("ringcompcfg.but.Reset")); - //// Reset the component to the rocket centerline - button.setToolTipText(trans.get("ringcompcfg.but.Resetcomponant")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ((RingComponent) component).setRadialDirection(0.0); - ((RingComponent) component).setRadialPosition(0.0); - } - }); - panel.add(button, "spanx, right, wrap para"); - - - DescriptionArea note = new DescriptionArea(3); - //// Note: An inner tube will not affect the aerodynamics of the rocket even if it is located outside of the body tube. - note.setText(trans.get("ringcompcfg.note.desc")); - panel.add(note, "spanx, growx"); - - - return panel; - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java deleted file mode 100644 index 95853b02..00000000 --- a/src/net/sf/openrocket/gui/configdialog/RocketComponentConfig.java +++ /dev/null @@ -1,640 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.Color; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JColorChooser; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSpinner; -import javax.swing.JTabbedPane; -import javax.swing.JTextArea; -import javax.swing.JTextField; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.BooleanModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.MaterialModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.ColorIcon; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.util.ColorConversion; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.ComponentAssembly; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Invalidatable; -import net.sf.openrocket.util.LineStyle; - -public class RocketComponentConfig extends JPanel { - - private static final Translator trans = Application.getTranslator(); - - protected final OpenRocketDocument document; - protected final RocketComponent component; - protected final JTabbedPane tabbedPane; - - private final List<Invalidatable> invalidatables = new ArrayList<Invalidatable>(); - - - protected final JTextField componentNameField; - protected JTextArea commentTextArea; - private final TextFieldListener textFieldListener; - private JButton colorButton; - private JCheckBox colorDefault; - private JPanel buttonPanel; - - private JLabel massLabel; - - - public RocketComponentConfig(OpenRocketDocument document, RocketComponent component) { - setLayout(new MigLayout("fill", "[grow, fill]")); - this.document = document; - this.component = component; - - //// Component name: - JLabel label = new JLabel(trans.get("RocketCompCfg.lbl.Componentname")); - //// The component name. - label.setToolTipText(trans.get("RocketCompCfg.ttip.Thecomponentname")); - this.add(label, "split, gapright 10"); - - componentNameField = new JTextField(15); - textFieldListener = new TextFieldListener(); - componentNameField.addActionListener(textFieldListener); - componentNameField.addFocusListener(textFieldListener); - //// The component name. - componentNameField.setToolTipText(trans.get("RocketCompCfg.ttip.Thecomponentname")); - this.add(componentNameField, "growx, growy 0, wrap"); - - - tabbedPane = new JTabbedPane(); - this.add(tabbedPane, "growx, growy 1, wrap"); - - //// Override and Mass and CG override options - tabbedPane.addTab(trans.get("RocketCompCfg.tab.Override"), null, overrideTab(), - trans.get("RocketCompCfg.tab.MassandCGoverride")); - if (component.isMassive()) - //// Figure and Figure style options - tabbedPane.addTab(trans.get("RocketCompCfg.tab.Figure"), null, figureTab(), - trans.get("RocketCompCfg.tab.Figstyleopt")); - //// Comment and Specify a comment for the component - tabbedPane.addTab(trans.get("RocketCompCfg.tab.Comment"), null, commentTab(), - trans.get("RocketCompCfg.tab.Specifyacomment")); - - addButtons(); - - updateFields(); - } - - - protected void addButtons(JButton... buttons) { - if (buttonPanel != null) { - this.remove(buttonPanel); - } - - buttonPanel = new JPanel(new MigLayout("fill, ins 0")); - - //// Mass: - massLabel = new StyledLabel(trans.get("RocketCompCfg.lbl.Mass") + " ", -1); - buttonPanel.add(massLabel, "growx"); - - for (JButton b : buttons) { - buttonPanel.add(b, "right, gap para"); - } - - //// Close button - JButton closeButton = new JButton(trans.get("dlg.but.close")); - closeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent arg0) { - ComponentConfigDialog.hideDialog(); - } - }); - buttonPanel.add(closeButton, "right, gap 30lp"); - - updateFields(); - - this.add(buttonPanel, "spanx, growx"); - } - - - /** - * Called when a change occurs, so that the fields can be updated if necessary. - * When overriding this method, the supermethod must always be called. - */ - public void updateFields() { - // Component name - componentNameField.setText(component.getName()); - - // Component color and "Use default color" checkbox - if (colorButton != null && colorDefault != null) { - colorButton.setIcon(new ColorIcon(getColor())); - - if ((component.getColor() == null) != colorDefault.isSelected()) - colorDefault.setSelected(component.getColor() == null); - } - - // Mass label - if (component.isMassive()) { - //// Component mass: - String text = trans.get("RocketCompCfg.lbl.Componentmass") + " "; - text += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( - component.getComponentMass()); - - String overridetext = null; - if (component.isMassOverridden()) { - //// (overridden to - overridetext = trans.get("RocketCompCfg.lbl.overriddento") + " " + UnitGroup.UNITS_MASS.getDefaultUnit(). - toStringUnit(component.getOverrideMass()) + ")"; - } - - for (RocketComponent c = component.getParent(); c != null; c = c.getParent()) { - if (c.isMassOverridden() && c.getOverrideSubcomponents()) { - ///// (overridden by - overridetext = trans.get("RocketCompCfg.lbl.overriddenby") + " " + c.getName() + ")"; - } - } - - if (overridetext != null) - text = text + " " + overridetext; - - massLabel.setText(text); - } else { - massLabel.setText(""); - } - } - - - protected JPanel materialPanel(JPanel panel, Material.Type type) { - ////Component material: and Component finish: - return materialPanel(panel, type, trans.get("RocketCompCfg.lbl.Componentmaterial"), - trans.get("RocketCompCfg.lbl.Componentfinish")); - } - - protected JPanel materialPanel(JPanel panel, Material.Type type, - String materialString, String finishString) { - JLabel label = new JLabel(materialString); - //// The component material affects the weight of the component. - label.setToolTipText(trans.get("RocketCompCfg.lbl.ttip.componentmaterialaffects")); - panel.add(label, "spanx 4, wrap rel"); - - JComboBox combo = new JComboBox(new MaterialModel(panel, component, type)); - //// The component material affects the weight of the component. - combo.setToolTipText(trans.get("RocketCompCfg.combo.ttip.componentmaterialaffects")); - panel.add(combo, "spanx 4, growx, wrap paragraph"); - - - if (component instanceof ExternalComponent) { - label = new JLabel(finishString); - ////<html>The component finish affects the aerodynamic drag of the component.<br> - String tip = trans.get("RocketCompCfg.lbl.longA1") - //// The value indicated is the average roughness height of the surface. - + trans.get("RocketCompCfg.lbl.longA2"); - label.setToolTipText(tip); - panel.add(label, "spanx 4, wmin 220lp, wrap rel"); - - combo = new JComboBox(new EnumModel<ExternalComponent.Finish>(component, "Finish")); - combo.setToolTipText(tip); - panel.add(combo, "spanx 4, growx, split"); - - //// Set for all - JButton button = new JButton(trans.get("RocketCompCfg.but.Setforall")); - //// Set this finish for all components of the rocket. - button.setToolTipText(trans.get("RocketCompCfg.but.ttip.Setforall")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Finish f = ((ExternalComponent) component).getFinish(); - try { - document.startUndo("Set rocket finish"); - - // Do changes - Iterator<RocketComponent> iter = component.getRoot().iterator(); - while (iter.hasNext()) { - RocketComponent c = iter.next(); - if (c instanceof ExternalComponent) { - ((ExternalComponent) c).setFinish(f); - } - } - } finally { - document.stopUndo(); - } - } - }); - panel.add(button, "wrap paragraph"); - } - - return panel; - } - - - private JPanel overrideTab() { - JPanel panel = new JPanel(new MigLayout("align 50% 20%, fillx, gap rel unrel", - "[][65lp::][30lp::][]", "")); - //// Override the mass or center of gravity of the - panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.Overridemassorcenter") + " " + - component.getComponentName() + ":", Style.BOLD), "spanx, wrap 20lp"); - - JCheckBox check; - BooleanModel bm; - UnitSelector us; - BasicSlider bs; - - //// Mass - bm = new BooleanModel(component, "MassOverridden"); - check = new JCheckBox(bm); - //// Override mass: - check.setText(trans.get("RocketCompCfg.checkbox.Overridemass")); - panel.add(check, "growx 1, gapright 20lp"); - - DoubleModel m = new DoubleModel(component, "OverrideMass", UnitGroup.UNITS_MASS, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - bm.addEnableComponent(spin, true); - panel.add(spin, "growx 1"); - - us = new UnitSelector(m); - bm.addEnableComponent(us, true); - panel.add(us, "growx 1"); - - bs = new BasicSlider(m.getSliderModel(0, 0.03, 1.0)); - bm.addEnableComponent(bs); - panel.add(bs, "growx 5, w 100lp, wrap"); - - - //// CG override - bm = new BooleanModel(component, "CGOverridden"); - check = new JCheckBox(bm); - //// Override center of gravity:" - check.setText(trans.get("RocketCompCfg.checkbox.Overridecenterofgrav")); - panel.add(check, "growx 1, gapright 20lp"); - - m = new DoubleModel(component, "OverrideCGX", UnitGroup.UNITS_LENGTH, 0); - // Calculate suitable length for slider - DoubleModel length; - if (component instanceof ComponentAssembly) { - double l = 0; - - Iterator<RocketComponent> iterator = component.iterator(false); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - if (c.getRelativePosition() == RocketComponent.Position.AFTER) - l += c.getLength(); - } - length = new DoubleModel(l); - } else { - length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - } - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - bm.addEnableComponent(spin, true); - panel.add(spin, "growx 1"); - - us = new UnitSelector(m); - bm.addEnableComponent(us, true); - panel.add(us, "growx 1"); - - bs = new BasicSlider(m.getSliderModel(new DoubleModel(0), length)); - bm.addEnableComponent(bs); - panel.add(bs, "growx 5, w 100lp, wrap 35lp"); - - - // Override subcomponents checkbox - bm = new BooleanModel(component, "OverrideSubcomponents"); - check = new JCheckBox(bm); - //// Override mass and CG of all subcomponents - check.setText(trans.get("RocketCompCfg.checkbox.OverridemassandCG")); - panel.add(check, "gap para, spanx, wrap para"); - - //// <html>The overridden mass does not include motors.<br> - panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.longB1") + - //// The center of gravity is measured from the front end of the - trans.get("RocketCompCfg.lbl.longB2") + " " + - component.getComponentName().toLowerCase() + ".", -1), - "spanx, wrap, gap para, height 0::30lp"); - - return panel; - } - - - private JPanel commentTab() { - JPanel panel = new JPanel(new MigLayout("fill")); - - //// Comments on the - panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.Commentsonthe") + " " + component.getComponentName() + ":", - Style.BOLD), "wrap"); - - // TODO: LOW: Changes in comment from other sources not reflected in component - commentTextArea = new JTextArea(component.getComment()); - commentTextArea.setLineWrap(true); - commentTextArea.setWrapStyleWord(true); - commentTextArea.setEditable(true); - GUIUtil.setTabToFocusing(commentTextArea); - commentTextArea.addFocusListener(textFieldListener); - - panel.add(new JScrollPane(commentTextArea), "width 10px, height 10px, growx, growy"); - - return panel; - } - - - - private JPanel figureTab() { - JPanel panel = new JPanel(new MigLayout("align 20% 20%")); - - //// Figure style: - panel.add(new StyledLabel(trans.get("RocketCompCfg.lbl.Figurestyle"), Style.BOLD), "wrap para"); - - //// Component color: - panel.add(new JLabel(trans.get("RocketCompCfg.lbl.Componentcolor")), "gapleft para, gapright 10lp"); - - colorButton = new JButton(new ColorIcon(getColor())); - colorButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - net.sf.openrocket.util.Color c = component.getColor(); - if (c == null) { - c = Application.getPreferences().getDefaultColor(component.getClass()); - } - - //// Choose color - Color awtColor = ColorConversion.toAwtColor(c); - awtColor = JColorChooser.showDialog(tabbedPane, trans.get("RocketCompCfg.lbl.Choosecolor"), awtColor); - c = ColorConversion.fromAwtColor(awtColor); - if (c != null) { - component.setColor(c); - } - } - }); - panel.add(colorButton, "gapright 10lp"); - - //// Use default color - colorDefault = new JCheckBox(trans.get("RocketCompCfg.checkbox.Usedefaultcolor")); - if (component.getColor() == null) - colorDefault.setSelected(true); - colorDefault.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (colorDefault.isSelected()) - component.setColor(null); - else - component.setColor(((SwingPreferences) Application.getPreferences()).getDefaultColor(component.getClass())); - } - }); - panel.add(colorDefault, "wrap para"); - - //// Component line style: - panel.add(new JLabel(trans.get("RocketCompCfg.lbl.Complinestyle")), "gapleft para, gapright 10lp"); - - LineStyle[] list = new LineStyle[LineStyle.values().length + 1]; - System.arraycopy(LineStyle.values(), 0, list, 1, LineStyle.values().length); - - JComboBox combo = new JComboBox(new EnumModel<LineStyle>(component, "LineStyle", - //// Default style - list, trans.get("LineStyle.Defaultstyle"))); - panel.add(combo, "spanx 2, growx, wrap 50lp"); - - //// Save as default style - JButton button = new JButton(trans.get("RocketCompCfg.but.Saveasdefstyle")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (component.getColor() != null) { - ((SwingPreferences) Application.getPreferences()).setDefaultColor(component.getClass(), component.getColor()); - component.setColor(null); - } - if (component.getLineStyle() != null) { - Application.getPreferences().setDefaultLineStyle(component.getClass(), component.getLineStyle()); - component.setLineStyle(null); - } - } - }); - panel.add(button, "gapleft para, spanx 3, growx, wrap"); - - return panel; - } - - - private Color getColor() { - net.sf.openrocket.util.Color c = component.getColor(); - if (c == null) { - c = Application.getPreferences().getDefaultColor(component.getClass()); - } - return ColorConversion.toAwtColor(c); - } - - - - protected JPanel shoulderTab() { - JPanel panel = new JPanel(new MigLayout("fill")); - JPanel sub; - DoubleModel m, m2; - DoubleModel m0 = new DoubleModel(0); - BooleanModel bm; - JCheckBox check; - JSpinner spin; - - - //// Fore shoulder, not for NoseCone - - if (!(component instanceof NoseCone)) { - sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Fore shoulder - sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.border.Foreshoulder"))); - - - //// Radius - //// Diameter: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); - - m = new DoubleModel(component, "ForeShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "ForeRadius", 2, UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - - //// Length: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); - - m = new DoubleModel(component, "ForeShoulderLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); - - - //// Thickness: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "ForeShoulderThickness", UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "ForeShoulderRadius", UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - - //// Capped - bm = new BooleanModel(component, "ForeShoulderCapped"); - check = new JCheckBox(bm); - //// End capped - check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); - //// Whether the end of the shoulder is capped. - check.setToolTipText(trans.get("RocketCompCfg.ttip.Endcapped")); - sub.add(check, "spanx"); - - - panel.add(sub); - } - - - //// Aft shoulder - sub = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - if (component instanceof NoseCone) - //// Nose cone shoulder - sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Noseconeshoulder"))); - else - //// Aft shoulder - sub.setBorder(BorderFactory.createTitledBorder(trans.get("RocketCompCfg.title.Aftshoulder"))); - - - //// Radius - //// Diameter: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Diameter"))); - - m = new DoubleModel(component, "AftShoulderRadius", 2, UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "AftRadius", 2, UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - - //// Length: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Length"))); - - m = new DoubleModel(component, "AftShoulderLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)), "w 100lp, wrap"); - - - //// Thickness: - sub.add(new JLabel(trans.get("RocketCompCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "AftShoulderThickness", UnitGroup.UNITS_LENGTH, 0); - m2 = new DoubleModel(component, "AftShoulderRadius", UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - sub.add(spin, "growx"); - - sub.add(new UnitSelector(m), "growx"); - sub.add(new BasicSlider(m.getSliderModel(m0, m2)), "w 100lp, wrap"); - - - //// Capped - bm = new BooleanModel(component, "AftShoulderCapped"); - check = new JCheckBox(bm); - //// End capped - check.setText(trans.get("RocketCompCfg.checkbox.Endcapped")); - //// Whether the end of the shoulder is capped. - check.setToolTipText(trans.get("RocketCompCfg.ttip.Endcapped")); - sub.add(check, "spanx"); - - - panel.add(sub); - - - return panel; - } - - - - - /* - * Private inner class to handle events in componentNameField. - */ - private class TextFieldListener implements ActionListener, FocusListener { - @Override - public void actionPerformed(ActionEvent e) { - setName(); - } - - @Override - public void focusGained(FocusEvent e) { - } - - @Override - public void focusLost(FocusEvent e) { - setName(); - } - - private void setName() { - if (!component.getName().equals(componentNameField.getText())) { - component.setName(componentNameField.getText()); - } - if (!component.getComment().equals(commentTextArea.getText())) { - component.setComment(commentTextArea.getText()); - } - } - } - - - protected void register(Invalidatable model) { - this.invalidatables.add(model); - } - - public void invalidateModels() { - for (Invalidatable i : invalidatables) { - i.invalidate(); - } - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/configdialog/RocketConfig.java b/src/net/sf/openrocket/gui/configdialog/RocketConfig.java deleted file mode 100644 index c692f498..00000000 --- a/src/net/sf/openrocket/gui/configdialog/RocketConfig.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.FocusEvent; -import java.awt.event.FocusListener; - -import javax.swing.JLabel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - -public class RocketConfig extends RocketComponentConfig { - private static final Translator trans = Application.getTranslator(); - - private TextFieldListener textFieldListener; - - private JTextArea designerTextArea; - private JTextArea revisionTextArea; - - private final Rocket rocket; - - public RocketConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - rocket = (Rocket) c; - - this.removeAll(); - setLayout(new MigLayout("fill")); - - //// Design name: - this.add(new JLabel(trans.get("RocketCfg.lbl.Designname")), "top, pad 4lp, gapright 10lp"); - this.add(componentNameField, "growx, wrap para"); - - //// Designer: - this.add(new JLabel(trans.get("RocketCfg.lbl.Designer")), "top, pad 4lp, gapright 10lp"); - - textFieldListener = new TextFieldListener(); - designerTextArea = new JTextArea(rocket.getDesigner()); - designerTextArea.setLineWrap(true); - designerTextArea.setWrapStyleWord(true); - designerTextArea.setEditable(true); - GUIUtil.setTabToFocusing(designerTextArea); - designerTextArea.addFocusListener(textFieldListener); - this.add(new JScrollPane(designerTextArea), "wmin 400lp, height 60lp:60lp:, grow 30, wrap para"); - - //// Comments: - this.add(new JLabel(trans.get("RocketCfg.lbl.Comments")), "top, pad 4lp, gapright 10lp"); - this.add(new JScrollPane(commentTextArea), "wmin 400lp, height 155lp:155lp:, grow 100, wrap para"); - - //// Revision history: - this.add(new JLabel(trans.get("RocketCfg.lbl.Revisionhistory")), "top, pad 4lp, gapright 10lp"); - revisionTextArea = new JTextArea(rocket.getRevision()); - revisionTextArea.setLineWrap(true); - revisionTextArea.setWrapStyleWord(true); - revisionTextArea.setEditable(true); - GUIUtil.setTabToFocusing(revisionTextArea); - revisionTextArea.addFocusListener(textFieldListener); - - this.add(new JScrollPane(revisionTextArea), "wmin 400lp, height 60lp:60lp:, grow 30, wrap para"); - - - addButtons(); - } - - - - private class TextFieldListener implements ActionListener, FocusListener { - @Override - public void actionPerformed(ActionEvent e) { - setName(); - } - - @Override - public void focusGained(FocusEvent e) { - } - - @Override - public void focusLost(FocusEvent e) { - setName(); - } - - private void setName() { - if (!rocket.getDesigner().equals(designerTextArea.getText())) { - rocket.setDesigner(designerTextArea.getText()); - } - if (!rocket.getRevision().equals(revisionTextArea.getText())) { - rocket.setRevision(revisionTextArea.getText()); - } - } - } - - - -} diff --git a/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java b/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java deleted file mode 100644 index fbc99f85..00000000 --- a/src/net/sf/openrocket/gui/configdialog/ShockCordConfig.java +++ /dev/null @@ -1,130 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class ShockCordConfig extends RocketComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public ShockCordConfig(OpenRocketDocument d, RocketComponent component) { - super(d, component); - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - JLabel label; - DoubleModel m; - JSpinner spin; - String tip; - - - ////// Left side - - // Cord length - //// Shock cord length - label = new JLabel(trans.get("ShockCordCfg.lbl.Shockcordlength")); - panel.add(label); - - m = new DoubleModel(component, "CordLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 1, 10)), "w 100lp, wrap"); - - - // Material - //// Shock cord material: - materialPanel(panel, Material.Type.LINE, trans.get("ShockCordCfg.lbl.Shockcordmaterial"), null); - - - - ///// Right side - JPanel panel2 = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany"); - - - //// Position - //// Position relative to: - panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.Posrelativeto"))); - - JComboBox combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel2.add(combo, "spanx, growx, wrap"); - - //// plus - panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel2.add(spin, "growx"); - - panel2.add(new UnitSelector(m), "growx"); - panel2.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - - //// Spatial length - //// Packed length: - panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.Packedlength"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel2.add(spin, "growx"); - - panel2.add(new UnitSelector(m), "growx"); - panel2.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); - - - //// Tube diameter - //// Packed diameter: - panel2.add(new JLabel(trans.get("ShockCordCfg.lbl.Packeddiam"))); - - DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel2.add(spin, "growx"); - - panel2.add(new UnitSelector(od), "growx"); - panel2.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap"); - - - - //// General and General properties - tabbedPane.insertTab(trans.get("ShockCordCfg.tab.General"), null, panel, trans.get("ShockCordCfg.tab.ttip.General"), 0); - // tabbedPane.insertTab("Radial position", null, positionTab(), - // "Radial position configuration", 1); - tabbedPane.setSelectedIndex(0); - } - - -} diff --git a/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java b/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java deleted file mode 100644 index 32192765..00000000 --- a/src/net/sf/openrocket/gui/configdialog/SleeveConfig.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JPanel; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - - - -public class SleeveConfig extends RingComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public SleeveConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel tab; - //// Outer diameter: - //// Inner diameter: - //// Wall thickness: - //// Length: - tab = generalTab(trans.get("SleeveCfg.tab.Outerdiam"), trans.get("SleeveCfg.tab.Innerdiam"), - trans.get("SleeveCfg.tab.Wallthickness"), trans.get("SleeveCfg.tab.Length")); - //// General and General properties - tabbedPane.insertTab(trans.get("SleeveCfg.tab.General"), null, tab, - trans.get("SleeveCfg.tab.Generalproperties"), 0); - tabbedPane.setSelectedIndex(0); - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java b/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java deleted file mode 100644 index 8d7be304..00000000 --- a/src/net/sf/openrocket/gui/configdialog/StreamerConfig.java +++ /dev/null @@ -1,291 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.MaterialModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.HtmlLabel; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class StreamerConfig extends RecoveryDeviceConfig { - private static final Translator trans = Application.getTranslator(); - - public StreamerConfig(OpenRocketDocument d, final RocketComponent component) { - super(d, component); - - JPanel primary = new JPanel(new MigLayout()); - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - - //// Strip length: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Striplength"))); - - DoubleModel m = new DoubleModel(component, "StripLength", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.6, 1.5)), "w 100lp, wrap"); - - //// Strip width: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Stripwidth"))); - - m = new DoubleModel(component, "StripWidth", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.2)), "w 100lp, wrap 20lp"); - - - - //// Strip area: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Striparea"))); - - m = new DoubleModel(component, "Area", UnitGroup.UNITS_AREA, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.25)), "w 100lp, wrap"); - - //// Aspect ratio: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Aspectratio"))); - - m = new DoubleModel(component, "AspectRatio", UnitGroup.UNITS_NONE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - // panel.add(new UnitSelector(m),"growx"); - panel.add(new BasicSlider(m.getSliderModel(2, 15)), "skip, w 100lp, wrap 20lp"); - - - //// Material: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Material"))); - - JComboBox combo = new JComboBox(new MaterialModel(panel, component, - Material.Type.SURFACE)); - //// The component material affects the weight of the component. - combo.setToolTipText(trans.get("StreamerCfg.combo.ttip.MaterialModel")); - panel.add(combo, "spanx 3, growx, wrap 20lp"); - - - - // CD - //// <html>Drag coefficient C<sub>D</sub>: - JLabel label = new HtmlLabel(trans.get("StreamerCfg.lbl.longA1")); - //// <html>The drag coefficient relative to the total area of the streamer.<br> - String tip = trans.get("StreamerCfg.lbl.longB1") + - //// "A larger drag coefficient yields a slowed descent rate. - trans.get("StreamerCfg.lbl.longB2"); - label.setToolTipText(tip); - panel.add(label); - - m = new DoubleModel(component, "CD", UnitGroup.UNITS_COEFFICIENT, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setToolTipText(tip); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - JCheckBox check = new JCheckBox(m.getAutomaticAction()); - //// Automatic - check.setText(trans.get("StreamerCfg.lbl.Automatic")); - panel.add(check, "skip, span, wrap"); - - //// The drag coefficient is relative to the area of the streamer. - panel.add(new StyledLabel(trans.get("StreamerCfg.lbl.longC1"), - -2), "span, wrap"); - - - - primary.add(panel, "grow, gapright 20lp"); - panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - - - - - //// Position - //// Position relative to: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Posrelativeto"))); - - combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx, growx, wrap"); - - //// plus - panel.add(new JLabel(trans.get("StreamerCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap"); - - - //// Spatial length: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Packedlength"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)), "w 100lp, wrap"); - - - //// Tube diameter - //// Packed diameter: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Packeddiam"))); - - DoubleModel od = new DoubleModel(component, "Radius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 30lp"); - - - //// Deployment - //// Deploys at: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Deploysat")), ""); - - combo = new JComboBox(new EnumModel<IgnitionEvent>(component, "DeployEvent")); - panel.add(combo, "spanx 3, growx, wrap"); - - // ... and delay - //// plus - panel.add(new JLabel(trans.get("StreamerCfg.lbl.plusdelay")), "right"); - - m = new DoubleModel(component, "DeployDelay", 0); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "spanx, split"); - - //// seconds - panel.add(new JLabel(trans.get("StreamerCfg.lbl.seconds")), "wrap paragraph"); - - // Altitude: - label = new JLabel(trans.get("StreamerCfg.lbl.Altitude")); - altitudeComponents.add(label); - panel.add(label); - - m = new DoubleModel(component, "DeployAltitude", UnitGroup.UNITS_DISTANCE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - altitudeComponents.add(spin); - panel.add(spin, "growx"); - UnitSelector unit = new UnitSelector(m); - altitudeComponents.add(unit); - panel.add(unit, "growx"); - BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000)); - altitudeComponents.add(slider); - panel.add(slider, "w 100lp, wrap"); - - - primary.add(panel, "grow"); - - updateFields(); - - //// General and General properties - tabbedPane.insertTab(trans.get("StreamerCfg.tab.General"), null, primary, - trans.get("StreamerCfg.tab.ttip.General"), 0); - //// Radial position and Radial position configuration - tabbedPane.insertTab(trans.get("StreamerCfg.tab.Radialpos"), null, positionTab(), - trans.get("StreamerCfg.tab.ttip.Radialpos"), 1); - tabbedPane.setSelectedIndex(0); - } - - - - - - protected JPanel positionTab() { - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Radial position - //// Radial distance: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Radialdistance"))); - - DoubleModel m = new DoubleModel(component, "RadialPosition", UnitGroup.UNITS_LENGTH, 0); - - JSpinner spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)), "w 100lp, wrap"); - - - //// Radial direction - //// Radial direction: - panel.add(new JLabel(trans.get("StreamerCfg.lbl.Radialdirection"))); - - m = new DoubleModel(component, "RadialDirection", UnitGroup.UNITS_ANGLE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - //// Reset button - JButton button = new JButton(trans.get("StreamerCfg.but.Reset")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ((MassComponent) component).setRadialDirection(0.0); - ((MassComponent) component).setRadialPosition(0.0); - } - }); - panel.add(button, "spanx, right"); - - return panel; - } -} diff --git a/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java b/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java deleted file mode 100644 index 3013b7c9..00000000 --- a/src/net/sf/openrocket/gui/configdialog/ThicknessRingComponentConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JPanel; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - - - -public class ThicknessRingComponentConfig extends RingComponentConfig { - private static final Translator trans = Application.getTranslator(); - - public ThicknessRingComponentConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - JPanel tab; - - //// Outer diameter: - //// Inner diameter: - //// Wall thickness: - //// Length: - tab = generalTab(trans.get("ThicknessRingCompCfg.tab.Outerdiam"), - trans.get("ThicknessRingCompCfg.tab.Innerdiam"), - trans.get("ThicknessRingCompCfg.tab.Wallthickness"), trans.get("ThicknessRingCompCfg.tab.Length")); - //// General and General properties - tabbedPane.insertTab(trans.get("ThicknessRingCompCfg.tab.General"), null, tab, - trans.get("ThicknessRingCompCfg.tab.Generalprop"), 0); - tabbedPane.setSelectedIndex(0); - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java b/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java deleted file mode 100644 index 5e0286d8..00000000 --- a/src/net/sf/openrocket/gui/configdialog/TransitionConfig.java +++ /dev/null @@ -1,209 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.BooleanModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -public class TransitionConfig extends RocketComponentConfig { - - private static final Translator trans = Application.getTranslator(); - private JComboBox typeBox; - //private JLabel description; - - private JLabel shapeLabel; - private JSpinner shapeSpinner; - private BasicSlider shapeSlider; - private DescriptionArea description; - - - // Prepended to the description from Transition.DESCRIPTIONS - private static final String PREDESC = "<html>"; - - - public TransitionConfig(OpenRocketDocument d, RocketComponent c) { - super(d, c); - - DoubleModel m; - JSpinner spin; - JCheckBox checkbox; - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - - - //// Shape selection - //// Transition shape: - panel.add(new JLabel(trans.get("TransitionCfg.lbl.Transitionshape"))); - - Transition.Shape selected = ((Transition) component).getType(); - Transition.Shape[] typeList = Transition.Shape.values(); - - typeBox = new JComboBox(typeList); - typeBox.setEditable(false); - typeBox.setSelectedItem(selected); - typeBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Transition.Shape s = (Transition.Shape) typeBox.getSelectedItem(); - ((Transition) component).setType(s); - description.setText(PREDESC + s.getTransitionDescription()); - updateEnabled(); - } - }); - panel.add(typeBox, "span, split 2"); - - //// Clipped - checkbox = new JCheckBox(new BooleanModel(component, "Clipped")); - //// Clipped - checkbox.setText(trans.get("TransitionCfg.checkbox.Clipped")); - panel.add(checkbox, "wrap"); - - - //// Shape parameter: - shapeLabel = new JLabel(trans.get("TransitionCfg.lbl.Shapeparam")); - panel.add(shapeLabel); - - m = new DoubleModel(component, "ShapeParameter"); - - shapeSpinner = new JSpinner(m.getSpinnerModel()); - shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner)); - panel.add(shapeSpinner, "growx"); - - DoubleModel min = new DoubleModel(component, "ShapeParameterMin"); - DoubleModel max = new DoubleModel(component, "ShapeParameterMax"); - shapeSlider = new BasicSlider(m.getSliderModel(min, max)); - panel.add(shapeSlider, "skip, w 100lp, wrap"); - - updateEnabled(); - - - //// Length - //// Transition length: - panel.add(new JLabel(trans.get("TransitionCfg.lbl.Transitionlength"))); - - m = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.3)), "w 100lp, wrap"); - - - //// Transition diameter 1 - //// Fore diameter: - panel.add(new JLabel(trans.get("TransitionCfg.lbl.Forediam"))); - - DoubleModel od = new DoubleModel(component, "ForeRadius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); - - checkbox = new JCheckBox(od.getAutomaticAction()); - //// Automatic - checkbox.setText(trans.get("TransitionCfg.checkbox.Automatic")); - panel.add(checkbox, "skip, span 2, wrap"); - - - //// Transition diameter 2 - //// Aft diameter: - panel.add(new JLabel(trans.get("TransitionCfg.lbl.Aftdiam"))); - - od = new DoubleModel(component, "AftRadius", 2, UnitGroup.UNITS_LENGTH, 0); - // Diameter = 2*Radius - - spin = new JSpinner(od.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(od), "growx"); - panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap 0px"); - - checkbox = new JCheckBox(od.getAutomaticAction()); - //// Automatic - checkbox.setText(trans.get("TransitionCfg.checkbox.Automatic")); - panel.add(checkbox, "skip, span 2, wrap"); - - - //// Wall thickness: - panel.add(new JLabel(trans.get("TransitionCfg.lbl.Wallthickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap 0px"); - - //// Filled - checkbox = new JCheckBox(new BooleanModel(component, "Filled")); - //// Filled - checkbox.setText(trans.get("TransitionCfg.checkbox.Filled")); - panel.add(checkbox, "skip, span 2, wrap"); - - - - //// Description - - JPanel panel2 = new JPanel(new MigLayout("ins 0")); - - description = new DescriptionArea(5); - description.setText(PREDESC + ((Transition) component).getType(). - getTransitionDescription()); - panel2.add(description, "wmin 250lp, spanx, growx, wrap para"); - - - //// Material - - - materialPanel(panel2, Material.Type.BULK); - panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany"); - - //// General and General properties - tabbedPane.insertTab(trans.get("TransitionCfg.tab.General"), null, panel, - trans.get("TransitionCfg.tab.Generalproperties"), 0); - //// Shoulder and Shoulder properties - tabbedPane.insertTab(trans.get("TransitionCfg.tab.Shoulder"), null, shoulderTab(), - trans.get("TransitionCfg.tab.Shoulderproperties"), 1); - tabbedPane.setSelectedIndex(0); - } - - - - private void updateEnabled() { - boolean e = ((Transition) component).getType().usesParameter(); - shapeLabel.setEnabled(e); - shapeSpinner.setEnabled(e); - shapeSlider.setEnabled(e); - } - -} diff --git a/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java b/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java deleted file mode 100644 index 47650ffd..00000000 --- a/src/net/sf/openrocket/gui/configdialog/TrapezoidFinSetConfig.java +++ /dev/null @@ -1,244 +0,0 @@ -package net.sf.openrocket.gui.configdialog; - - -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSeparator; -import javax.swing.JSpinner; -import javax.swing.SwingConstants; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.IntegerModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - - -public class TrapezoidFinSetConfig extends FinSetConfig { - private static final Translator trans = Application.getTranslator(); - - public TrapezoidFinSetConfig(OpenRocketDocument d, final RocketComponent component) { - super(d, component); - - DoubleModel m; - JSpinner spin; - JComboBox combo; - - JPanel mainPanel = new JPanel(new MigLayout()); - - - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - //// Number of fins: - JLabel label = new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Nbroffins")); - //// The number of fins in the fin set. - label.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Nbroffins")); - panel.add(label); - - IntegerModel im = new IntegerModel(component, "FinCount", 1, 8); - - spin = new JSpinner(im.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - //// The number of fins in the fin set. - spin.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Nbroffins")); - panel.add(spin, "growx, wrap"); - - - //// Base rotation - //// Fin rotation: - label = new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Finrotation")); - //// The angle of the first fin in the fin set. - label.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Finrotation")); - panel.add(label); - - m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE, -Math.PI, Math.PI); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)), "w 100lp, wrap"); - - - //// Fin cant: - label = new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Fincant")); - //// The angle that the fins are canted with respect to the rocket - label.setToolTipText(trans.get("TrapezoidFinSetCfg.lbl.ttip.Fincant")); - panel.add(label); - - m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE, - -FinSet.MAX_CANT, FinSet.MAX_CANT); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT, FinSet.MAX_CANT)), - "w 100lp, wrap"); - - - //// Root chord: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Rootchord"))); - - m = new DoubleModel(component, "RootChord", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); - - - - //// Tip chord: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Tipchord"))); - - m = new DoubleModel(component, "TipChord", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); - - - //// Height: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Height"))); - - m = new DoubleModel(component, "Height", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.2)), "w 100lp, wrap"); - - - - //// Sweep length: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Sweeplength"))); - - m = new DoubleModel(component, "Sweep", UnitGroup.UNITS_LENGTH); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - - // sweep slider from -1.1*TipChord to 1.1*RootChord - DoubleModel tc = new DoubleModel(component, "TipChord", -1.1, UnitGroup.UNITS_LENGTH); - DoubleModel rc = new DoubleModel(component, "RootChord", 1.1, UnitGroup.UNITS_LENGTH); - panel.add(new BasicSlider(m.getSliderModel(tc, rc)), "w 100lp, wrap"); - - - //// Sweep angle: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Sweepangle"))); - - m = new DoubleModel(component, "SweepAngle", UnitGroup.UNITS_ANGLE, - -TrapezoidFinSet.MAX_SWEEP_ANGLE, TrapezoidFinSet.MAX_SWEEP_ANGLE); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(-Math.PI / 4, Math.PI / 4)), - "w 100lp, wrap paragraph"); - - - - - - mainPanel.add(panel, "aligny 20%"); - - mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy"); - - - - panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::]", "")); - - - - //// Fin cross section: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.FincrossSection"))); - combo = new JComboBox( - new EnumModel<FinSet.CrossSection>(component, "CrossSection")); - panel.add(combo, "span, growx, wrap"); - - - //// Thickness: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Thickness"))); - - m = new DoubleModel(component, "Thickness", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel(0, 0.01)), "w 100lp, wrap para"); - - - //// Position - //// Position relative to: - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.Posrelativeto"))); - - combo = new JComboBox( - new EnumModel<RocketComponent.Position>(component, "RelativePosition", - new RocketComponent.Position[] { - RocketComponent.Position.TOP, - RocketComponent.Position.MIDDLE, - RocketComponent.Position.BOTTOM, - RocketComponent.Position.ABSOLUTE - })); - panel.add(combo, "spanx, growx, wrap"); - //// plus - panel.add(new JLabel(trans.get("TrapezoidFinSetCfg.lbl.plus")), "right"); - - m = new DoubleModel(component, "PositionValue", UnitGroup.UNITS_LENGTH); - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - panel.add(spin, "growx"); - - panel.add(new UnitSelector(m), "growx"); - panel.add(new BasicSlider(m.getSliderModel( - new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE), - new DoubleModel(component.getParent(), "Length"))), - "w 100lp, wrap para"); - - - - //// Material - materialPanel(panel, Material.Type.BULK); - - - - - mainPanel.add(panel, "aligny 20%"); - - //// General and General properties - tabbedPane.insertTab(trans.get("TrapezoidFinSetCfg.tab.General"), null, mainPanel, - trans.get("TrapezoidFinSetCfg.tab.Generalproperties"), 0); - tabbedPane.setSelectedIndex(0); - - addFinSetButtons(); - - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/AboutDialog.java b/src/net/sf/openrocket/gui/dialogs/AboutDialog.java deleted file mode 100644 index 7aaadd24..00000000 --- a/src/net/sf/openrocket/gui/dialogs/AboutDialog.java +++ /dev/null @@ -1,125 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.components.URLLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.Chars; - -public class AboutDialog extends JDialog { - - public static final String OPENROCKET_URL = "http://openrocket.sourceforge.net/"; - private static final Translator trans = Application.getTranslator(); - - private static final String CREDITS = "<html><center>" + - "<font size=\"+1\"><b>OpenRocket has been developed by:</b></font><br><br>" + - "Sampo Niskanen (main developer)<br>" + - "Doug Pedrick (RockSim file format, printing)<br>" + - "Boris du Reau (internationalization, translation lead)<br>" + - "Richard Graham (geodetic computations)<br><br>" + - "<b>Translations by:</b><br><br>" + - "Tripoli France (French)<br>" + - "Stefan Lobas / ERIG e.V. (German)<br>" + - "Tripoli Spain (Spanish)<br><br>" + - "<b>OpenRocket utilizes the following libraries:</b><br><br>" + - "MiG Layout (http://www.miglayout.com/)<br>" + - "JFreeChart (http://www.jfree.org/jfreechart/)<br>" + - "iText (http://www.itextpdf.com/)"; - - - public AboutDialog(JFrame parent) { - super(parent, true); - - final String version = BuildProperties.getVersion(); - - JPanel panel = new JPanel(new MigLayout("fill")); - JPanel sub; - - - // OpenRocket logo - panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket")), "top"); - - - // OpenRocket version info + copyright - sub = new JPanel(new MigLayout("fill")); - - sub.add(new StyledLabel("OpenRocket", 20), "ax 50%, growy, wrap para"); - sub.add(new StyledLabel(trans.get("lbl.version").trim() + " " + version, 3), "ax 50%, growy, wrap rel"); - sub.add(new StyledLabel("Copyright " + Chars.COPY + " 2007-2011 Sampo Niskanen"), "ax 50%, growy, wrap para"); - - sub.add(new URLLabel(OPENROCKET_URL), "ax 50%, growy, wrap para"); - panel.add(sub, "grow"); - - - // Translation information (if present) - String translation = trans.get("lbl.translation").trim(); - String translator = trans.get("lbl.translator").trim(); - String translatorWebsite = trans.get("lbl.translatorWebsite").trim(); - String translatorIcon = trans.get("lbl.translatorIcon").trim(); - - if (translator.length() > 0 || translatorWebsite.length() > 0 || translatorIcon.length() > 0) { - sub = new JPanel(new MigLayout("fill")); - - sub.add(new StyledLabel(translation, Style.BOLD), "ax 50%, growy, wrap para"); - - if (translatorIcon.length() > 0) { - sub.add(new JLabel(Icons.loadImageIcon("pix/translators/" + translatorIcon, translator)), - "ax 50%, growy, wrap para"); - } - if (translator.length() > 0) { - sub.add(new JLabel(translator), "ax 50%, growy, wrap rel"); - } - if (translatorWebsite.length() > 0) { - sub.add(new URLLabel(translatorWebsite), "ax 50%, growy, wrap para"); - } - - panel.add(sub); - } - - - DescriptionArea info = new DescriptionArea(5); - info.setText(CREDITS); - panel.add(info, "newline, width 10px, height 100lp, grow, spanx, wrap para"); - - // JTextArea area = new JTextArea(CREATORS); - // area.setEditable(false); - // area.setLineWrap(true); - // area.setWrapStyleWord(true); - // panel.add(new JScrollPane(area), "width 10px, height 100lp, grow, spanx, wrap para"); - - - //Close button - JButton close = new JButton(trans.get("button.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - AboutDialog.this.dispose(); - } - }); - panel.add(close, "spanx, right"); - - this.add(panel); - this.setTitle("OpenRocket " + version); - this.pack(); - this.setResizable(false); - this.setLocationRelativeTo(parent); - - GUIUtil.setDisposableDialogOptions(this, close); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java b/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java deleted file mode 100644 index 9f203f36..00000000 --- a/src/net/sf/openrocket/gui/dialogs/BugReportDialog.java +++ /dev/null @@ -1,352 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Desktop; -import java.awt.Dialog; -import java.awt.Dimension; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.util.List; -import java.util.Locale; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.communication.BugReporter; -import net.sf.openrocket.gui.components.SelectableLabel; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogLevelBufferLogger; -import net.sf.openrocket.logging.LogLine; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.JarUtil; - -public class BugReportDialog extends JDialog { - - private static final String REPORT_EMAIL = "openrocket-bugs@lists.sourceforge.net"; - private static final Translator trans = Application.getTranslator(); - - - public BugReportDialog(Window parent, String labelText, final String message, final boolean sendIfUnchanged) { - //// Bug report - super(parent, trans.get("bugreport.dlg.title"), Dialog.ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill")); - - // Some fscking Swing bug that makes html labels initially way too high - JLabel label = new JLabel(labelText); - Dimension d = label.getPreferredSize(); - d.width = 100000; - label.setMaximumSize(d); - panel.add(label, "gapleft para, wrap para"); - - //// <html>If connected to the Internet, you can simply click - //// <em>Send bug report</em>. - label = new JLabel(trans.get("bugreport.dlg.connectedInternet")); - d = label.getPreferredSize(); - d.width = 100000; - label.setMaximumSize(d); - panel.add(label, "gapleft para, wrap"); - - //// Otherwise, send the text below to the address: - panel.add(new JLabel(trans.get("bugreport.dlg.otherwise") + " "), - "gapleft para, split 2, gapright rel"); - panel.add(new SelectableLabel(REPORT_EMAIL), "growx, wrap para"); - - - final JTextArea textArea = new JTextArea(message, 20, 70); - textArea.setEditable(true); - panel.add(new JScrollPane(textArea), "grow, wrap"); - - - panel.add(new StyledLabel(trans.get("bugreport.lbl.Theinformation"), -1), "wrap para"); - - - - ////Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - BugReportDialog.this.dispose(); - } - }); - panel.add(close, "right, sizegroup buttons, split"); - - - //// Mail button - // if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Action.MAIL)) { - // JButton mail = new JButton("Open email"); - // mail.setToolTipText("Open email client with the suitable email ready."); - // mail.addActionListener(new ActionListener() { - // @Override - // public void actionPerformed(ActionEvent e) { - // String text = textArea.getText(); - // openEmail(text); - // } - // }); - // panel.add(mail, "right, sizegroup buttons"); - // } - - - //// Send bug report button - JButton send = new JButton(trans.get("bugreport.dlg.but.Sendbugreport")); - //// Automatically send the bug report to the OpenRocket developers. - send.setToolTipText(trans.get("bugreport.dlg.but.Sendbugreport.Ttip")); - send.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - String text = textArea.getText(); - if (text.equals(message) && !sendIfUnchanged) { - JOptionPane.showMessageDialog(BugReportDialog.this, - trans.get("bugreport.dlg.provideDescription"), - trans.get("bugreport.dlg.provideDescription.title"), JOptionPane.ERROR_MESSAGE); - return; - } - - try { - - BugReporter.sendBugReport(text); - - // Success if we came here - //bugreport.dlg.successmsg - /*JOptionPane.showMessageDialog(BugReportDialog.this, - new Object[] { "Bug report successfully sent.", - "Thank you for helping make OpenRocket better!" }, - "Bug report sent", JOptionPane.INFORMATION_MESSAGE);*/ - JOptionPane.showMessageDialog(BugReportDialog.this, - new Object[] { trans.get("bugreport.dlg.successmsg1"), - trans.get("bugreport.dlg.successmsg2") }, - trans.get("bugreport.dlg.successmsg3"), JOptionPane.INFORMATION_MESSAGE); - - } catch (Exception ex) { - // Sending the message failed. - JOptionPane.showMessageDialog(BugReportDialog.this, - //// OpenRocket was unable to send the bug report: - new Object[] { trans.get("bugreport.dlg.failedmsg1"), - ex.getClass().getSimpleName() + ": " + ex.getMessage(), " ", - //// Please send the report manually to - trans.get("bugreport.dlg.failedmsg2") + " " + REPORT_EMAIL }, - //// Error sending report - trans.get("bugreport.dlg.failedmsg3"), JOptionPane.ERROR_MESSAGE); - } - } - }); - panel.add(send, "right, sizegroup buttons"); - - this.add(panel); - - this.validate(); - this.pack(); - this.pack(); - this.setLocationRelativeTo(parent); - - GUIUtil.setDisposableDialogOptions(this, send); - } - - - - /** - * Show a general bug report dialog allowing the user to input information about - * the bug they encountered. - * - * @param parent the parent window (may be null). - */ - public static void showBugReportDialog(Window parent) { - - StringBuilder sb = new StringBuilder(); - - sb.append("---------- Bug report ----------\n"); - sb.append('\n'); - sb.append("Include detailed steps on how to trigger the bug:\n"); - sb.append('\n'); - sb.append("1. \n"); - sb.append("2. \n"); - sb.append("3. \n"); - sb.append('\n'); - - sb.append("What does the software do and what in your opinion should it do in the " + - "case described above:\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - - sb.append("Include your email address (optional; it helps if we can " + - "contact you in case we need additional information):\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - - - sb.append("(Do not modify anything below this line.)\n"); - sb.append("---------- System information ----------\n"); - addSystemInformation(sb); - sb.append("---------- Error log ----------\n"); - addErrorLog(sb); - sb.append("---------- End of bug report ----------\n"); - sb.append('\n'); - - BugReportDialog reportDialog = new BugReportDialog(parent, - trans.get("bugreport.reportDialog.txt"), sb.toString(), false); - reportDialog.setVisible(true); - } - - - /** - * Show a dialog presented when an uncaught exception occurs. - * - * @param parent the parent window (may be null). - * @param t the thread that encountered the exception (may be null). - * @param e the exception. - */ - public static void showExceptionDialog(Window parent, Thread t, Throwable e) { - StringBuilder sb = new StringBuilder(); - - sb.append("---------- Bug report ----------\n"); - sb.append('\n'); - sb.append("Please include a description about what actions you were " + - "performing when the exception occurred:\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - - - sb.append("Include your email address (optional; it helps if we can " + - "contact you in case we need additional information):\n"); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - sb.append('\n'); - - sb.append("(Do not modify anything below this line.)\n"); - sb.append("---------- Exception stack trace ----------\n"); - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - e.printStackTrace(pw); - sb.append(sw.getBuffer()); - sb.append('\n'); - - - sb.append("---------- Thread information ----------\n"); - if (t == null) { - sb.append("Thread is not specified."); - } else { - sb.append(t + "\n"); - } - sb.append('\n'); - - - sb.append("---------- System information ----------\n"); - addSystemInformation(sb); - sb.append("---------- Error log ----------\n"); - addErrorLog(sb); - sb.append("---------- End of bug report ----------\n"); - sb.append('\n'); - - BugReportDialog reportDialog = - //// <html><b>Please include a short description about what you were doing when the exception occurred.</b> - new BugReportDialog(parent, trans.get("bugreport.reportDialog.txt2"), sb.toString(), true); - reportDialog.setVisible(true); - } - - - private static void addSystemInformation(StringBuilder sb) { - sb.append("OpenRocket version: " + BuildProperties.getVersion() + "\n"); - sb.append("OpenRocket source: " + BuildProperties.getBuildSource() + "\n"); - sb.append("OpenRocket location: " + JarUtil.getCurrentJarFile() + "\n"); - sb.append("Current default locale: " + Locale.getDefault() + "\n"); - sb.append("System properties:\n"); - - // Sort the keys - SortedSet<String> keys = new TreeSet<String>(); - for (Object key : System.getProperties().keySet()) { - keys.add((String) key); - } - - for (String key : keys) { - String value = System.getProperty(key); - sb.append(" " + key + "="); - if (key.equals("line.separator")) { - for (char c : value.toCharArray()) { - sb.append(String.format("\\u%04x", (int) c)); - } - } else { - sb.append(value); - } - sb.append('\n'); - } - } - - - private static void addErrorLog(StringBuilder sb) { - LogLevelBufferLogger buffer = Application.getLogBuffer(); - List<LogLine> logs = buffer.getLogs(); - for (LogLine l : logs) { - sb.append(l.toString()).append('\n'); - } - } - - - - /** - * Open the default email client with the suitable bug report. - * Note that this does not work on some systems even if Desktop.isSupported() - * claims so. - * - * @param text the bug report text. - * @return whether opening the client succeeded. - */ - private boolean openEmail(String text) { - String version; - - try { - text = URLEncoder.encode(text, "UTF-8"); - version = URLEncoder.encode(BuildProperties.getVersion(), "UTF-8"); - } catch (UnsupportedEncodingException e) { - throw new BugException(e); - } - - - - String mailto = "mailto:" + REPORT_EMAIL - + "?subject=Bug%20report%20for%20OpenRocket%20" + version - + "?body=" + text; - URI uri; - try { - uri = new URI(mailto); - } catch (URISyntaxException e) { - e.printStackTrace(); - return false; - } - - Desktop desktop = Desktop.getDesktop(); - try { - desktop.mail(uri); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - - return true; - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java b/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java deleted file mode 100644 index 635e6468..00000000 --- a/src/net/sf/openrocket/gui/dialogs/ComponentAnalysisDialog.java +++ /dev/null @@ -1,653 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import static net.sf.openrocket.unit.Unit.NOUNIT2; -import static net.sf.openrocket.util.Chars.ALPHA; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Vector; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTabbedPane; -import javax.swing.JTable; -import javax.swing.JToggleButton; -import javax.swing.ListSelectionModel; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.table.TableCellRenderer; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.aerodynamics.AerodynamicCalculator; -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.gui.adaptors.Column; -import net.sf.openrocket.gui.adaptors.ColumnTableModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.StageSelector; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.scalefigure.RocketPanel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -public class ComponentAnalysisDialog extends JDialog implements ChangeListener { - - private static ComponentAnalysisDialog singletonDialog = null; - private static final Translator trans = Application.getTranslator(); - - - private final FlightConditions conditions; - private final Configuration configuration; - private final DoubleModel theta, aoa, mach, roll; - private final JToggleButton worstToggle; - private boolean fakeChange = false; - private AerodynamicCalculator aerodynamicCalculator; - private final MassCalculator massCalculator = new BasicMassCalculator(); - - private final ColumnTableModel cpTableModel; - private final ColumnTableModel dragTableModel; - private final ColumnTableModel rollTableModel; - - private final JList warningList; - - - private final List<AerodynamicForces> cpData = new ArrayList<AerodynamicForces>(); - private final List<Coordinate> cgData = new ArrayList<Coordinate>(); - private final List<AerodynamicForces> dragData = new ArrayList<AerodynamicForces>(); - private double totalCD = 0; - private final List<AerodynamicForces> rollData = new ArrayList<AerodynamicForces>(); - - - public ComponentAnalysisDialog(final RocketPanel rocketPanel) { - ////Component analysis - super(SwingUtilities.getWindowAncestor(rocketPanel), - trans.get("componentanalysisdlg.componentanalysis")); - - JTable table; - - JPanel panel = new JPanel(new MigLayout("fill", "[][35lp::][fill][fill]")); - add(panel); - - this.configuration = rocketPanel.getConfiguration(); - this.aerodynamicCalculator = rocketPanel.getAerodynamicCalculator().newInstance(); - - - conditions = new FlightConditions(configuration); - - rocketPanel.setCPAOA(0); - aoa = new DoubleModel(rocketPanel, "CPAOA", UnitGroup.UNITS_ANGLE, 0, Math.PI); - rocketPanel.setCPMach(Application.getPreferences().getDefaultMach()); - mach = new DoubleModel(rocketPanel, "CPMach", UnitGroup.UNITS_COEFFICIENT, 0); - rocketPanel.setCPTheta(rocketPanel.getFigure().getRotation()); - theta = new DoubleModel(rocketPanel, "CPTheta", UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI); - rocketPanel.setCPRoll(0); - roll = new DoubleModel(rocketPanel, "CPRoll", UnitGroup.UNITS_ROLL); - - //// Wind direction: - panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.winddir")), "width 100lp!"); - panel.add(new UnitSelector(theta, true), "width 50lp!"); - BasicSlider slider = new BasicSlider(theta.getSliderModel(0, 2 * Math.PI)); - panel.add(slider, "growx, split 2"); - //// Worst button - worstToggle = new JToggleButton(trans.get("componentanalysisdlg.ToggleBut.worst")); - worstToggle.setSelected(true); - worstToggle.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - stateChanged(null); - } - }); - slider.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - if (!fakeChange) - worstToggle.setSelected(false); - } - }); - panel.add(worstToggle, ""); - - - warningList = new JList(); - JScrollPane scrollPane = new JScrollPane(warningList); - ////Warnings: - scrollPane.setBorder(BorderFactory.createTitledBorder(trans.get("componentanalysisdlg.TitledBorder.warnings"))); - panel.add(scrollPane, "gap paragraph, spany 4, width 300lp!, growy 1, height :100lp:, wrap"); - - ////Angle of attack: - panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.angleofattack")), "width 100lp!"); - panel.add(new UnitSelector(aoa, true), "width 50lp!"); - panel.add(new BasicSlider(aoa.getSliderModel(0, Math.PI)), "growx, wrap"); - - //// Mach number: - panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.machnumber")), "width 100lp!"); - panel.add(new UnitSelector(mach, true), "width 50lp!"); - panel.add(new BasicSlider(mach.getSliderModel(0, 3)), "growx, wrap"); - - //// Roll rate: - panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.rollrate")), "width 100lp!"); - panel.add(new UnitSelector(roll, true), "width 50lp!"); - panel.add(new BasicSlider(roll.getSliderModel(-20 * 2 * Math.PI, 20 * 2 * Math.PI)), - "growx, wrap paragraph"); - - - // Stage and motor selection: - //// Active stages: - panel.add(new JLabel(trans.get("componentanalysisdlg.lbl.activestages")), "spanx, split, gapafter rel"); - panel.add(new StageSelector(configuration), "gapafter paragraph"); - - //// Motor configuration: - JLabel label = new JLabel(trans.get("componentanalysisdlg.lbl.motorconf")); - label.setHorizontalAlignment(JLabel.RIGHT); - panel.add(label, "growx, right"); - panel.add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap"); - - - - // Tabbed pane - - JTabbedPane tabbedPane = new JTabbedPane(); - panel.add(tabbedPane, "spanx, growx, growy"); - - - // Create the CP data table - cpTableModel = new ColumnTableModel( - - //// Component - new Column(trans.get("componentanalysisdlg.TabStability.Col.Component")) { - @Override - public Object getValueAt(int row) { - RocketComponent c = cpData.get(row).getComponent(); - if (c instanceof Rocket) { - return "Total"; - } - return c.toString(); - } - - @Override - public int getDefaultWidth() { - return 200; - } - }, - new Column("CG / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) { - private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - - @Override - public Object getValueAt(int row) { - return unit.toString(cgData.get(row).x); - } - }, - new Column("Mass / " + UnitGroup.UNITS_MASS.getDefaultUnit().getUnit()) { - private Unit unit = UnitGroup.UNITS_MASS.getDefaultUnit(); - - @Override - public Object getValueAt(int row) { - return unit.toString(cgData.get(row).weight); - } - }, - new Column("CP / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) { - private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - - @Override - public Object getValueAt(int row) { - return unit.toString(cpData.get(row).getCP().x); - } - }, - new Column("<html>C<sub>N<sub>" + ALPHA + "</sub></sub>") { - @Override - public Object getValueAt(int row) { - return NOUNIT2.toString(cpData.get(row).getCP().weight); - } - } - - ) { - @Override - public int getRowCount() { - return cpData.size(); - } - }; - - table = new JTable(cpTableModel); - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - table.setSelectionBackground(Color.LIGHT_GRAY); - table.setSelectionForeground(Color.BLACK); - cpTableModel.setColumnWidths(table.getColumnModel()); - - table.setDefaultRenderer(Object.class, new CustomCellRenderer()); - // table.setShowHorizontalLines(false); - // table.setShowVerticalLines(true); - - JScrollPane scrollpane = new JScrollPane(table); - scrollpane.setPreferredSize(new Dimension(600, 200)); - - //// Stability and Stability information - tabbedPane.addTab(trans.get("componentanalysisdlg.TabStability"), - null, scrollpane, trans.get("componentanalysisdlg.TabStability.ttip")); - - - - // Create the drag data table - dragTableModel = new ColumnTableModel( - //// Component - new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Component")) { - @Override - public Object getValueAt(int row) { - RocketComponent c = dragData.get(row).getComponent(); - if (c instanceof Rocket) { - return "Total"; - } - return c.toString(); - } - - @Override - public int getDefaultWidth() { - return 200; - } - }, - //// <html>Pressure C<sub>D</sub> - new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Pressure")) { - @Override - public Object getValueAt(int row) { - return dragData.get(row).getPressureCD(); - } - }, - //// <html>Base C<sub>D</sub> - new Column(trans.get("componentanalysisdlg.dragTableModel.Col.Base")) { - @Override - public Object getValueAt(int row) { - return dragData.get(row).getBaseCD(); - } - }, - //// <html>Friction C<sub>D</sub> - new Column(trans.get("componentanalysisdlg.dragTableModel.Col.friction")) { - @Override - public Object getValueAt(int row) { - return dragData.get(row).getFrictionCD(); - } - }, - //// <html>Total C<sub>D</sub> - new Column(trans.get("componentanalysisdlg.dragTableModel.Col.total")) { - @Override - public Object getValueAt(int row) { - return dragData.get(row).getCD(); - } - } - ) { - @Override - public int getRowCount() { - return dragData.size(); - } - }; - - - table = new JTable(dragTableModel); - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - table.setSelectionBackground(Color.LIGHT_GRAY); - table.setSelectionForeground(Color.BLACK); - dragTableModel.setColumnWidths(table.getColumnModel()); - - table.setDefaultRenderer(Object.class, new DragCellRenderer(new Color(0.5f, 1.0f, 0.5f))); - // table.setShowHorizontalLines(false); - // table.setShowVerticalLines(true); - - scrollpane = new JScrollPane(table); - scrollpane.setPreferredSize(new Dimension(600, 200)); - - //// Drag characteristics and Drag characteristics tooltip - tabbedPane.addTab(trans.get("componentanalysisdlg.dragTabchar"), null, scrollpane, - trans.get("componentanalysisdlg.dragTabchar.ttip")); - - - - - // Create the roll data table - rollTableModel = new ColumnTableModel( - //// Component - new Column(trans.get("componentanalysisdlg.rollTableModel.Col.component")) { - @Override - public Object getValueAt(int row) { - RocketComponent c = rollData.get(row).getComponent(); - if (c instanceof Rocket) { - return "Total"; - } - return c.toString(); - } - }, - //// Roll forcing coefficient - new Column(trans.get("componentanalysisdlg.rollTableModel.Col.rollforc")) { - @Override - public Object getValueAt(int row) { - return rollData.get(row).getCrollForce(); - } - }, - //// Roll damping coefficient - new Column(trans.get("componentanalysisdlg.rollTableModel.Col.rolldamp")) { - @Override - public Object getValueAt(int row) { - return rollData.get(row).getCrollDamp(); - } - }, - //// <html>Total C<sub>l</sub> - new Column(trans.get("componentanalysisdlg.rollTableModel.Col.total")) { - @Override - public Object getValueAt(int row) { - return rollData.get(row).getCroll(); - } - } - ) { - @Override - public int getRowCount() { - return rollData.size(); - } - }; - - - table = new JTable(rollTableModel); - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - table.setSelectionBackground(Color.LIGHT_GRAY); - table.setSelectionForeground(Color.BLACK); - rollTableModel.setColumnWidths(table.getColumnModel()); - - scrollpane = new JScrollPane(table); - scrollpane.setPreferredSize(new Dimension(600, 200)); - - //// Roll dynamics and Roll dynamics tooltip - tabbedPane.addTab(trans.get("componentanalysisdlg.rollTableModel"), null, scrollpane, - trans.get("componentanalysisdlg.rollTableModel.ttip")); - - - - - - // Add the data updater to listen to changes in aoa and theta - mach.addChangeListener(this); - theta.addChangeListener(this); - aoa.addChangeListener(this); - roll.addChangeListener(this); - configuration.addChangeListener(this); - this.stateChanged(null); - - - - // Remove listeners when closing window - this.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - System.out.println("Closing method called: " + this); - theta.removeChangeListener(ComponentAnalysisDialog.this); - aoa.removeChangeListener(ComponentAnalysisDialog.this); - mach.removeChangeListener(ComponentAnalysisDialog.this); - roll.removeChangeListener(ComponentAnalysisDialog.this); - configuration.removeChangeListener(ComponentAnalysisDialog.this); - System.out.println("SETTING NAN VALUES"); - rocketPanel.setCPAOA(Double.NaN); - rocketPanel.setCPTheta(Double.NaN); - rocketPanel.setCPMach(Double.NaN); - rocketPanel.setCPRoll(Double.NaN); - singletonDialog = null; - } - }); - - //// Reference length: - panel.add(new StyledLabel(trans.get("componentanalysisdlg.lbl.reflenght"), -1), - "span, split, gapleft para, gapright rel"); - DoubleModel dm = new DoubleModel(conditions, "RefLength", UnitGroup.UNITS_LENGTH); - UnitSelector sel = new UnitSelector(dm, true); - sel.resizeFont(-1); - panel.add(sel, "gapright para"); - - //// Reference area: - panel.add(new StyledLabel(trans.get("componentanalysisdlg.lbl.refarea"), -1), "gapright rel"); - dm = new DoubleModel(conditions, "RefArea", UnitGroup.UNITS_AREA); - sel = new UnitSelector(dm, true); - sel.resizeFont(-1); - panel.add(sel, "wrap"); - - - - // Buttons - JButton button; - - // TODO: LOW: printing - // button = new JButton("Print"); - // button.addActionListener(new ActionListener() { - // public void actionPerformed(ActionEvent e) { - // try { - // table.print(); - // } catch (PrinterException e1) { - // JOptionPane.showMessageDialog(ComponentAnalysisDialog.this, - // "An error occurred while printing.", "Print error", - // JOptionPane.ERROR_MESSAGE); - // } - // } - // }); - // panel.add(button,"tag ok"); - - //button = new JButton("Close"); - //Close button - button = new JButton(trans.get("dlg.but.close")); - button.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - ComponentAnalysisDialog.this.dispose(); - } - }); - panel.add(button, "span, split, tag cancel"); - - - this.setLocationByPlatform(true); - setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - pack(); - - GUIUtil.setDisposableDialogOptions(this, null); - } - - - - /** - * Updates the data in the table and fires a table data change event. - */ - @Override - public void stateChanged(ChangeEvent e) { - AerodynamicForces forces; - WarningSet set = new WarningSet(); - conditions.setAOA(aoa.getValue()); - conditions.setTheta(theta.getValue()); - conditions.setMach(mach.getValue()); - conditions.setRollRate(roll.getValue()); - conditions.setReference(configuration); - - if (worstToggle.isSelected()) { - aerodynamicCalculator.getWorstCP(configuration, conditions, null); - if (!MathUtil.equals(conditions.getTheta(), theta.getValue())) { - fakeChange = true; - theta.setValue(conditions.getTheta()); // Fires a stateChanged event - fakeChange = false; - return; - } - } - - Map<RocketComponent, AerodynamicForces> aeroData = - aerodynamicCalculator.getForceAnalysis(configuration, conditions, set); - Map<RocketComponent, Coordinate> massData = - massCalculator.getCGAnalysis(configuration, MassCalcType.LAUNCH_MASS); - - - cpData.clear(); - cgData.clear(); - dragData.clear(); - rollData.clear(); - for (RocketComponent c : configuration) { - forces = aeroData.get(c); - Coordinate cg = massData.get(c); - - if (forces == null) - continue; - if (forces.getCP() != null) { - cpData.add(forces); - cgData.add(cg); - } - if (!Double.isNaN(forces.getCD())) { - dragData.add(forces); - } - if (c instanceof FinSet) { - rollData.add(forces); - } - } - forces = aeroData.get(configuration.getRocket()); - if (forces != null) { - cpData.add(forces); - cgData.add(massData.get(configuration.getRocket())); - dragData.add(forces); - rollData.add(forces); - totalCD = forces.getCD(); - } else { - totalCD = 0; - } - - // Set warnings - if (set.isEmpty()) { - warningList.setListData(new String[] { - "<html><i><font color=\"gray\">No warnings.</font></i>" - }); - } else { - warningList.setListData(new Vector<Warning>(set)); - } - - cpTableModel.fireTableDataChanged(); - dragTableModel.fireTableDataChanged(); - rollTableModel.fireTableDataChanged(); - } - - - private class CustomCellRenderer extends JLabel implements TableCellRenderer { - private final Font normalFont; - private final Font boldFont; - - public CustomCellRenderer() { - super(); - normalFont = getFont(); - boldFont = normalFont.deriveFont(Font.BOLD); - } - - @Override - public Component getTableCellRendererComponent(JTable table, Object value, - boolean isSelected, boolean hasFocus, int row, int column) { - - this.setText(value.toString()); - - if ((row < 0) || (row >= cpData.size())) - return this; - - if (cpData.get(row).getComponent() instanceof Rocket) { - this.setFont(boldFont); - } else { - this.setFont(normalFont); - } - return this; - } - } - - - - private class DragCellRenderer extends JLabel implements TableCellRenderer { - private final Font normalFont; - private final Font boldFont; - - - public DragCellRenderer(Color baseColor) { - super(); - normalFont = getFont(); - boldFont = normalFont.deriveFont(Font.BOLD); - } - - @Override - public Component getTableCellRendererComponent(JTable table, Object value, - boolean isSelected, boolean hasFocus, int row, int column) { - - if (value instanceof Double) { - - // A drag coefficient - double cd = (Double) value; - this.setText(String.format("%.2f (%.0f%%)", cd, 100 * cd / totalCD)); - - float r = (float) (cd / 1.5); - - float hue = MathUtil.clamp(0.3333f * (1 - 2.0f * r), 0, 0.3333f); - float sat = MathUtil.clamp(0.8f * r + 0.1f * (1 - r), 0, 1); - float val = 1.0f; - - this.setBackground(Color.getHSBColor(hue, sat, val)); - this.setOpaque(true); - this.setHorizontalAlignment(SwingConstants.CENTER); - - } else { - - // Other - this.setText(value.toString()); - this.setOpaque(false); - this.setHorizontalAlignment(SwingConstants.LEFT); - - } - - if ((row < 0) || (row >= dragData.size())) - return this; - - if ((dragData.get(row).getComponent() instanceof Rocket) || (column == 4)) { - this.setFont(boldFont); - } else { - this.setFont(normalFont); - } - return this; - } - } - - - ///////// Singleton implementation - - public static void showDialog(RocketPanel rocketpanel) { - if (singletonDialog != null) - singletonDialog.dispose(); - singletonDialog = new ComponentAnalysisDialog(rocketpanel); - singletonDialog.setVisible(true); - } - - public static void hideDialog() { - if (singletonDialog != null) - singletonDialog.dispose(); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java b/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java deleted file mode 100644 index 5e5aece8..00000000 --- a/src/net/sf/openrocket/gui/dialogs/CustomMaterialDialog.java +++ /dev/null @@ -1,183 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Dialog; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.JTextField; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.startup.Application; - -public class CustomMaterialDialog extends JDialog { - - private final Material originalMaterial; - - private boolean okClicked = false; - private JComboBox typeBox; - private JTextField nameField; - private DoubleModel density; - private JSpinner densitySpinner; - private UnitSelector densityUnit; - private JCheckBox addBox; - private static final Translator trans = Application.getTranslator(); - - public CustomMaterialDialog(Window parent, Material material, boolean saveOption, - String title) { - this(parent, material, saveOption, title, null); - } - - - public CustomMaterialDialog(Window parent, Material material, boolean saveOption, - String title, String note) { - //// Custom material - super(parent, trans.get("custmatdlg.title.Custommaterial"), Dialog.ModalityType.APPLICATION_MODAL); - - this.originalMaterial = material; - - JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel")); - - - // Add title and note - if (title != null) { - panel.add(new JLabel("<html><b>" + title + ":"), - "gapleft para, span, wrap" + (note == null ? " para":"")); - } - if (note != null) { - panel.add(new StyledLabel(note, -1), "span, wrap para"); - } - - - //// Material name - panel.add(new JLabel(trans.get("custmatdlg.lbl.Materialname"))); - nameField = new JTextField(15); - if (material != null) { - nameField.setText(material.getName()); - } - panel.add(nameField, "span, growx, wrap"); - - - // Material type (if not known) - panel.add(new JLabel(trans.get("custmatdlg.lbl.Materialtype"))); - if (material == null) { - typeBox = new JComboBox(Material.Type.values()); - typeBox.setSelectedItem(Material.Type.BULK); - typeBox.setEditable(false); - typeBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - updateDensityModel(); - } - }); - panel.add(typeBox, "span, growx, wrap"); - } else { - panel.add(new JLabel(material.getType().toString()), "span, growx, wrap"); - } - - - // Material density: - panel.add(new JLabel(trans.get("custmatdlg.lbl.Materialdensity"))); - densitySpinner = new JSpinner(); - panel.add(densitySpinner, "w 70lp"); - densityUnit = new UnitSelector((DoubleModel)null); - panel.add(densityUnit, "w 30lp"); - panel.add(new JPanel(), "growx, wrap"); - updateDensityModel(); - - - // Save option - if (saveOption) { - //// Add material to database - addBox = new JCheckBox(trans.get("custmatdlg.checkbox.Addmaterial")); - panel.add(addBox,"span, wrap"); - } - - //// OK button - JButton okButton = new JButton(trans.get("dlg.but.ok")); - - okButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - okClicked = true; - CustomMaterialDialog.this.setVisible(false); - } - }); - panel.add(okButton,"span, split, tag ok"); - - //// Cancel - JButton closeButton = new JButton(trans.get("dlg.but.cancel")); - closeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - okClicked = false; - CustomMaterialDialog.this.setVisible(false); - } - }); - panel.add(closeButton,"tag cancel"); - - this.setContentPane(panel); - this.pack(); - this.setLocationByPlatform(true); - GUIUtil.setDisposableDialogOptions(this, okButton); - } - - - public boolean getOkClicked() { - return okClicked; - } - - - public boolean isAddSelected() { - return addBox.isSelected(); - } - - - public Material getMaterial() { - Material.Type type; - String name; - double density; - - if (typeBox != null) { - type = (Material.Type) typeBox.getSelectedItem(); - } else { - type = originalMaterial.getType(); - } - - name = nameField.getText().trim(); - - density = this.density.getValue(); - - return Material.newMaterial(type, name, density, true); - } - - - private void updateDensityModel() { - if (originalMaterial != null) { - if (density == null) { - density = new DoubleModel(originalMaterial.getDensity(), - originalMaterial.getType().getUnitGroup(), 0); - densitySpinner.setModel(density.getSpinnerModel()); - densityUnit.setModel(density); - } - } else { - Material.Type type = (Material.Type) typeBox.getSelectedItem(); - density = new DoubleModel(0, type.getUnitGroup(), 0); - densitySpinner.setModel(density.getSpinnerModel()); - densityUnit.setModel(density); - } - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java b/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java deleted file mode 100644 index 5c36f5d8..00000000 --- a/src/net/sf/openrocket/gui/dialogs/DebugLogDialog.java +++ /dev/null @@ -1,537 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Rectangle; -import java.awt.Toolkit; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.EnumMap; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.JTable; -import javax.swing.JTextArea; -import javax.swing.ListSelectionModel; -import javax.swing.RowFilter; -import javax.swing.SwingUtilities; -import javax.swing.Timer; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableModel; -import javax.swing.table.TableRowSorter; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.adaptors.Column; -import net.sf.openrocket.gui.adaptors.ColumnTableModel; -import net.sf.openrocket.gui.components.SelectableLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.DelegatorLogger; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.LogLevel; -import net.sf.openrocket.logging.LogLevelBufferLogger; -import net.sf.openrocket.logging.LogLine; -import net.sf.openrocket.logging.StackTraceWriter; -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.NumericComparator; - -public class DebugLogDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - - private static final int POLL_TIME = 250; - private static final String STACK_TRACE_MARK = "\uFF01"; - private static final Translator trans = Application.getTranslator(); - - private static final EnumMap<LogLevel, Color> backgroundColors = new EnumMap<LogLevel, Color>(LogLevel.class); - static { - for (LogLevel l : LogLevel.values()) { - // Just to ensure every level has a bg color - backgroundColors.put(l, Color.ORANGE); - } - final int hi = 255; - final int lo = 150; - backgroundColors.put(LogLevel.ERROR, new Color(hi, lo, lo)); - backgroundColors.put(LogLevel.WARN, new Color(hi, (hi + lo) / 2, lo)); - backgroundColors.put(LogLevel.USER, new Color(lo, lo, hi)); - backgroundColors.put(LogLevel.INFO, new Color(hi, hi, lo)); - backgroundColors.put(LogLevel.DEBUG, new Color(lo, hi, lo)); - backgroundColors.put(LogLevel.VBOSE, new Color(lo, hi, (hi + lo) / 2)); - } - - /** Buffer containing the log lines displayed */ - private final List<LogLine> buffer = new ArrayList<LogLine>(); - - /** Queue of log lines to be added to the displayed buffer */ - private final Queue<LogLine> queue = new ConcurrentLinkedQueue<LogLine>(); - - private final DelegatorLogger delegator; - private final LogListener logListener; - - private final EnumMap<LogLevel, JCheckBox> filterButtons = new EnumMap<LogLevel, JCheckBox>(LogLevel.class); - private final JCheckBox followBox; - private final Timer timer; - - - private final JTable table; - private final ColumnTableModel model; - private final TableRowSorter<TableModel> sorter; - - private final SelectableLabel numberLabel; - private final SelectableLabel timeLabel; - private final SelectableLabel levelLabel; - private final SelectableLabel locationLabel; - private final SelectableLabel messageLabel; - private final JTextArea stackTraceLabel; - - public DebugLogDialog(Window parent) { - //// OpenRocket debug log - super(parent, trans.get("debuglogdlg.OpenRocketdebuglog")); - - // Start listening to log lines - LogHelper applicationLog = Application.getLogger(); - if (applicationLog instanceof DelegatorLogger) { - log.info("Adding log listener"); - delegator = (DelegatorLogger) applicationLog; - logListener = new LogListener(); - delegator.addLogger(logListener); - } else { - log.warn("Application log is not a DelegatorLogger"); - delegator = null; - logListener = null; - } - - // Fetch old log lines - LogLevelBufferLogger bufferLogger = Application.getLogBuffer(); - if (bufferLogger != null) { - buffer.addAll(bufferLogger.getLogs()); - } else { - log.warn("Application does not have a log buffer"); - } - - - // Create the UI - JPanel mainPanel = new JPanel(new MigLayout("fill")); - this.add(mainPanel); - - - JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT); - split.setDividerLocation(0.7); - mainPanel.add(split, "grow"); - - // Top panel - JPanel panel = new JPanel(new MigLayout("fill")); - split.add(panel); - - //// Display log lines: - panel.add(new JLabel(trans.get("debuglogdlg.Displayloglines")), "gapright para, split"); - for (LogLevel l : LogLevel.values()) { - JCheckBox box = new JCheckBox(l.toString()); - // By default display DEBUG and above - box.setSelected(l.atLeast(LogLevel.DEBUG)); - box.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - sorter.setRowFilter(new LogFilter()); - } - }); - panel.add(box, "gapright unrel"); - filterButtons.put(l, box); - } - - //// Follow - followBox = new JCheckBox(trans.get("debuglogdlg.Follow")); - followBox.setSelected(true); - panel.add(followBox, "skip, gapright para, right"); - - //// Clear button - JButton clear = new JButton(trans.get("debuglogdlg.but.clear")); - clear.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Clearing log buffer"); - buffer.clear(); - queue.clear(); - model.fireTableDataChanged(); - } - }); - panel.add(clear, "right, wrap"); - - - - // Create the table model - model = new ColumnTableModel( - - new Column("#") { - @Override - public Object getValueAt(int row) { - return buffer.get(row).getLogCount(); - } - - @Override - public int getDefaultWidth() { - return 60; - } - }, - //// Time - new Column(trans.get("debuglogdlg.col.Time")) { - @Override - public Object getValueAt(int row) { - return String.format("%.3f", buffer.get(row).getTimestamp() / 1000.0); - } - - @Override - public int getDefaultWidth() { - return 60; - } - }, - //// Level - new Column(trans.get("debuglogdlg.col.Level")) { - @Override - public Object getValueAt(int row) { - return buffer.get(row).getLevel(); - } - - @Override - public int getDefaultWidth() { - return 60; - } - }, - new Column("") { - @Override - public Object getValueAt(int row) { - if (buffer.get(row).getCause() != null) { - return STACK_TRACE_MARK; - } else { - return ""; - } - } - - @Override - public int getExactWidth() { - return 16; - } - }, - //// Location - new Column(trans.get("debuglogdlg.col.Location")) { - @Override - public Object getValueAt(int row) { - TraceException e = buffer.get(row).getTrace(); - if (e != null) { - return e.getMessage(); - } else { - return ""; - } - } - - @Override - public int getDefaultWidth() { - return 200; - } - }, - //// Message - new Column(trans.get("debuglogdlg.col.Message")) { - @Override - public Object getValueAt(int row) { - return buffer.get(row).getMessage(); - } - - @Override - public int getDefaultWidth() { - return 580; - } - } - - ) { - @Override - public int getRowCount() { - return buffer.size(); - } - }; - - table = new JTable(model); - table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - table.setSelectionBackground(Color.LIGHT_GRAY); - table.setSelectionForeground(Color.BLACK); - model.setColumnWidths(table.getColumnModel()); - table.setDefaultRenderer(Object.class, new Renderer()); - - table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - int row = table.getSelectedRow(); - if (row >= 0) { - row = sorter.convertRowIndexToModel(row); - } - updateSelected(row); - } - }); - - sorter = new TableRowSorter<TableModel>(model); - sorter.setComparator(0, NumericComparator.INSTANCE); - sorter.setComparator(1, NumericComparator.INSTANCE); - sorter.setComparator(4, new LocationComparator()); - table.setRowSorter(sorter); - sorter.setRowFilter(new LogFilter()); - - - panel.add(new JScrollPane(table), "span, grow, width " + - (Toolkit.getDefaultToolkit().getScreenSize().width * 8 / 10) + - "px, height 400px"); - - - panel = new JPanel(new MigLayout("fill")); - split.add(panel); - - //// Log line number: - panel.add(new JLabel(trans.get("debuglogdlg.lbl.Loglinenbr")), "split, gapright rel"); - numberLabel = new SelectableLabel(); - panel.add(numberLabel, "width 70lp, gapright para"); - - //// Time: - panel.add(new JLabel(trans.get("debuglogdlg.lbl.Time")), "split, gapright rel"); - timeLabel = new SelectableLabel(); - panel.add(timeLabel, "width 70lp, gapright para"); - - //// Level: - panel.add(new JLabel(trans.get("debuglogdlg.lbl.Level")), "split, gapright rel"); - levelLabel = new SelectableLabel(); - panel.add(levelLabel, "width 70lp, gapright para"); - - //// Location: - panel.add(new JLabel(trans.get("debuglogdlg.lbl.Location")), "split, gapright rel"); - locationLabel = new SelectableLabel(); - panel.add(locationLabel, "growx, wrap unrel"); - - //// Log message: - panel.add(new JLabel(trans.get("debuglogdlg.lbl.Logmessage")), "split, gapright rel"); - messageLabel = new SelectableLabel(); - panel.add(messageLabel, "growx, wrap para"); - - //// Stack trace: - panel.add(new JLabel(trans.get("debuglogdlg.lbl.Stacktrace")), "wrap rel"); - stackTraceLabel = new JTextArea(8, 80); - stackTraceLabel.setEditable(false); - GUIUtil.changeFontSize(stackTraceLabel, -2); - panel.add(new JScrollPane(stackTraceLabel), "grow"); - - - //Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - DebugLogDialog.this.dispose(); - } - }); - mainPanel.add(close, "newline para, right, tag ok"); - - - // Use timer to purge the queue so as not to overwhelm the EDT with events - timer = new Timer(POLL_TIME, new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - purgeQueue(); - } - }); - timer.setRepeats(true); - timer.start(); - - this.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - log.user("Closing debug log dialog"); - timer.stop(); - if (delegator != null) { - log.info("Removing log listener"); - delegator.removeLogger(logListener); - } - } - }); - - GUIUtil.setDisposableDialogOptions(this, close); - followBox.requestFocus(); - } - - - - private void updateSelected(int row) { - if (row < 0) { - - numberLabel.setText(""); - timeLabel.setText(""); - levelLabel.setText(""); - locationLabel.setText(""); - messageLabel.setText(""); - stackTraceLabel.setText(""); - - } else { - - LogLine line = buffer.get(row); - numberLabel.setText("" + line.getLogCount()); - timeLabel.setText(String.format("%.3f s", line.getTimestamp() / 1000.0)); - levelLabel.setText(line.getLevel().toString()); - TraceException e = line.getTrace(); - if (e != null) { - locationLabel.setText(e.getMessage()); - } else { - locationLabel.setText("-"); - } - messageLabel.setText(line.getMessage()); - Throwable t = line.getCause(); - if (t != null) { - StackTraceWriter stw = new StackTraceWriter(); - PrintWriter pw = new PrintWriter(stw); - t.printStackTrace(pw); - pw.flush(); - stackTraceLabel.setText(stw.toString()); - stackTraceLabel.setCaretPosition(0); - } else { - stackTraceLabel.setText(""); - } - - } - } - - - /** - * Check whether a row signifies a number of missing rows. This check is "heuristic" - * and checks whether the timestamp is zero and the message starts with "---". - */ - private boolean isExcludedRow(int row) { - LogLine line = buffer.get(row); - return (line.getTimestamp() == 0) && (line.getMessage().startsWith("---")); - } - - - /** - * Purge the queue of incoming log lines. This is called periodically from the EDT, and - * it adds any lines in the queue to the buffer, and fires a table event. - */ - private void purgeQueue() { - int start = buffer.size(); - - LogLine line; - while ((line = queue.poll()) != null) { - buffer.add(line); - } - - int end = buffer.size() - 1; - if (end >= start) { - model.fireTableRowsInserted(start, end); - if (followBox.isSelected()) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - Rectangle rect = table.getCellRect(1000000000, 1, true); - table.scrollRectToVisible(rect); - } - }); - } - } - } - - - /** - * A logger that adds log lines to the queue. This method may be called from any - * thread, and therefore must be thread-safe. - */ - private class LogListener extends LogHelper { - @Override - public void log(LogLine line) { - queue.add(line); - } - } - - private class LogFilter extends RowFilter<TableModel, Integer> { - - @Override - public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) { - int index = entry.getIdentifier(); - LogLine line = buffer.get(index); - return filterButtons.get(line.getLevel()).isSelected(); - } - - } - - - private class Renderer extends JLabel implements TableCellRenderer { - @Override - public Component getTableCellRendererComponent(JTable table1, Object value, boolean isSelected, boolean hasFocus, - int row, int column) { - Color fg, bg; - - row = sorter.convertRowIndexToModel(row); - - if (STACK_TRACE_MARK.equals(value)) { - fg = Color.RED; - } else { - fg = table1.getForeground(); - } - bg = backgroundColors.get(buffer.get(row).getLevel()); - - if (isSelected) { - bg = bg.darker(); - } else if (isExcludedRow(row)) { - bg = bg.brighter(); - } - - this.setForeground(fg); - this.setBackground(bg); - - this.setOpaque(true); - this.setText(value.toString()); - - return this; - } - } - - - private class LocationComparator implements Comparator<Object> { - private final Pattern splitPattern = Pattern.compile("^\\(([^:]*+):([0-9]++).*\\)$"); - - @Override - public int compare(Object o1, Object o2) { - String s1 = o1.toString(); - String s2 = o2.toString(); - - Matcher m1 = splitPattern.matcher(s1); - Matcher m2 = splitPattern.matcher(s2); - - if (m1.matches() && m2.matches()) { - String class1 = m1.group(1); - String pos1 = m1.group(2); - String class2 = m2.group(1); - String pos2 = m2.group(2); - - if (class1.equals(class2)) { - return NumericComparator.INSTANCE.compare(pos1, pos2); - } else { - return class1.compareTo(class2); - } - } - - return s1.compareTo(s2); - } - - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/DetailDialog.java b/src/net/sf/openrocket/gui/dialogs/DetailDialog.java deleted file mode 100644 index d1f0f547..00000000 --- a/src/net/sf/openrocket/gui/dialogs/DetailDialog.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Component; - -import javax.swing.JOptionPane; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - -import net.sf.openrocket.gui.util.GUIUtil; - -public class DetailDialog { - - public static void showDetailedMessageDialog(Component parentComponent, Object message, - String details, String title, int messageType) { - - if (details != null) { - JTextArea textArea = null; - textArea = new JTextArea(5, 40); - textArea.setText(details); - textArea.setCaretPosition(0); - textArea.setEditable(false); - GUIUtil.changeFontSize(textArea, -2); - JOptionPane.showMessageDialog(parentComponent, - new Object[] { message, new JScrollPane(textArea) }, - title, messageType, null); - } else { - JOptionPane.showMessageDialog(parentComponent, message, title, messageType, null); - } - - } - - -} diff --git a/src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java b/src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java deleted file mode 100644 index 0a41ce66..00000000 --- a/src/net/sf/openrocket/gui/dialogs/EditMotorConfigurationDialog.java +++ /dev/null @@ -1,506 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.util.ArrayList; -import java.util.Iterator; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.JTextField; -import javax.swing.ListSelectionModel; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.dialogs.motor.MotorChooserDialog; -import net.sf.openrocket.gui.main.BasicFrame; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Chars; - -public class EditMotorConfigurationDialog extends JDialog { - - private final Rocket rocket; - - private final MotorMount[] mounts; - - private final JTable configurationTable; - private final MotorConfigurationTableModel configurationTableModel; - - - private final JButton newConfButton, removeConfButton; - private final JButton selectMotorButton, removeMotorButton; - private final JTextField configurationNameField; - - - private String currentID = null; - private MotorMount currentMount = null; - - // Positive when user is modifying configuration name - private int configurationNameModification = 0; - private static final Translator trans = Application.getTranslator(); - - public EditMotorConfigurationDialog(final Rocket rocket, Window parent) { - //// Edit motor configurations - super(parent, trans.get("edtmotorconfdlg.title.Editmotorconf")); - - if (parent != null) - this.setModalityType(ModalityType.DOCUMENT_MODAL); - else - this.setModalityType(ModalityType.APPLICATION_MODAL); - - this.rocket = rocket; - - ArrayList<MotorMount> mountList = new ArrayList<MotorMount>(); - Iterator<RocketComponent> iterator = rocket.iterator(); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - if (c instanceof MotorMount) { - mountList.add((MotorMount) c); - } - } - mounts = mountList.toArray(new MotorMount[0]); - - - - JPanel panel = new JPanel(new MigLayout("fill, flowy")); - - - //// Motor mount selection - //// <html><b>Motor mounts:</b> - JLabel label = new JLabel(trans.get("edtmotorconfdlg.lbl.Motormounts")); - panel.add(label, "gapbottom para"); - - //// <html>Select which components function as motor mounts: - label = new JLabel(trans.get("edtmotorconfdlg.selectcomp")); - panel.add(label, "ay 100%, w 1px, growx"); - - - JTable table = new JTable(new MotorMountTableModel()); - table.setTableHeader(null); - table.setShowVerticalLines(false); - table.setRowSelectionAllowed(false); - table.setColumnSelectionAllowed(false); - - TableColumnModel columnModel = table.getColumnModel(); - TableColumn col0 = columnModel.getColumn(0); - int w = table.getRowHeight() + 2; - col0.setMinWidth(w); - col0.setPreferredWidth(w); - col0.setMaxWidth(w); - - table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); - - JScrollPane scroll = new JScrollPane(table); - panel.add(scroll, "w 200lp, h 150lp, grow, wrap 20lp"); - - - - - - //// Motor selection - //// <html><b>Motor configurations:</b> - label = new JLabel(trans.get("edtmotorconfdlg.lbl.Motorconfig")); - panel.add(label, "spanx, gapbottom para"); - - //// Configuration name: - label = new JLabel(trans.get("edtmotorconfdlg.lbl.Configname")); - //// Leave name empty for default. - String tip = trans.get("edtmotorconfdlg.lbl.Leavenamedefault"); - label.setToolTipText(tip); - panel.add(label, ""); - - configurationNameField = new JTextField(10); - configurationNameField.setToolTipText(tip); - configurationNameField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void changedUpdate(DocumentEvent e) { - update(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - update(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - update(); - } - - private void update() { - if (configurationNameModification != 0) - return; - - String text = configurationNameField.getText(); - if (currentID != null) { - configurationNameModification++; - rocket.setMotorConfigurationName(currentID, text); - int row = configurationTable.getSelectedRow(); - configurationTableModel.fireTableCellUpdated(row, 0); - updateEnabled(); - configurationNameModification--; - } - } - }); - panel.add(configurationNameField, "cell 2 1, gapright para"); - - //// New configuration - newConfButton = new JButton(trans.get("edtmotorconfdlg.but.Newconfiguration")); - newConfButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - String id = rocket.newMotorConfigurationID(); - rocket.getDefaultConfiguration().setMotorConfigurationID(id); - configurationTableModel.fireTableDataChanged(); - updateEnabled(); - } - }); - panel.add(newConfButton, "cell 3 1"); - - //// Remove configuration - removeConfButton = new JButton(trans.get("edtmotorconfdlg.but.Removeconfiguration")); - removeConfButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (currentID == null) - return; - rocket.removeMotorConfigurationID(currentID); - rocket.getDefaultConfiguration().setMotorConfigurationID(null); - configurationTableModel.fireTableDataChanged(); - updateEnabled(); - } - }); - panel.add(removeConfButton, "cell 4 1"); - - - - - configurationTableModel = new MotorConfigurationTableModel(); - configurationTable = new JTable(configurationTableModel); - configurationTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - configurationTable.setCellSelectionEnabled(true); - - configurationTable.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - - if (e.getClickCount() == 1) { - - // Single click updates selection - updateEnabled(); - - } else if (e.getClickCount() == 2) { - - // Double-click edits motor - selectMotor(); - - } - - } - }); - - - scroll = new JScrollPane(configurationTable); - panel.add(scroll, "cell 1 2, spanx, w 500lp, h 150lp, grow"); - - //// Select motor - selectMotorButton = new JButton(trans.get("edtmotorconfdlg.but.Selectmotor")); - selectMotorButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - selectMotor(); - } - }); - panel.add(selectMotorButton, "spanx, flowx, split 2, ax 50%"); - - //// Remove motor button - removeMotorButton = new JButton(trans.get("edtmotorconfdlg.but.removemotor")); - removeMotorButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - removeMotor(); - } - }); - panel.add(removeMotorButton, "ax 50%"); - - - - //// Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - EditMotorConfigurationDialog.this.dispose(); - } - }); - panel.add(close, "spanx, right"); - - this.add(panel); - this.validate(); - this.pack(); - - updateEnabled(); - - this.setLocationByPlatform(true); - GUIUtil.setDisposableDialogOptions(this, close); - - // Undo description - final OpenRocketDocument document = BasicFrame.findDocument(rocket); - if (document != null) { - //// Edit motor configurations - document.startUndo(trans.get("edtmotorconfdlg.title.Editmotorconf")); - this.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - document.stopUndo(); - } - }); - } - } - - - - - private void updateEnabled() { - int column = configurationTable.getSelectedColumn(); - int row = configurationTable.getSelectedRow(); - - if (column < 0 || row < 0) { - currentID = null; - currentMount = null; - } else { - - currentID = findID(row); - if (column == 0) { - currentMount = null; - } else { - currentMount = findMount(column); - } - rocket.getDefaultConfiguration().setMotorConfigurationID(currentID); - - } - - if (configurationNameModification == 0) { - // Don't update name field when user is modifying it - configurationNameModification++; - - configurationNameField.setEnabled(currentID != null); - if (currentID == null) { - configurationNameField.setText(""); - } else { - configurationNameField.setText(rocket.getMotorConfigurationName(currentID)); - } - - configurationNameModification--; - } - removeConfButton.setEnabled(currentID != null); - selectMotorButton.setEnabled(currentMount != null && currentID != null); - removeMotorButton.setEnabled(currentMount != null && currentID != null); - } - - - - - private void selectMotor() { - if (currentID == null || currentMount == null) - return; - - MotorChooserDialog dialog = new MotorChooserDialog(currentMount.getMotor(currentID), - currentMount.getMotorDelay(currentID), currentMount.getMotorMountDiameter(), this); - dialog.setVisible(true); - Motor m = dialog.getSelectedMotor(); - double d = dialog.getSelectedDelay(); - - if (m != null) { - currentMount.setMotor(currentID, m); - currentMount.setMotorDelay(currentID, d); - } - - int row = configurationTable.getSelectedRow(); - configurationTableModel.fireTableRowsUpdated(row, row); - updateEnabled(); - } - - - private void removeMotor() { - if (currentID == null || currentMount == null) - return; - - currentMount.setMotor(currentID, null); - - int row = configurationTable.getSelectedRow(); - configurationTableModel.fireTableRowsUpdated(row, row); - updateEnabled(); - } - - - private String findID(int row) { - return rocket.getMotorConfigurationIDs()[row + 1]; - } - - - private MotorMount findMount(int column) { - MotorMount mount = null; - - int count = column; - for (MotorMount m : mounts) { - if (m.isMotorMount()) - count--; - if (count <= 0) { - mount = m; - break; - } - } - - if (mount == null) { - throw new IndexOutOfBoundsException("motor mount not found, column=" + column); - } - return mount; - } - - - /** - * The table model for selecting whether components are motor mounts or not. - */ - private class MotorMountTableModel extends AbstractTableModel { - - @Override - public int getColumnCount() { - return 2; - } - - @Override - public int getRowCount() { - return mounts.length; - } - - @Override - public Class<?> getColumnClass(int column) { - switch (column) { - case 0: - return Boolean.class; - - case 1: - return String.class; - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - @Override - public Object getValueAt(int row, int column) { - switch (column) { - case 0: - return new Boolean(mounts[row].isMotorMount()); - - case 1: - return mounts[row].toString(); - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - @Override - public boolean isCellEditable(int row, int column) { - return column == 0; - } - - @Override - public void setValueAt(Object value, int row, int column) { - if (column != 0 || !(value instanceof Boolean)) { - throw new IllegalArgumentException("column=" + column + ", value=" + value); - } - - mounts[row].setMotorMount((Boolean) value); - configurationTableModel.fireTableStructureChanged(); - updateEnabled(); - } - } - - - - /** - * The table model for selecting and editing the motor configurations. - */ - private class MotorConfigurationTableModel extends AbstractTableModel { - - @Override - public int getColumnCount() { - int count = 1; - for (MotorMount m : mounts) { - if (m.isMotorMount()) - count++; - } - return count; - } - - @Override - public int getRowCount() { - return rocket.getMotorConfigurationIDs().length - 1; - } - - @Override - public Object getValueAt(int row, int column) { - - String id = findID(row); - - if (column == 0) { - return rocket.getMotorConfigurationNameOrDescription(id); - } - - MotorMount mount = findMount(column); - Motor motor = mount.getMotor(id); - if (motor == null) - //// None - return "None"; - - String str = motor.getDesignation(mount.getMotorDelay(id)); - int count = mount.getMotorCount(); - if (count > 1) { - str = "" + count + Chars.TIMES + " " + str; - } - return str; - } - - - @Override - public String getColumnName(int column) { - if (column == 0) { - //// Configuration name - return trans.get("edtmotorconfdlg.lbl.Configname"); - } - - MotorMount mount = findMount(column); - String name = mount.toString(); - int count = mount.getMotorCount(); - if (count > 1) { - name = name + " (" + Chars.TIMES + count + ")"; - } - return name; - } - - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java b/src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java deleted file mode 100644 index 95ad56df..00000000 --- a/src/net/sf/openrocket/gui/dialogs/ExampleDesignDialog.java +++ /dev/null @@ -1,266 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Dialog; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Enumeration; -import java.util.jar.JarEntry; -import java.util.jar.JarFile; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.ListSelectionModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.JarUtil; - -public class ExampleDesignDialog extends JDialog { - - private static final String DIRECTORY = "datafiles/examples/"; - private static final String PATTERN = ".*\\.[oO][rR][kK]$"; - private static final Translator trans = Application.getTranslator(); - - private static final FilenameFilter FILTER = new FilenameFilter() { - @Override - public boolean accept(File dir, String name) { - return name.matches(PATTERN); - } - }; - - - private boolean open = false; - private final JList designSelection; - - private ExampleDesignDialog(ExampleDesign[] designs, Window parent) { - //// Open example design - super(parent, trans.get("exdesigndlg.lbl.Openexampledesign"), Dialog.ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill")); - - //// Select example designs to open: - panel.add(new JLabel(trans.get("exdesigndlg.lbl.Selectexample")), "wrap"); - - designSelection = new JList(designs); - designSelection.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - designSelection.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() >= 2) { - open = true; - ExampleDesignDialog.this.setVisible(false); - } - } - }); - panel.add(new JScrollPane(designSelection), "grow, wmin 300lp, wrap para"); - - //// Open button - JButton openButton = new JButton(trans.get("exdesigndlg.but.open")); - openButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - open = true; - ExampleDesignDialog.this.setVisible(false); - } - }); - panel.add(openButton, "split 2, sizegroup buttons, growx"); - - //// Cancel button - JButton cancelButton = new JButton(trans.get("dlg.but.cancel")); - cancelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - open = false; - ExampleDesignDialog.this.setVisible(false); - } - }); - panel.add(cancelButton, "sizegroup buttons, growx"); - - this.add(panel); - this.pack(); - this.setLocationByPlatform(true); - - GUIUtil.setDisposableDialogOptions(this, openButton); - } - - - /** - * Open a dialog to allow opening the example designs. - * - * @param parent the parent window of the dialog. - * @return an array of URL's to open, or <code>null</code> if the operation - * was cancelled. - */ - public static URL[] selectExampleDesigns(Window parent) { - - ExampleDesign[] designs; - - designs = getJarFileNames(); - if (designs == null || designs.length == 0) { - designs = getDirFileNames(); - } - if (designs == null || designs.length == 0) { - //// Example designs could not be found. - JOptionPane.showMessageDialog(parent, trans.get("exdesigndlg.lbl.Exampledesignsnotfound"), - //// Examples not found - trans.get("exdesigndlg.lbl.Examplesnotfound"), JOptionPane.ERROR_MESSAGE); - return null; - } - - Arrays.sort(designs); - - ExampleDesignDialog dialog = new ExampleDesignDialog(designs, parent); - dialog.setVisible(true); - - if (!dialog.open) { - return null; - } - - Object[] selected = dialog.designSelection.getSelectedValues(); - URL[] urls = new URL[selected.length]; - for (int i=0; i<selected.length; i++) { - urls[i] = ((ExampleDesign)selected[i]).getURL(); - } - return urls; - } - - - - - - private static ExampleDesign[] getDirFileNames() { - - // Try to find directory as a system resource - File dir; - URL url = ClassLoader.getSystemResource(DIRECTORY); - - try { - dir = JarUtil.urlToFile(url); - } catch (Exception e1) { - dir = new File(DIRECTORY); - } - - // Get the list of files - File[] files = dir.listFiles(FILTER); - if (files == null) - return null; - - ExampleDesign[] designs = new ExampleDesign[files.length]; - - for (int i=0; i<files.length; i++) { - String name = files[i].getName(); - try { - designs[i] = new ExampleDesign(files[i].toURI().toURL(), - name.substring(0, name.length()-4)); - } catch (MalformedURLException e) { - throw new BugException(e); - } - } - return designs; - } - - - - private static ExampleDesign[] getJarFileNames() { - - ArrayList<ExampleDesign> list = new ArrayList<ExampleDesign>(); - int dirLength = DIRECTORY.length(); - - // Find and open the jar file this class is contained in - File file = JarUtil.getCurrentJarFile(); - if (file == null) - return null; - - - // Generate URL pointing to JAR file - URL fileUrl; - try { - fileUrl = file.toURI().toURL(); - } catch (MalformedURLException e1) { - e1.printStackTrace(); - throw new BugException(e1); - } - - // Iterate over JAR entries searching for designs - JarFile jarFile = null; - try { - jarFile = new JarFile(file); - - // Loop through JAR entries searching for files to load - Enumeration<JarEntry> entries = jarFile.entries(); - while (entries.hasMoreElements()) { - JarEntry entry = entries.nextElement(); - String name = entry.getName(); - if (name.startsWith(DIRECTORY) && FILTER.accept(null, name)) { - String urlName = "jar:" + fileUrl + "!/" + name; - URL url = new URL(urlName); - list.add(new ExampleDesign(url, - name.substring(dirLength, name.length()-4))); - } - } - - } catch (IOException e) { - // Could be normal condition if not package in JAR - return null; - } finally { - if (jarFile != null) { - try { - jarFile.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - return list.toArray(new ExampleDesign[0]); - } - - - - /** - * Data holder class. - */ - private static class ExampleDesign implements Comparable<ExampleDesign> { - - private final URL url; - private final String name; - - public ExampleDesign(URL url, String name) { - this.url = url; - this.name = name; - } - - @Override - public String toString() { - return name; - } - - public URL getURL() { - return url; - } - - @Override - public int compareTo(ExampleDesign o) { - return this.name.compareTo(o.name); - } - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java b/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java deleted file mode 100644 index c3e6141e..00000000 --- a/src/net/sf/openrocket/gui/dialogs/LicenseDialog.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Font; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTextArea; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -public class LicenseDialog extends JDialog { - private static final String LICENSE_FILENAME = "LICENSE.TXT"; - private static final Translator trans = Application.getTranslator(); - - private static final String DEFAULT_LICENSE_TEXT = - "\n" + - "Error: Unable to load " + LICENSE_FILENAME + "!\n" + - "\n" + - "OpenRocket is licensed under the GNU GPL version 3, with additional permissions.\n" + - "See http://openrocket.sourceforge.net/ for details."; - - public LicenseDialog(JFrame parent) { - super(parent, true); - - JPanel panel = new JPanel(new MigLayout("fill")); - - panel.add(new StyledLabel("OpenRocket license", 10), "ax 50%, wrap para"); - - String licenseText; - try { - - BufferedReader reader = new BufferedReader( - new InputStreamReader(ClassLoader.getSystemResourceAsStream(LICENSE_FILENAME))); - StringBuffer sb = new StringBuffer(); - for (String s = reader.readLine(); s != null; s = reader.readLine()) { - sb.append(s); - sb.append('\n'); - } - licenseText = sb.toString(); - - } catch (IOException e) { - - licenseText = DEFAULT_LICENSE_TEXT; - - } - - JTextArea text = new JTextArea(licenseText); - text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - text.setRows(20); - text.setColumns(80); - text.setEditable(false); - panel.add(new JScrollPane(text),"grow, wrap para"); - - //Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - LicenseDialog.this.dispose(); - } - }); - panel.add(close, "right"); - - this.add(panel); - this.setTitle("OpenRocket license"); - this.pack(); - this.setLocationRelativeTo(parent); - - GUIUtil.setDisposableDialogOptions(this, close); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java b/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java deleted file mode 100644 index f19e590d..00000000 --- a/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java +++ /dev/null @@ -1,114 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.SplashScreen; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.Timer; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.database.ThrustCurveMotorSetDatabase; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * A progress dialog displayed while loading motors. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MotorDatabaseLoadingDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - private MotorDatabaseLoadingDialog(Window parent) { - //// Loading motors - super(parent, trans.get("MotorDbLoadDlg.title"), ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill")); - //// Loading motors... - panel.add(new JLabel(trans.get("MotorDbLoadDlg.Loadingmotors")), "wrap para"); - - JProgressBar progress = new JProgressBar(); - progress.setIndeterminate(true); - panel.add(progress, "growx"); - - this.add(panel); - this.pack(); - this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - this.setLocationByPlatform(true); - GUIUtil.setWindowIcons(this); - } - - - /** - * Check whether the motor database is loaded and block until it is. - * An uncloseable modal dialog window is opened while loading unless the splash screen - * is still being displayed. - * - * @param parent the parent window for the dialog, or <code>null</code> - */ - public static void check(Window parent) { - // TODO - ugly blind cast - final ThrustCurveMotorSetDatabase db = (ThrustCurveMotorSetDatabase) Application.getMotorSetDatabase(); - if (db.isLoaded()) - return; - - if (SplashScreen.getSplashScreen() == null) { - - log.info(1, "Motor database not loaded yet, displaying dialog"); - - final MotorDatabaseLoadingDialog dialog = new MotorDatabaseLoadingDialog(parent); - - final Timer timer = new Timer(100, new ActionListener() { - private int count = 0; - - @Override - public void actionPerformed(ActionEvent e) { - count++; - if (db.isLoaded()) { - log.debug("Database loaded, closing dialog"); - dialog.setVisible(false); - } else if (count % 10 == 0) { - log.debug("Database not loaded, count=" + count); - } - } - }); - - db.setInUse(); - timer.start(); - dialog.setVisible(true); - timer.stop(); - - } else { - - log.info(1, "Motor database not loaded yet, splash screen still present, delaying until loaded"); - - db.setInUse(); - int count = 0; - while (!db.isLoaded()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - // No-op - } - - count++; - if (count % 10 == 0) { - log.debug("Database not loaded, count=" + count); - } - } - - } - - log.info("Motor database now loaded"); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/PrintDialog.java b/src/net/sf/openrocket/gui/dialogs/PrintDialog.java deleted file mode 100644 index 729586d3..00000000 --- a/src/net/sf/openrocket/gui/dialogs/PrintDialog.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * PrintPanel.java - */ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Color; -import java.awt.Desktop; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Enumeration; -import java.util.Iterator; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JDialog; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.TreeNode; -import javax.swing.tree.TreePath; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.print.PrintController; -import net.sf.openrocket.gui.print.PrintSettings; -import net.sf.openrocket.gui.print.PrintableContext; -import net.sf.openrocket.gui.print.TemplateProperties; -import net.sf.openrocket.gui.print.components.CheckTreeManager; -import net.sf.openrocket.gui.print.components.RocketPrintTree; -import net.sf.openrocket.gui.util.FileHelper; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.startup.Application; - -/** - * This class isolates the Swing components used to create a panel that is added to a standard Java print dialog. - */ -public class PrintDialog extends JDialog implements TreeSelectionListener { - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private final Desktop desktop; - - private final RocketPrintTree stagedTree; - private final RocketPrintTree noStagedTree; - private OpenRocketDocument document; - private RocketPrintTree currentTree; - - private JButton previewButton; - private JButton saveAsPDF; - private JButton cancel; - - - private final static SwingPreferences prefs = (SwingPreferences) Application.getPreferences(); - - /** - * Constructor. - * - * @param orDocument the OR rocket container - */ - public PrintDialog(Window parent, OpenRocketDocument orDocument) { - super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); - - - JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel")); - this.add(panel); - - - // before any Desktop APIs are used, first check whether the API is - // supported by this particular VM on this particular host - if (Desktop.isDesktopSupported()) { - desktop = Desktop.getDesktop(); - } else { - desktop = null; - } - - document = orDocument; - Rocket rocket = orDocument.getRocket(); - - noStagedTree = RocketPrintTree.create(rocket.getName()); - noStagedTree.setShowsRootHandles(false); - CheckTreeManager ctm = new net.sf.openrocket.gui.print.components.CheckTreeManager(noStagedTree); - ctm.addTreeSelectionListener(this); - - final int stages = rocket.getStageCount(); - - - JLabel label = new JLabel(trans.get("lbl.selectElements")); - panel.add(label, "wrap unrel"); - - // Create the tree - if (stages > 1) { - stagedTree = RocketPrintTree.create(rocket.getName(), rocket.getChildren()); - ctm = new CheckTreeManager(stagedTree); - stagedTree.setShowsRootHandles(false); - ctm.addTreeSelectionListener(this); - } else { - stagedTree = noStagedTree; - } - currentTree = stagedTree; - - // Add the tree to the UI - final JScrollPane scrollPane = new JScrollPane(stagedTree); - panel.add(scrollPane, "width 400lp, height 200lp, grow, wrap para"); - - - // Checkboxes and buttons - final JCheckBox sortByStage = new JCheckBox(trans.get("checkbox.showByStage")); - sortByStage.setEnabled(stages > 1); - sortByStage.setSelected(stages > 1); - sortByStage.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (sortByStage.isEnabled()) { - if (((JCheckBox) e.getSource()).isSelected()) { - scrollPane.setViewportView(stagedTree); - stagedTree.setExpandsSelectedPaths(true); - currentTree = stagedTree; - } - else { - scrollPane.setViewportView(noStagedTree); - noStagedTree.setExpandsSelectedPaths(true); - currentTree = noStagedTree; - } - } - } - }); - panel.add(sortByStage, "aligny top, split"); - - - panel.add(new JPanel(), "growx"); - - - JButton settingsButton = new JButton(trans.get("printdlg.but.settings")); - settingsButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - PrintSettings settings = getPrintSettings(); - log.debug("settings=" + settings); - PrintSettingsDialog settingsDialog = new PrintSettingsDialog(PrintDialog.this, settings); - settingsDialog.setVisible(true); - setPrintSettings(settings); - } - }); - panel.add(settingsButton, "wrap para"); - - - previewButton = new JButton(trans.get("but.previewAndPrint")); - previewButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - onPreview(); - PrintDialog.this.setVisible(false); - } - }); - panel.add(previewButton, "split, right, gap para"); - - - saveAsPDF = new JButton(trans.get("printdlg.but.saveaspdf")); - saveAsPDF.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (onSavePDF()) { - PrintDialog.this.setVisible(false); - } - } - }); - panel.add(saveAsPDF, "right, gap para"); - - - cancel = new JButton(trans.get("button.cancel")); - cancel.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - PrintDialog.this.setVisible(false); - } - }); - panel.add(cancel, "right, gap para"); - - - expandAll(currentTree, true); - if (currentTree != noStagedTree) { - expandAll(noStagedTree, true); - } - - - GUIUtil.setDisposableDialogOptions(this, previewButton); - - } - - - @Override - public void valueChanged(final TreeSelectionEvent e) { - final TreePath path = e.getNewLeadSelectionPath(); - if (path != null) { - previewButton.setEnabled(true); - saveAsPDF.setEnabled(true); - } else { - previewButton.setEnabled(false); - saveAsPDF.setEnabled(false); - } - } - - /** - * If expand is true, expands all nodes in the tree. Otherwise, collapses all nodes in the theTree. - * - * @param theTree the tree to expand/contract - * @param expand expand if true, contract if not - */ - public void expandAll(RocketPrintTree theTree, boolean expand) { - TreeNode root = (TreeNode) theTree.getModel().getRoot(); - // Traverse theTree from root - expandAll(theTree, new TreePath(root), expand); - } - - /** - * Recursively walk a tree, and if expand is true, expands all nodes in the tree. Otherwise, collapses all nodes in - * the theTree. - * - * @param theTree the tree to expand/contract - * @param parent the node to iterate/recurse over - * @param expand expand if true, contract if not - */ - private void expandAll(RocketPrintTree theTree, TreePath parent, boolean expand) { - theTree.addSelectionPath(parent); - // Traverse children - TreeNode node = (TreeNode) parent.getLastPathComponent(); - if (node.getChildCount() >= 0) { - for (Enumeration<?> e = node.children(); e.hasMoreElements();) { - TreeNode n = (TreeNode) e.nextElement(); - TreePath path = parent.pathByAddingChild(n); - expandAll(theTree, path, expand); - } - } - // Expansion or collapse must be done bottom-up - if (expand) { - theTree.expandPath(parent); - } else { - theTree.collapsePath(parent); - } - } - - - - /** - * Generate a report using a temporary file. The file will be deleted upon JVM exit. - * - * @param paper the name of the paper size - * - * @return a file, populated with the "printed" output (the rocket info) - * - * @throws IOException thrown if the file could not be generated - */ - private File generateReport(PrintSettings settings) throws IOException { - final File f = File.createTempFile("openrocket-", ".pdf"); - f.deleteOnExit(); - return generateReport(f, settings); - } - - /** - * Generate a report to a specified file. - * - * @param f the file to which rocket data will be written - * @param paper the name of the paper size - * - * @return a file, populated with the "printed" output (the rocket info) - * - * @throws IOException thrown if the file could not be generated - */ - private File generateReport(File f, PrintSettings settings) throws IOException { - Iterator<PrintableContext> toBePrinted = currentTree.getToBePrinted(); - new PrintController().print(document, toBePrinted, new FileOutputStream(f), settings); - return f; - } - - - /** - * Handler for when the Preview button is clicked. - */ - private void onPreview() { - if (desktop != null) { - try { - PrintSettings settings = getPrintSettings(); - // TODO: HIGH: Remove UIManager, and pass settings to the actual printing methods - TemplateProperties.setColors(settings); - File f = generateReport(settings); - desktop.open(f); - } catch (IOException e) { - log.error("Could not open preview.", e); - JOptionPane.showMessageDialog(this, new String[] { - trans.get("error.preview.desc1"), - trans.get("error.preview.desc2") }, - trans.get("error.preview.title"), - JOptionPane.ERROR_MESSAGE); - } - } else { - JOptionPane.showMessageDialog(this, new String[] { - trans.get("error.preview.desc1"), - trans.get("error.preview.desc2") }, - trans.get("error.preview.title"), - JOptionPane.INFORMATION_MESSAGE); - } - } - - /** - * Handler for when the "Save as PDF" button is clicked. - * - * @return true if the PDF was saved - */ - private boolean onSavePDF() { - - JFileChooser chooser = new JFileChooser(); - chooser.setFileFilter(FileHelper.PDF_FILTER); - - // Select initial directory - File dir = document.getFile(); - if (dir != null) { - dir = dir.getParentFile(); - } - if (dir == null) { - dir = prefs.getDefaultDirectory(); - } - chooser.setCurrentDirectory(dir); - - int returnVal = chooser.showSaveDialog(this); - File file = chooser.getSelectedFile(); - if (returnVal == JFileChooser.APPROVE_OPTION && file != null) { - - file = FileHelper.ensureExtension(file, "pdf"); - if (!FileHelper.confirmWrite(file, this)) { - return false; - } - - try { - - PrintSettings settings = getPrintSettings(); - // TODO: HIGH: Remove UIManager, and pass settings to the actual printing methods - TemplateProperties.setColors(settings); - generateReport(file, settings); - - } catch (IOException e) { - FileHelper.errorWriting(e, this); - return false; - } - return true; - } else { - return false; - } - } - - public PrintSettings getPrintSettings() { - PrintSettings settings = new PrintSettings(); - Color c; - - c = prefs.getColor("print.template.fillColor", (java.awt.Color) null); - if (c != null) { - settings.setTemplateFillColor(c); - } - - c = prefs.getColor("print.template.borderColor", (java.awt.Color) null); - if (c != null) { - settings.setTemplateBorderColor(c); - } - - settings.setPaperSize(prefs.getEnum("print.paper.size", settings.getPaperSize())); - settings.setPaperOrientation(prefs.getEnum("print.paper.orientation", settings.getPaperOrientation())); - - return settings; - } - - public void setPrintSettings(PrintSettings settings) { - prefs.putColor("print.template.fillColor", settings.getTemplateFillColor() ); - prefs.putColor("print.template.borderColor", settings.getTemplateBorderColor() ); - prefs.putEnum("print.paper.size", settings.getPaperSize()); - prefs.putEnum("print.paper.orientation", settings.getPaperOrientation()); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/PrintSettingsDialog.java b/src/net/sf/openrocket/gui/dialogs/PrintSettingsDialog.java deleted file mode 100644 index 53411709..00000000 --- a/src/net/sf/openrocket/gui/dialogs/PrintSettingsDialog.java +++ /dev/null @@ -1,118 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Color; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.components.ColorChooserButton; -import net.sf.openrocket.gui.print.PaperOrientation; -import net.sf.openrocket.gui.print.PaperSize; -import net.sf.openrocket.gui.print.PrintSettings; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * This class is a dialog for displaying advanced settings for printing rocket related info. - */ -public class PrintSettingsDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - /** - * Construct a dialog for setting the advanced rocket print settings. - * - * @param parent the owning dialog - */ - public PrintSettingsDialog(Window parent, final PrintSettings settings) { - ////Print settings - super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); - - - JPanel panel = new JPanel(new MigLayout("fill")); - - ////Template fill color: - panel.add(new JLabel(trans.get("lbl.Templatefillcolor"))); - final ColorChooserButton fillColorButton = new ColorChooserButton(settings.getTemplateFillColor()); - fillColorButton.addColorPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - Color c = (Color) evt.getNewValue(); - log.info("Template fill color changed to " + c); - settings.setTemplateFillColor(c); - } - }); - panel.add(fillColorButton, "wrap para"); - - //// Template border color: - panel.add(new JLabel(trans.get("lbl.Templatebordercolor"))); - final ColorChooserButton borderColorButton = new ColorChooserButton(settings.getTemplateBorderColor()); - borderColorButton.addColorPropertyChangeListener(new PropertyChangeListener() { - @Override - public void propertyChange(PropertyChangeEvent evt) { - Color c = (Color) evt.getNewValue(); - log.info("Template border color changed to " + c); - settings.setTemplateBorderColor(c); - } - }); - panel.add(borderColorButton, "wrap para*2"); - - - - JComboBox combo = new JComboBox(new EnumModel<PaperSize>(settings, "PaperSize")); - ////Paper size: - panel.add(new JLabel(trans.get("lbl.Papersize"))); - panel.add(combo, "growx, wrap para"); - - - combo = new JComboBox(new EnumModel<PaperOrientation>(settings, "PaperOrientation")); - //// Paper orientation: - panel.add(new JLabel(trans.get("lbl.Paperorientation"))); - panel.add(combo, "growx, wrap para*2"); - - - - - //// Reset - JButton button = new JButton(trans.get("but.Reset")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Resetting print setting values to defaults"); - PrintSettings defaults = new PrintSettings(); - settings.loadFrom(defaults); - fillColorButton.setSelectedColor(settings.getTemplateFillColor()); - borderColorButton.setSelectedColor(settings.getTemplateBorderColor()); - } - }); - panel.add(button, "spanx, split, right"); - - //// Close - JButton closeButton = new JButton(trans.get("but.Close")); - closeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - PrintSettingsDialog.this.setVisible(false); - } - }); - panel.add(closeButton, "right"); - - this.add(panel); - GUIUtil.setDisposableDialogOptions(this, closeButton); - } - - -} diff --git a/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java b/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java deleted file mode 100644 index 34f3d646..00000000 --- a/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java +++ /dev/null @@ -1,606 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSpinner; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.BodyComponent; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RadiusRingComponent; -import net.sf.openrocket.rocketcomponent.RingComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.ShockCord; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.rocketcomponent.ThicknessRingComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.Reflection.Method; - -/** - * Dialog that allows scaling the rocket design. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ScaleDialog extends JDialog { - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - /* - * Scaler implementations - * - * Each scaled value (except override cg/mass) is defined using a Scaler instance. - */ - private static final Map<Class<? extends RocketComponent>, List<Scaler>> SCALERS = - new HashMap<Class<? extends RocketComponent>, List<Scaler>>(); - static { - List<Scaler> list; - - // RocketComponent - addScaler(RocketComponent.class, "PositionValue"); - SCALERS.get(RocketComponent.class).add(new OverrideScaler()); - - // BodyComponent - addScaler(BodyComponent.class, "Length"); - - // SymmetricComponent - addScaler(SymmetricComponent.class, "Thickness", "isFilled"); - - // Transition + Nose cone - addScaler(Transition.class, "ForeRadius", "isForeRadiusAutomatic"); - addScaler(Transition.class, "AftRadius", "isAftRadiusAutomatic"); - addScaler(Transition.class, "ForeShoulderRadius"); - addScaler(Transition.class, "ForeShoulderThickness"); - addScaler(Transition.class, "ForeShoulderLength"); - addScaler(Transition.class, "AftShoulderRadius"); - addScaler(Transition.class, "AftShoulderThickness"); - addScaler(Transition.class, "AftShoulderLength"); - - // Body tube - addScaler(BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic"); - addScaler(BodyTube.class, "MotorOverhang"); - - // Launch lug - addScaler(LaunchLug.class, "OuterRadius"); - addScaler(LaunchLug.class, "Thickness"); - addScaler(LaunchLug.class, "Length"); - - // FinSet - addScaler(FinSet.class, "Thickness"); - addScaler(FinSet.class, "TabHeight"); - addScaler(FinSet.class, "TabLength"); - addScaler(FinSet.class, "TabShift"); - - // TrapezoidFinSet - addScaler(TrapezoidFinSet.class, "Sweep"); - addScaler(TrapezoidFinSet.class, "RootChord"); - addScaler(TrapezoidFinSet.class, "TipChord"); - addScaler(TrapezoidFinSet.class, "Height"); - - // EllipticalFinSet - addScaler(EllipticalFinSet.class, "Length"); - addScaler(EllipticalFinSet.class, "Height"); - - // FreeformFinSet - list = new ArrayList<ScaleDialog.Scaler>(1); - list.add(new FreeformFinSetScaler()); - SCALERS.put(FreeformFinSet.class, list); - - // MassObject - addScaler(MassObject.class, "Length"); - addScaler(MassObject.class, "Radius"); - addScaler(MassObject.class, "RadialPosition"); - - // MassComponent - list = new ArrayList<ScaleDialog.Scaler>(1); - list.add(new MassComponentScaler()); - SCALERS.put(MassComponent.class, list); - - // Parachute - addScaler(Parachute.class, "Diameter"); - addScaler(Parachute.class, "LineLength"); - - // Streamer - addScaler(Streamer.class, "StripLength"); - addScaler(Streamer.class, "StripWidth"); - - // ShockCord - addScaler(ShockCord.class, "CordLength"); - - // RingComponent - addScaler(RingComponent.class, "Length"); - addScaler(RingComponent.class, "RadialPosition"); - - // ThicknessRingComponent - addScaler(ThicknessRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic"); - addScaler(ThicknessRingComponent.class, "Thickness"); - - // InnerTube - addScaler(InnerTube.class, "MotorOverhang"); - - // RadiusRingComponent - addScaler(RadiusRingComponent.class, "OuterRadius", "isOuterRadiusAutomatic"); - addScaler(RadiusRingComponent.class, "InnerRadius", "isInnerRadiusAutomatic"); - } - - private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName) { - addScaler(componentClass, methodName, null); - } - - private static void addScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) { - List<Scaler> list = SCALERS.get(componentClass); - if (list == null) { - list = new ArrayList<ScaleDialog.Scaler>(); - SCALERS.put(componentClass, list); - } - list.add(new GeneralScaler(componentClass, methodName, autoMethodName)); - } - - - - - - private static final double DEFAULT_INITIAL_SIZE = 0.1; // meters - private static final double SCALE_MIN = 0.01; - private static final double SCALE_MAX = 100.0; - - private static final String SCALE_ROCKET = trans.get("lbl.scaleRocket"); - private static final String SCALE_SUBSELECTION = trans.get("lbl.scaleSubselection"); - private static final String SCALE_SELECTION = trans.get("lbl.scaleSelection"); - - - - - private final DoubleModel multiplier = new DoubleModel(1.0, UnitGroup.UNITS_RELATIVE, SCALE_MIN, SCALE_MAX); - private final DoubleModel fromField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); - private final DoubleModel toField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); - - private final OpenRocketDocument document; - private final RocketComponent selection; - - private final JComboBox selectionOption; - private final JCheckBox scaleMassValues; - - private boolean changing = false; - - /** - * Sole constructor. - * - * @param document the document to modify. - * @param selection the currently selected component (or <code>null</code> if none selected). - * @param parent the parent window. - */ - public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) { - super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); - - this.document = document; - this.selection = selection; - - // Generate options for scaling - List<String> options = new ArrayList<String>(); - options.add(SCALE_ROCKET); - if (selection != null && selection.getChildCount() > 0) { - options.add(SCALE_SUBSELECTION); - } - if (selection != null) { - options.add(SCALE_SELECTION); - } - - - /* - * Select initial size for "from" field. - * - * If a component is selected, either its diameter (for SymmetricComponents) or length is selected. - * Otherwise the maximum body diameter is selected. As a fallback DEFAULT_INITIAL_SIZE is used. - */ - // - double initialSize = 0; - if (selection != null) { - if (selection instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent) selection; - initialSize = s.getForeRadius() * 2; - initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); - } else { - initialSize = selection.getLength(); - } - } else { - for (RocketComponent c : document.getRocket()) { - if (c instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent) c; - initialSize = s.getForeRadius() * 2; - initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); - } - } - } - if (initialSize < 0.001) { - Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - initialSize = unit.fromUnit(unit.round(unit.toUnit(DEFAULT_INITIAL_SIZE))); - } - - fromField.setValue(initialSize); - toField.setValue(initialSize); - - - // Add actions to the values - multiplier.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - if (!changing) { - changing = true; - updateToField(); - changing = false; - } - } - }); - fromField.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - if (!changing) { - changing = true; - updateToField(); - changing = false; - } - } - }); - toField.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - if (!changing) { - changing = true; - updateMultiplier(); - changing = false; - } - } - }); - - - - String tip; - JPanel panel = new JPanel(new MigLayout("gap rel unrel", "[][65lp::][30lp::][]", "")); - this.add(panel); - - - // Scaling selection - tip = trans.get("lbl.scale.ttip"); - JLabel label = new JLabel(trans.get("lbl.scale")); - label.setToolTipText(tip); - panel.add(label, "span, split, gapright unrel"); - - selectionOption = new JComboBox(options.toArray()); - selectionOption.setEditable(false); - selectionOption.setToolTipText(tip); - panel.add(selectionOption, "growx, wrap para*2"); - - - // Scale multiplier - tip = trans.get("lbl.scaling.ttip"); - label = new JLabel(trans.get("lbl.scaling")); - label.setToolTipText(tip); - panel.add(label, "gapright unrel"); - - - JSpinner spin = new JSpinner(multiplier.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - panel.add(spin, "w :30lp:65lp"); - - UnitSelector unit = new UnitSelector(multiplier); - unit.setToolTipText(tip); - panel.add(unit, "w 30lp"); - BasicSlider slider = new BasicSlider(multiplier.getSliderModel(0.25, 1.0, 4.0)); - slider.setToolTipText(tip); - panel.add(slider, "w 100lp, growx, wrap para"); - - - // Scale from ... to ... - tip = trans.get("lbl.scaleFromTo.ttip"); - label = new JLabel(trans.get("lbl.scaleFrom")); - label.setToolTipText(tip); - panel.add(label, "gapright unrel, right"); - - spin = new JSpinner(fromField.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - panel.add(spin, "span, split, w :30lp:65lp"); - - unit = new UnitSelector(fromField); - unit.setToolTipText(tip); - panel.add(unit, "w 30lp"); - - label = new JLabel(trans.get("lbl.scaleTo")); - label.setToolTipText(tip); - panel.add(label, "gap unrel"); - - spin = new JSpinner(toField.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - panel.add(spin, "w :30lp:65lp"); - - unit = new UnitSelector(toField); - unit.setToolTipText(tip); - panel.add(unit, "w 30lp, wrap para*2"); - - - // Scale override - scaleMassValues = new JCheckBox(trans.get("checkbox.scaleMass")); - scaleMassValues.setToolTipText(trans.get("checkbox.scaleMass.ttip")); - scaleMassValues.setSelected(true); - boolean overridden = false; - for (RocketComponent c : document.getRocket()) { - if (c instanceof MassComponent || c.isMassOverridden()) { - overridden = true; - break; - } - } - scaleMassValues.setEnabled(overridden); - panel.add(scaleMassValues, "span, wrap para*3"); - - - // Buttons - - JButton scale = new JButton(trans.get("button.scale")); - scale.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - doScale(); - ScaleDialog.this.setVisible(false); - } - }); - panel.add(scale, "span, split, right, gap para"); - - JButton cancel = new JButton(trans.get("button.cancel")); - cancel.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ScaleDialog.this.setVisible(false); - } - }); - panel.add(cancel, "right, gap para"); - - - - GUIUtil.setDisposableDialogOptions(this, scale); - } - - - - private void doScale() { - double mul = multiplier.getValue(); - if (!(SCALE_MIN <= mul && mul <= SCALE_MAX)) { - Application.getExceptionHandler().handleErrorCondition("Illegal multiplier value, mul=" + mul); - return; - } - - if (MathUtil.equals(mul, 1.0)) { - // Nothing to do - log.user("Scaling by value 1.0 - nothing to do"); - return; - } - - boolean scaleMass = scaleMassValues.isSelected(); - - Object item = selectionOption.getSelectedItem(); - log.user("Scaling design by factor " + mul + ", option=" + item); - if (SCALE_ROCKET.equals(item)) { - - // Scale the entire rocket design - try { - document.startUndo(trans.get("undo.scaleRocket")); - for (RocketComponent c : document.getRocket()) { - scale(c, mul, scaleMass); - } - } finally { - document.stopUndo(); - } - - } else if (SCALE_SUBSELECTION.equals(item)) { - - // Scale component and subcomponents - try { - document.startUndo(trans.get("undo.scaleComponents")); - for (RocketComponent c : selection) { - scale(c, mul, scaleMass); - } - } finally { - document.stopUndo(); - } - - } else if (SCALE_SELECTION.equals(item)) { - - // Scale only the selected component - try { - document.startUndo(trans.get("undo.scaleComponent")); - scale(selection, mul, scaleMass); - } finally { - document.stopUndo(); - } - - } else { - throw new BugException("Unknown item selected, item=" + item); - } - } - - - /** - * Perform scaling on a single component. - */ - private void scale(RocketComponent component, double mul, boolean scaleMass) { - - Class<?> clazz = component.getClass(); - while (clazz != null) { - List<Scaler> list = SCALERS.get(clazz); - if (list != null) { - for (Scaler s : list) { - s.scale(component, mul, scaleMass); - } - } - - clazz = clazz.getSuperclass(); - } - } - - - private void updateToField() { - double mul = multiplier.getValue(); - double from = fromField.getValue(); - double to = from * mul; - toField.setValue(to); - } - - private void updateMultiplier() { - double from = fromField.getValue(); - double to = toField.getValue(); - double mul = to / from; - - if (!MathUtil.equals(from, 0)) { - mul = MathUtil.clamp(mul, SCALE_MIN, SCALE_MAX); - multiplier.setValue(mul); - } - updateToField(); - } - - - - /** - * Interface for scaling a specific component/value. - */ - private interface Scaler { - public void scale(RocketComponent c, double multiplier, boolean scaleMass); - } - - /** - * General scaler implementation that uses reflection to get/set a specific value. - */ - private static class GeneralScaler implements Scaler { - - private final Method getter; - private final Method setter; - private final Method autoMethod; - - public GeneralScaler(Class<? extends RocketComponent> componentClass, String methodName, String autoMethodName) { - - getter = Reflection.findMethod(componentClass, "get" + methodName); - setter = Reflection.findMethod(componentClass, "set" + methodName, double.class); - if (autoMethodName != null) { - autoMethod = Reflection.findMethod(componentClass, autoMethodName); - } else { - autoMethod = null; - } - - } - - @Override - public void scale(RocketComponent c, double multiplier, boolean scaleMass) { - - // Do not scale if set to automatic - if (autoMethod != null) { - boolean auto = (Boolean) autoMethod.invoke(c); - if (auto) { - return; - } - } - - // Scale value - double value = (Double) getter.invoke(c); - value = value * multiplier; - setter.invoke(c, value); - } - - } - - - private static class OverrideScaler implements Scaler { - - @Override - public void scale(RocketComponent component, double multiplier, boolean scaleMass) { - - if (component.isCGOverridden()) { - double cgx = component.getOverrideCGX(); - cgx = cgx * multiplier; - component.setOverrideCGX(cgx); - } - - if (scaleMass && component.isMassOverridden()) { - double mass = component.getOverrideMass(); - mass = mass * MathUtil.pow3(multiplier); - component.setOverrideMass(mass); - } - } - - } - - private static class MassComponentScaler implements Scaler { - - @Override - public void scale(RocketComponent component, double multiplier, boolean scaleMass) { - if (scaleMass) { - MassComponent c = (MassComponent) component; - double mass = c.getComponentMass(); - mass = mass * MathUtil.pow3(multiplier); - c.setComponentMass(mass); - } - } - - } - - private static class FreeformFinSetScaler implements Scaler { - - @Override - public void scale(RocketComponent component, double multiplier, boolean scaleMass) { - FreeformFinSet finset = (FreeformFinSet) component; - Coordinate[] points = finset.getFinPoints(); - for (int i = 0; i < points.length; i++) { - points[i] = points[i].multiply(multiplier); - } - try { - finset.setPoints(points); - } catch (IllegalFinPointException e) { - throw new BugException("Failed to set points after scaling, original=" + Arrays.toString(finset.getFinPoints()) + " scaled=" + Arrays.toString(points), e); - } - } - - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java b/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java deleted file mode 100644 index 13fb93b0..00000000 --- a/src/net/sf/openrocket/gui/dialogs/SwingWorkerDialog.java +++ /dev/null @@ -1,181 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Dimension; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.SwingWorker; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; - - -/** - * A modal dialog that runs specific SwingWorkers and waits until they complete. - * A message and progress bar is provided and a cancel button. If the cancel button - * is pressed, the currently running worker is interrupted and the later workers are not - * executed. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SwingWorkerDialog extends JDialog implements PropertyChangeListener { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - /** Number of milliseconds to wait at a time between checking worker status */ - private static final int DELAY = 100; - - /** Minimum number of milliseconds to wait before estimating work length */ - private static final int ESTIMATION_DELAY = 190; - - /** Open the dialog if estimated remaining time is longer than this */ - private static final int REMAINING_TIME_FOR_DIALOG = 1000; - - /** Open the dialog if estimated total time is longed than this */ - private static final int TOTAL_TIME_FOR_DIALOG = 2000; - - - private final SwingWorker<?, ?> worker; - private final JProgressBar progressBar; - - private boolean cancelled = false; - - - private SwingWorkerDialog(Window parent, String title, String label, SwingWorker<?, ?> w) { - super(parent, title, ModalityType.APPLICATION_MODAL); - - this.worker = w; - w.addPropertyChangeListener(this); - - JPanel panel = new JPanel(new MigLayout("fill")); - - if (label != null) { - panel.add(new JLabel(label), "wrap para"); - } - - progressBar = new JProgressBar(); - panel.add(progressBar, "growx, wrap para"); - - //// Cancel button - JButton cancel = new JButton(trans.get("dlg.but.cancel")); - cancel.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("User cancelled SwingWorker operation"); - cancel(); - } - }); - panel.add(cancel, "right"); - - this.add(panel); - this.setMinimumSize(new Dimension(250, 100)); - this.pack(); - this.setLocationRelativeTo(parent); - this.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); - } - - @Override - public void propertyChange(PropertyChangeEvent evt) { - if (worker.getState() == SwingWorker.StateValue.DONE) { - close(); - } - progressBar.setValue(worker.getProgress()); - } - - private void cancel() { - cancelled = true; - worker.cancel(true); - close(); - } - - private void close() { - worker.removePropertyChangeListener(this); - this.setVisible(false); - // For some reason setVisible(false) is not always enough... - this.dispose(); - } - - - /** - * Run a SwingWorker and if necessary show a dialog displaying the progress of - * the worker. The progress information is obtained from the SwingWorker's - * progress property. The dialog is shown only if the worker is estimated to - * take a notable amount of time. - * <p> - * The dialog contains a cancel button. Clicking it will call - * <code>worker.cancel(true)</code> and close the dialog immediately. - * - * @param parent the parent window for the dialog, or <code>null</code>. - * @param title the title for the dialog. - * @param label an additional label for the dialog, or <code>null</code>. - * @param worker the SwingWorker to execute. - * @return <code>true</code> if the worker has completed normally, - * <code>false</code> if the user cancelled the operation - */ - public static boolean runWorker(Window parent, String title, String label, - SwingWorker<?, ?> worker) { - - log.info("Running SwingWorker " + worker); - - // Start timing the worker - final long startTime = System.currentTimeMillis(); - worker.execute(); - - // Monitor worker thread before opening the dialog - while (true) { - - try { - Thread.sleep(DELAY); - } catch (InterruptedException e) { - // Should never occur - log.error("EDT was interrupted", e); - } - - if (worker.isDone()) { - // Worker has completed within time limits - log.info("Worker completed before opening dialog"); - return true; - } - - // Check whether enough time has gone to get realistic estimate - long elapsed = System.currentTimeMillis() - startTime; - if (elapsed < ESTIMATION_DELAY) - continue; - - - // Calculate and check estimated remaining time - int progress = MathUtil.clamp(worker.getProgress(), 1, 100); // Avoid div-by-zero - long estimate = elapsed * 100 / progress; - long remaining = estimate - elapsed; - - log.debug("Estimated run time, estimate=" + estimate + " remaining=" + remaining); - - if (estimate >= TOTAL_TIME_FOR_DIALOG) - break; - - if (remaining >= REMAINING_TIME_FOR_DIALOG) - break; - } - - - // Dialog is required - - log.info("Opening dialog for SwingWorker " + worker); - SwingWorkerDialog dialog = new SwingWorkerDialog(parent, title, label, worker); - dialog.setVisible(true); - log.info("Worker done, cancelled=" + dialog.cancelled); - - return !dialog.cancelled; - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java b/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java deleted file mode 100644 index 3ec6d6a1..00000000 --- a/src/net/sf/openrocket/gui/dialogs/UpdateInfoDialog.java +++ /dev/null @@ -1,97 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.Collections; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.communication.UpdateInfo; -import net.sf.openrocket.gui.components.URLLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.ComparablePair; - -public class UpdateInfoDialog extends JDialog { - - private final JCheckBox remind; - private static final Translator trans = Application.getTranslator(); - - public UpdateInfoDialog(UpdateInfo info) { - //// OpenRocket update available - super((Window)null, "OpenRocket update available", ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill")); - - - panel.add(new JLabel(Icons.loadImageIcon("pix/icon/icon-about.png", "OpenRocket")), - "spany 100, top"); - - //// <html><b>OpenRocket version - panel.add(new JLabel("<html><b>OpenRocket version " + info.getLatestVersion() + - " is available!"), "wrap para"); - - List<ComparablePair<Integer, String>> updates = info.getUpdates(); - if (updates.size() > 0) { - //// Updates include: - panel.add(new JLabel("Updates include:"), "wrap rel"); - - Collections.sort(updates); - int count = 0; - int n = -1; - for (int i=updates.size()-1; i>=0; i--) { - // Add only specific number of top features - if (count >= 4 && n != updates.get(i).getU()) - break; - n = updates.get(i).getU(); - panel.add(new JLabel(" " + Chars.BULLET + " " + updates.get(i).getV()), - "wrap 0px"); - count++; - } - } - - //// Download the new version from: - panel.add(new JLabel("Download the new version from:"), - "gaptop para, alignx 50%, wrap unrel"); - panel.add(new URLLabel(AboutDialog.OPENROCKET_URL), "alignx 50%, wrap para"); - - //// Remind me later - remind = new JCheckBox("Remind me later"); - //// Show this update also the next time you start OpenRocket - remind.setToolTipText("Show this update also the next time you start OpenRocket"); - remind.setSelected(true); - panel.add(remind); - - //Close button - JButton button = new JButton(trans.get("dlg.but.close")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - UpdateInfoDialog.this.dispose(); - } - }); - panel.add(button, "right, gapright para"); - - this.add(panel); - - this.pack(); - this.setLocationRelativeTo(null); - GUIUtil.setDisposableDialogOptions(this, button); - } - - - public boolean isReminderSelected() { - return remind.isSelected(); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/WarningDialog.java b/src/net/sf/openrocket/gui/dialogs/WarningDialog.java deleted file mode 100644 index 30ab240f..00000000 --- a/src/net/sf/openrocket/gui/dialogs/WarningDialog.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.gui.dialogs; - -import java.awt.Component; - -import javax.swing.JDialog; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JScrollPane; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; - -public class WarningDialog extends JDialog { - - public static void showWarnings(Component parent, Object message, String title, - WarningSet warnings) { - - Warning[] w = warnings.toArray(new Warning[0]); - JList list = new JList(w); - JScrollPane pane = new JScrollPane(list); - - JOptionPane.showMessageDialog(parent, new Object[] { message, pane }, - title, JOptionPane.WARNING_MESSAGE); - - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/CloseableDialog.java b/src/net/sf/openrocket/gui/dialogs/motor/CloseableDialog.java deleted file mode 100644 index 48e58a0f..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/CloseableDialog.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor; - -public interface CloseableDialog { - - /** - * Close this dialog. - * - * @param ok whether "OK" should be considered to have been clicked. - */ - public void close(boolean ok); - -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java b/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java deleted file mode 100644 index eeb939c7..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/MotorChooserDialog.java +++ /dev/null @@ -1,115 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor; - - -import java.awt.Dialog; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog; -import net.sf.openrocket.gui.dialogs.motor.thrustcurve.ThrustCurveMotorSelectionPanel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; - -public class MotorChooserDialog extends JDialog implements CloseableDialog { - - private final ThrustCurveMotorSelectionPanel selectionPanel; - - private boolean okClicked = false; - private static final Translator trans = Application.getTranslator(); - - - public MotorChooserDialog(Motor current, double delay, double diameter, Window owner) { - super(owner, "Select a rocket motor", Dialog.ModalityType.APPLICATION_MODAL); - - // Check that the motor database has been loaded properly - MotorDatabaseLoadingDialog.check(null); - - - JPanel panel = new JPanel(new MigLayout("fill")); - - selectionPanel = new ThrustCurveMotorSelectionPanel((ThrustCurveMotor) current, delay, diameter); - - panel.add(selectionPanel, "grow, wrap para"); - - - // OK / Cancel buttons - JButton okButton = new JButton(trans.get("dlg.but.ok")); - okButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - close(true); - } - }); - panel.add(okButton, "tag ok, spanx, split"); - - //// Cancel button - JButton cancelButton = new JButton(trans.get("dlg.but.cancel")); - cancelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - close(false); - } - }); - panel.add(cancelButton, "tag cancel"); - - this.add(panel); - - this.setModal(true); - this.pack(); - this.setLocationByPlatform(true); - GUIUtil.setDisposableDialogOptions(this, okButton); - - JComponent focus = selectionPanel.getDefaultFocus(); - if (focus != null) { - focus.grabFocus(); - } - - // Set the closeable dialog after all initialization - selectionPanel.setCloseableDialog(this); - } - - - /** - * Return the motor selected by this chooser dialog, or <code>null</code> if the selection has been aborted. - * - * @return the selected motor, or <code>null</code> if no motor has been selected or the selection was canceled. - */ - public Motor getSelectedMotor() { - if (!okClicked) - return null; - return selectionPanel.getSelectedMotor(); - } - - /** - * Return the selected ejection charge delay. - * - * @return the selected ejection charge delay. - */ - public double getSelectedDelay() { - return selectionPanel.getSelectedDelay(); - } - - - - @Override - public void close(boolean ok) { - okClicked = ok; - this.setVisible(false); - - Motor selected = getSelectedMotor(); - if (okClicked && selected != null) { - selectionPanel.selectedMotor(selected); - } - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/MotorSelector.java b/src/net/sf/openrocket/gui/dialogs/motor/MotorSelector.java deleted file mode 100644 index 23bd9984..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/MotorSelector.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor; - -import javax.swing.JComponent; - -import net.sf.openrocket.motor.Motor; - -public interface MotorSelector { - - /** - * Return the currently selected motor. - * - * @return the currently selected motor, or <code>null</code> if no motor is selected. - */ - public Motor getSelectedMotor(); - - /** - * Return the currently selected ejection charge delay. - * - * @return the currently selected ejection charge delay. - */ - public double getSelectedDelay(); - - /** - * Return the component that should have the default focus in this motor selector panel. - * - * @return the component that should have default focus, or <code>null</code> for none. - */ - public JComponent getDefaultFocus(); - - /** - * Notify that the provided motor has been selected. This can be used to store preference - * data for later usage. - * - * @param m the motor that was selected. - */ - public void selectedMotor(Motor m); - -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorHolder.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorHolder.java deleted file mode 100644 index bb811333..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/MotorHolder.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor.thrustcurve; - -import net.sf.openrocket.motor.ThrustCurveMotor; - -class MotorHolder { - - private final ThrustCurveMotor motor; - private final int index; - - public MotorHolder(ThrustCurveMotor motor, int index) { - this.motor = motor; - this.index = index; - } - - public ThrustCurveMotor getMotor() { - return motor; - } - - public int getIndex() { - return index; - } - - @Override - public String toString() { - return motor.getDesignation(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof MotorHolder)) - return false; - MotorHolder other = (MotorHolder) obj; - return this.motor.equals(other.motor); - } - - @Override - public int hashCode() { - return motor.hashCode(); - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorColumns.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorColumns.java deleted file mode 100644 index d4cefca2..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorColumns.java +++ /dev/null @@ -1,146 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor.thrustcurve; - -import java.text.Collator; -import java.util.Comparator; - -import net.sf.openrocket.database.ThrustCurveMotorSet; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.DesignationComparator; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.unit.ValueComparator; - - -/** - * Enum defining the table columns. - */ - -enum ThrustCurveMotorColumns { - //// Manufacturer - MANUFACTURER("TCurveMotorCol.MANUFACTURER", 100) { - @Override - public String getValue(ThrustCurveMotorSet m) { - return m.getManufacturer().getDisplayName(); - } - - @Override - public Comparator<?> getComparator() { - return Collator.getInstance(); - } - }, - //// Designation - DESIGNATION("TCurveMotorCol.DESIGNATION") { - @Override - public String getValue(ThrustCurveMotorSet m) { - return m.getDesignation(); - } - - @Override - public Comparator<?> getComparator() { - return new DesignationComparator(); - } - }, - //// Type - TYPE("TCurveMotorCol.TYPE") { - @Override - public String getValue(ThrustCurveMotorSet m) { - return m.getType().getName(); - } - - @Override - public Comparator<?> getComparator() { - return Collator.getInstance(); - } - }, - //// Diameter - DIAMETER("TCurveMotorCol.DIAMETER") { - @Override - public Object getValue(ThrustCurveMotorSet m) { - return new Value(m.getDiameter(), UnitGroup.UNITS_MOTOR_DIMENSIONS); - } - - @Override - public Comparator<?> getComparator() { - return ValueComparator.INSTANCE; - } - }, - //// Length - LENGTH("TCurveMotorCol.LENGTH") { - @Override - public Object getValue(ThrustCurveMotorSet m) { - return new Value(m.getLength(), UnitGroup.UNITS_MOTOR_DIMENSIONS); - } - - @Override - public Comparator<?> getComparator() { - return ValueComparator.INSTANCE; - } - }; - - - private final String title; - private final int width; - private static final Translator trans = Application.getTranslator(); - - ThrustCurveMotorColumns(String title) { - this(title, 50); - } - - ThrustCurveMotorColumns(String title, int width) { - this.title = title; - this.width = width; - } - - - public abstract Object getValue(ThrustCurveMotorSet m); - - public abstract Comparator<?> getComparator(); - - public String getTitle() { - return trans.get(title); - } - - public int getWidth() { - return width; - } - - public String getToolTipText(ThrustCurveMotor m) { - String tip = "<html>"; - tip += "<b>" + m.toString() + "</b>"; - tip += " (" + m.getMotorType().getDescription() + ")<br><hr>"; - - String desc = m.getDescription().trim(); - if (desc.length() > 0) { - tip += "<i>" + desc.replace("\n", "<br>") + "</i><br><hr>"; - } - - tip += ("Diameter: " + - UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getDiameter()) + - "<br>"); - tip += ("Length: " + - UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getLength()) + - "<br>"); - tip += ("Maximum thrust: " + - UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getMaxThrustEstimate()) + - "<br>"); - tip += ("Average thrust: " + - UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getAverageThrustEstimate()) + - "<br>"); - tip += ("Burn time: " + - UnitGroup.UNITS_SHORT_TIME.getDefaultUnit() - .toStringUnit(m.getBurnTimeEstimate()) + "<br>"); - tip += ("Total impulse: " + - UnitGroup.UNITS_IMPULSE.getDefaultUnit() - .toStringUnit(m.getTotalImpulseEstimate()) + "<br>"); - tip += ("Launch mass: " + - UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(m.getLaunchCG().weight) + - "<br>"); - tip += ("Empty mass: " + - UnitGroup.UNITS_MASS.getDefaultUnit() - .toStringUnit(m.getEmptyCG().weight)); - return tip; - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorComparator.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorComparator.java deleted file mode 100644 index 0096bf17..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorComparator.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor.thrustcurve; - -import java.util.Comparator; - -import net.sf.openrocket.motor.ThrustCurveMotor; - -/** - * Compares two ThrustCurveMotor objects for quality. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ThrustCurveMotorComparator implements Comparator<ThrustCurveMotor> { - - - @Override - public int compare(ThrustCurveMotor o1, ThrustCurveMotor o2) { - return calculateGoodness(o2) - calculateGoodness(o1); - } - - - private int calculateGoodness(ThrustCurveMotor motor) { - /* - * 10 chars of comments correspond to one thrust point, max ten points. - */ - int commentLength = Math.min(motor.getDescription().length(), 100); - return motor.getTimePoints().length * 10 + commentLength; - } - - -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorDatabaseModel.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorDatabaseModel.java deleted file mode 100644 index 2dc92bd3..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorDatabaseModel.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor.thrustcurve; - -import java.util.List; - -import javax.swing.table.AbstractTableModel; - -import net.sf.openrocket.database.ThrustCurveMotorSet; - -class ThrustCurveMotorDatabaseModel extends AbstractTableModel { - private final List<ThrustCurveMotorSet> database; - - public ThrustCurveMotorDatabaseModel(List<ThrustCurveMotorSet> database) { - this.database = database; - } - - @Override - public int getColumnCount() { - return ThrustCurveMotorColumns.values().length; - } - - @Override - public int getRowCount() { - return database.size(); - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - ThrustCurveMotorColumns column = getColumn(columnIndex); - return column.getValue(database.get(rowIndex)); - } - - @Override - public String getColumnName(int columnIndex) { - return getColumn(columnIndex).getTitle(); - } - - - public ThrustCurveMotorSet getMotorSet(int rowIndex) { - return database.get(rowIndex); - } - - - public int getIndex(ThrustCurveMotorSet m) { - return database.indexOf(m); - } - - private ThrustCurveMotorColumns getColumn(int index) { - return ThrustCurveMotorColumns.values()[index]; - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorPlotDialog.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorPlotDialog.java deleted file mode 100644 index c1ecb7cf..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorPlotDialog.java +++ /dev/null @@ -1,134 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor.thrustcurve; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Rectangle; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.List; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.StandardXYItemRenderer; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - -public class ThrustCurveMotorPlotDialog extends JDialog { - private static final Translator trans = Application.getTranslator(); - - public ThrustCurveMotorPlotDialog(List<ThrustCurveMotor> motors, int selected, Window parent) { - super(parent, "Motor thrust curves", ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill")); - - // Thrust curve plot - JFreeChart chart = ChartFactory.createXYLineChart( - "Motor thrust curves", // title - "Time / " + UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().getUnit(), // xAxisLabel - "Thrust / " + UnitGroup.UNITS_FORCE.getDefaultUnit().getUnit(), // yAxisLabel - null, // dataset - PlotOrientation.VERTICAL, - true, // legend - true, // tooltips - false // urls - ); - - - // Add the data and formatting to the plot - XYPlot plot = chart.getXYPlot(); - - chart.setBackgroundPaint(panel.getBackground()); - plot.setBackgroundPaint(Color.WHITE); - plot.setDomainGridlinePaint(Color.LIGHT_GRAY); - plot.setRangeGridlinePaint(Color.LIGHT_GRAY); - - ChartPanel chartPanel = new ChartPanel(chart, - false, // properties - true, // save - false, // print - true, // zoom - true); // tooltips - chartPanel.setMouseWheelEnabled(true); - chartPanel.setEnforceFileExtensions(true); - chartPanel.setInitialDelay(500); - - StandardXYItemRenderer renderer = new StandardXYItemRenderer(); - renderer.setBaseShapesVisible(true); - renderer.setBaseShapesFilled(true); - plot.setRenderer(renderer); - - - // Create the plot data set - XYSeriesCollection dataset = new XYSeriesCollection(); - - // Selected thrust curve - int n = 0; - if (selected >= 0) { - dataset.addSeries(generateSeries(motors.get(selected))); - renderer.setSeriesStroke(n, new BasicStroke(1.5f)); - renderer.setSeriesPaint(n, ThrustCurveMotorSelectionPanel.getColor(selected)); - } - n++; - - // Other thrust curves - for (int i = 0; i < motors.size(); i++) { - if (i == selected) - continue; - - ThrustCurveMotor m = motors.get(i); - dataset.addSeries(generateSeries(m)); - renderer.setSeriesStroke(n, new BasicStroke(1.5f)); - renderer.setSeriesPaint(n, ThrustCurveMotorSelectionPanel.getColor(i)); - renderer.setSeriesShape(n, new Rectangle()); - n++; - } - - plot.setDataset(dataset); - - panel.add(chartPanel, "width 600:600:, height 400:400:, grow, wrap para"); - - - // Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - ThrustCurveMotorPlotDialog.this.setVisible(false); - } - }); - panel.add(close, "right, tag close"); - - - this.add(panel); - - this.pack(); - GUIUtil.setDisposableDialogOptions(this, null); - } - - - private XYSeries generateSeries(ThrustCurveMotor motor) { - XYSeries series = new XYSeries(motor.getManufacturer() + " " + motor.getDesignation()); - double[] time = motor.getTimePoints(); - double[] thrust = motor.getThrustPoints(); - - for (int j = 0; j < time.length; j++) { - series.add(time[j], thrust[j]); - } - return series; - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java b/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java deleted file mode 100644 index e4c22229..00000000 --- a/src/net/sf/openrocket/gui/dialogs/motor/thrustcurve/ThrustCurveMotorSelectionPanel.java +++ /dev/null @@ -1,991 +0,0 @@ -package net.sf.openrocket.gui.dialogs.motor.thrustcurve; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Component; -import java.awt.Cursor; -import java.awt.Font; -import java.awt.Paint; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.prefs.Preferences; - -import javax.swing.BorderFactory; -import javax.swing.DefaultComboBoxModel; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.JLayeredPane; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSeparator; -import javax.swing.JTable; -import javax.swing.JTextArea; -import javax.swing.JTextField; -import javax.swing.ListCellRenderer; -import javax.swing.ListSelectionModel; -import javax.swing.RowFilter; -import javax.swing.SwingUtilities; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.TableModel; -import javax.swing.table.TableRowSorter; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.database.ThrustCurveMotorSet; -import net.sf.openrocket.database.ThrustCurveMotorSetDatabase; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.dialogs.motor.CloseableDialog; -import net.sf.openrocket.gui.dialogs.motor.MotorSelector; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.utils.MotorCorrelation; - -import org.jfree.chart.ChartColor; -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.axis.ValueAxis; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.title.TextTitle; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - -public class ThrustCurveMotorSelectionPanel extends JPanel implements MotorSelector { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private static final double MOTOR_SIMILARITY_THRESHOLD = 0.95; - - private static final int SHOW_ALL = 0; - private static final int SHOW_SMALLER = 1; - private static final int SHOW_EXACT = 2; - private static final String[] SHOW_DESCRIPTIONS = { - //// Show all motors - trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc1"), - //// Show motors with diameter less than that of the motor mount - trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc2"), - //// Show motors with diameter equal to that of the motor mount - trans.get("TCMotorSelPan.SHOW_DESCRIPTIONS.desc3") - }; - private static final int SHOW_MAX = 2; - - private static final int ZOOM_ICON_POSITION_NEGATIVE_X = 50; - private static final int ZOOM_ICON_POSITION_POSITIVE_Y = 12; - - private static final Paint[] CURVE_COLORS = ChartColor.createDefaultPaintArray(); - - private static final Color NO_COMMENT_COLOR = Color.GRAY; - private static final Color WITH_COMMENT_COLOR = Color.BLACK; - - private static final ThrustCurveMotorComparator MOTOR_COMPARATOR = new ThrustCurveMotorComparator(); - - - - private final List<ThrustCurveMotorSet> database; - - private final double diameter; - private CloseableDialog dialog = null; - - - private final ThrustCurveMotorDatabaseModel model; - private final JTable table; - private final TableRowSorter<TableModel> sorter; - - private final JCheckBox hideSimilarBox; - - private final JTextField searchField; - private String[] searchTerms = new String[0]; - - - private final JLabel curveSelectionLabel; - private final JComboBox curveSelectionBox; - private final DefaultComboBoxModel curveSelectionModel; - - private final JLabel totalImpulseLabel; - private final JLabel avgThrustLabel; - private final JLabel maxThrustLabel; - private final JLabel burnTimeLabel; - private final JLabel launchMassLabel; - private final JLabel emptyMassLabel; - private final JLabel dataPointsLabel; - private final JLabel digestLabel; - - private final JTextArea comment; - private final Font noCommentFont; - private final Font withCommentFont; - - private final JFreeChart chart; - private final ChartPanel chartPanel; - private final JLabel zoomIcon; - - private final JComboBox delayBox; - - private ThrustCurveMotor selectedMotor; - private ThrustCurveMotorSet selectedMotorSet; - private double selectedDelay; - - - /** - * Sole constructor. - * - * @param current the currently selected ThrustCurveMotor, or <code>null</code> for none. - * @param delay the currently selected ejection charge delay. - * @param diameter the diameter of the motor mount. - */ - public ThrustCurveMotorSelectionPanel(ThrustCurveMotor current, double delay, double diameter) { - super(new MigLayout("fill", "[grow][]")); - - this.diameter = diameter; - - - // Construct the database (adding the current motor if not in the db already) - List<ThrustCurveMotorSet> db; - // TODO - ugly blind cast. - db = ((ThrustCurveMotorSetDatabase) Application.getMotorSetDatabase()).getMotorSets(); - - // If current motor is not found in db, add a new ThrustCurveMotorSet containing it - if (current != null) { - selectedMotor = current; - for (ThrustCurveMotorSet motorSet : db) { - if (motorSet.getMotors().contains(current)) { - selectedMotorSet = motorSet; - break; - } - } - if (selectedMotorSet == null) { - db = new ArrayList<ThrustCurveMotorSet>(db); - ThrustCurveMotorSet extra = new ThrustCurveMotorSet(); - extra.addMotor(current); - selectedMotorSet = extra; - db.add(extra); - Collections.sort(db); - } - } - database = db; - - - - //// GUI - - JPanel panel; - JLabel label; - - panel = new JPanel(new MigLayout("fill")); - this.add(panel, "grow"); - - - - // Selection label - //// Select rocket motor: - label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Selrocketmotor"), Style.BOLD); - panel.add(label, "spanx, wrap para"); - - // Diameter selection - JComboBox filterComboBox = new JComboBox(SHOW_DESCRIPTIONS); - filterComboBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JComboBox cb = (JComboBox) e.getSource(); - int sel = cb.getSelectedIndex(); - if ((sel < 0) || (sel > SHOW_MAX)) - sel = SHOW_ALL; - switch (sel) { - case SHOW_ALL: - sorter.setRowFilter(new MotorRowFilterAll()); - break; - - case SHOW_SMALLER: - sorter.setRowFilter(new MotorRowFilterSmaller()); - break; - - case SHOW_EXACT: - sorter.setRowFilter(new MotorRowFilterExact()); - break; - - default: - throw new BugException("Invalid selection mode sel=" + sel); - } - Application.getPreferences().putChoice("MotorDiameterMatch", sel); - scrollSelectionVisible(); - } - }); - panel.add(filterComboBox, "spanx, growx, wrap rel"); - - //// Hide very similar thrust curves - hideSimilarBox = new JCheckBox(trans.get("TCMotorSelPan.checkbox.hideSimilar")); - GUIUtil.changeFontSize(hideSimilarBox, -1); - hideSimilarBox.setSelected(Application.getPreferences().getBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, true)); - hideSimilarBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Application.getPreferences().putBoolean(net.sf.openrocket.startup.Preferences.MOTOR_HIDE_SIMILAR, hideSimilarBox.isSelected()); - updateData(); - } - }); - panel.add(hideSimilarBox, "gapleft para, spanx, growx, wrap para"); - - - // Motor selection table - model = new ThrustCurveMotorDatabaseModel(database); - table = new JTable(model); - - - // Set comparators and widths - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - sorter = new TableRowSorter<TableModel>(model); - for (int i = 0; i < ThrustCurveMotorColumns.values().length; i++) { - ThrustCurveMotorColumns column = ThrustCurveMotorColumns.values()[i]; - sorter.setComparator(i, column.getComparator()); - table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth()); - } - table.setRowSorter(sorter); - - // Set selection and double-click listeners - table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - int row = table.getSelectedRow(); - if (row >= 0) { - row = table.convertRowIndexToModel(row); - ThrustCurveMotorSet motorSet = model.getMotorSet(row); - log.user("Selected table row " + row + ": " + motorSet); - if (motorSet != selectedMotorSet) { - select(selectMotor(motorSet)); - } - } else { - log.user("Selected table row " + row + ", nothing selected"); - } - } - }); - table.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { - if (dialog != null) { - dialog.close(true); - } - } - } - }); - - - JScrollPane scrollpane = new JScrollPane(); - scrollpane.setViewportView(table); - panel.add(scrollpane, "grow, width :500:, height :300:, spanx, wrap para"); - - - - - // Motor mount diameter label - //// Motor mount diameter: - label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Motormountdia")+ " " + - UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter)); - panel.add(label, "gapright 30lp, spanx, split"); - - - - // Search field - //// Search: - label = new StyledLabel(trans.get("TCMotorSelPan.lbl.Search")); - panel.add(label, ""); - - searchField = new JTextField(); - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void changedUpdate(DocumentEvent e) { - update(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - update(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - update(); - } - - private void update() { - String text = searchField.getText().trim(); - String[] split = text.split("\\s+"); - ArrayList<String> list = new ArrayList<String>(); - for (String s : split) { - s = s.trim().toLowerCase(); - if (s.length() > 0) { - list.add(s); - } - } - searchTerms = list.toArray(new String[0]); - sorter.sort(); - scrollSelectionVisible(); - } - }); - panel.add(searchField, "growx, wrap"); - - - - // Vertical split - this.add(panel, "grow"); - this.add(new JSeparator(JSeparator.VERTICAL), "growy, gap para para"); - panel = new JPanel(new MigLayout("fill")); - - - - // Thrust curve selection - //// Select thrust curve: - curveSelectionLabel = new JLabel(trans.get("TCMotorSelPan.lbl.Selectthrustcurve")); - panel.add(curveSelectionLabel); - - curveSelectionModel = new DefaultComboBoxModel(); - curveSelectionBox = new JComboBox(curveSelectionModel); - curveSelectionBox.setRenderer(new CurveSelectionRenderer(curveSelectionBox.getRenderer())); - curveSelectionBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Object value = curveSelectionBox.getSelectedItem(); - if (value != null) { - select(((MotorHolder) value).getMotor()); - } - } - }); - panel.add(curveSelectionBox, "growx, wrap para"); - - - - - - // Ejection charge delay: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Ejectionchargedelay"))); - - delayBox = new JComboBox(); - delayBox.setEditable(true); - delayBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JComboBox cb = (JComboBox) e.getSource(); - String sel = (String) cb.getSelectedItem(); - //// None - if (sel.equalsIgnoreCase(trans.get("TCMotorSelPan.equalsIgnoreCase.None"))) { - selectedDelay = Motor.PLUGGED; - } else { - try { - selectedDelay = Double.parseDouble(sel); - } catch (NumberFormatException ignore) { - } - } - setDelays(false); - } - }); - panel.add(delayBox, "growx, wrap rel"); - //// (Number of seconds or \"None\") - panel.add(new StyledLabel(trans.get("TCMotorSelPan.lbl.NumberofsecondsorNone"), -3), "skip, wrap para"); - setDelays(false); - - - panel.add(new JSeparator(), "spanx, growx, wrap para"); - - - - // Thrust curve info - //// Total impulse: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Totalimpulse"))); - totalImpulseLabel = new JLabel(); - panel.add(totalImpulseLabel, "wrap"); - - //// Avg. thrust: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Avgthrust"))); - avgThrustLabel = new JLabel(); - panel.add(avgThrustLabel, "wrap"); - - //// Max. thrust: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Maxthrust"))); - maxThrustLabel = new JLabel(); - panel.add(maxThrustLabel, "wrap"); - - //// Burn time: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Burntime"))); - burnTimeLabel = new JLabel(); - panel.add(burnTimeLabel, "wrap"); - - //// Launch mass: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Launchmass"))); - launchMassLabel = new JLabel(); - panel.add(launchMassLabel, "wrap"); - - //// Empty mass: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Emptymass"))); - emptyMassLabel = new JLabel(); - panel.add(emptyMassLabel, "wrap"); - - //// Data points: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Datapoints"))); - dataPointsLabel = new JLabel(); - panel.add(dataPointsLabel, "wrap para"); - - if (System.getProperty("openrocket.debug.motordigest") != null) { - //// Digest: - panel.add(new JLabel(trans.get("TCMotorSelPan.lbl.Digest"))); - digestLabel = new JLabel(); - panel.add(digestLabel, "w :300:, wrap para"); - } else { - digestLabel = null; - } - - - comment = new JTextArea(5, 5); - GUIUtil.changeFontSize(comment, -2); - withCommentFont = comment.getFont(); - noCommentFont = withCommentFont.deriveFont(Font.ITALIC); - comment.setLineWrap(true); - comment.setWrapStyleWord(true); - comment.setEditable(false); - scrollpane = new JScrollPane(comment); - panel.add(scrollpane, "spanx, growx, wrap para"); - - - - - // Thrust curve plot - chart = ChartFactory.createXYLineChart( - null, // title - null, // xAxisLabel - null, // yAxisLabel - null, // dataset - PlotOrientation.VERTICAL, - false, // legend - false, // tooltips - false // urls - ); - - - // Add the data and formatting to the plot - XYPlot plot = chart.getXYPlot(); - - changeLabelFont(plot.getRangeAxis(), -2); - changeLabelFont(plot.getDomainAxis(), -2); - - //// Thrust curve: - chart.setTitle(new TextTitle(trans.get("TCMotorSelPan.title.Thrustcurve"), this.getFont())); - chart.setBackgroundPaint(this.getBackground()); - plot.setBackgroundPaint(Color.WHITE); - plot.setDomainGridlinePaint(Color.LIGHT_GRAY); - plot.setRangeGridlinePaint(Color.LIGHT_GRAY); - - chartPanel = new ChartPanel(chart, - false, // properties - false, // save - false, // print - false, // zoom - false); // tooltips - chartPanel.setMouseZoomable(false); - chartPanel.setPopupMenu(null); - chartPanel.setMouseWheelEnabled(false); - chartPanel.setRangeZoomable(false); - chartPanel.setDomainZoomable(false); - - chartPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - chartPanel.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (selectedMotor == null || selectedMotorSet == null) - return; - if (e.getButton() == MouseEvent.BUTTON1) { - // Open plot dialog - List<ThrustCurveMotor> motors = getFilteredCurves(); - ThrustCurveMotorPlotDialog plotDialog = new ThrustCurveMotorPlotDialog(motors, - motors.indexOf(selectedMotor), - SwingUtilities.getWindowAncestor(ThrustCurveMotorSelectionPanel.this)); - plotDialog.setVisible(true); - } - } - }); - - JLayeredPane layer = new CustomLayeredPane(); - - layer.setBorder(BorderFactory.createLineBorder(Color.BLUE)); - - layer.add(chartPanel, (Integer) 0); - - zoomIcon = new JLabel(Icons.ZOOM_IN); - zoomIcon.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); - layer.add(zoomIcon, (Integer) 1); - - - panel.add(layer, "width 300:300:, height 180:180:, grow, spanx"); - - - - this.add(panel, "grow"); - - - - // Sets the filter: - int showMode = Application.getPreferences().getChoice(net.sf.openrocket.startup.Preferences.MOTOR_DIAMETER_FILTER, SHOW_MAX, SHOW_EXACT); - filterComboBox.setSelectedIndex(showMode); - - - // Update the panel data - updateData(); - selectedDelay = delay; - setDelays(false); - - } - - - @Override - public Motor getSelectedMotor() { - return selectedMotor; - } - - - @Override - public double getSelectedDelay() { - return selectedDelay; - } - - - @Override - public JComponent getDefaultFocus() { - return searchField; - } - - @Override - public void selectedMotor(Motor motorSelection) { - if (!(motorSelection instanceof ThrustCurveMotor)) { - log.error("Received argument that was not ThrustCurveMotor: " + motorSelection); - return; - } - - ThrustCurveMotor motor = (ThrustCurveMotor) motorSelection; - ThrustCurveMotorSet set = findMotorSet(motor); - if (set == null) { - log.error("Could not find set for motor:" + motorSelection); - return; - } - - // Store selected motor in preferences node, set all others to false - Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); - for (ThrustCurveMotor m : set.getMotors()) { - String digest = MotorDigest.digestMotor(m); - prefs.putBoolean(digest, m == motor); - } - } - - public void setCloseableDialog(CloseableDialog dialog) { - this.dialog = dialog; - } - - - - private void changeLabelFont(ValueAxis axis, float size) { - Font font = axis.getTickLabelFont(); - font = font.deriveFont(font.getSize2D() + size); - axis.setTickLabelFont(font); - } - - - /** - * Called when a different motor is selected from within the panel. - */ - private void select(ThrustCurveMotor motor) { - if (selectedMotor == motor) - return; - - ThrustCurveMotorSet set = findMotorSet(motor); - if (set == null) { - throw new BugException("Could not find motor from database, motor=" + motor); - } - - boolean updateDelays = (selectedMotorSet != set); - - selectedMotor = motor; - selectedMotorSet = set; - updateData(); - if (updateDelays) { - setDelays(true); - } - } - - - private void updateData() { - - if (selectedMotorSet == null) { - // No motor selected - curveSelectionModel.removeAllElements(); - curveSelectionBox.setEnabled(false); - curveSelectionLabel.setEnabled(false); - totalImpulseLabel.setText(""); - avgThrustLabel.setText(""); - maxThrustLabel.setText(""); - burnTimeLabel.setText(""); - launchMassLabel.setText(""); - emptyMassLabel.setText(""); - dataPointsLabel.setText(""); - if (digestLabel != null) { - digestLabel.setText(""); - } - setComment(""); - chart.getXYPlot().setDataset(new XYSeriesCollection()); - return; - } - - - // Check which thrust curves to display - List<ThrustCurveMotor> motors = getFilteredCurves(); - final int index = motors.indexOf(selectedMotor); - - - // Update the thrust curve selection box - curveSelectionModel.removeAllElements(); - for (int i = 0; i < motors.size(); i++) { - curveSelectionModel.addElement(new MotorHolder(motors.get(i), i)); - } - curveSelectionBox.setSelectedIndex(index); - - if (motors.size() > 1) { - curveSelectionBox.setEnabled(true); - curveSelectionLabel.setEnabled(true); - } else { - curveSelectionBox.setEnabled(false); - curveSelectionLabel.setEnabled(false); - } - - - // Update thrust curve data - totalImpulseLabel.setText(UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit( - selectedMotor.getTotalImpulseEstimate())); - avgThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( - selectedMotor.getAverageThrustEstimate())); - maxThrustLabel.setText(UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit( - selectedMotor.getMaxThrustEstimate())); - burnTimeLabel.setText(UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit( - selectedMotor.getBurnTimeEstimate())); - launchMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( - selectedMotor.getLaunchCG().weight)); - emptyMassLabel.setText(UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit( - selectedMotor.getEmptyCG().weight)); - dataPointsLabel.setText("" + (selectedMotor.getTimePoints().length - 1)); - if (digestLabel != null) { - digestLabel.setText(MotorDigest.digestMotor(selectedMotor)); - } - - setComment(selectedMotor.getDescription()); - - - // Update the plot - XYPlot plot = chart.getXYPlot(); - - XYSeriesCollection dataset = new XYSeriesCollection(); - for (int i = 0; i < motors.size(); i++) { - ThrustCurveMotor m = motors.get(i); - - //// Thrust - XYSeries series = new XYSeries(trans.get("TCMotorSelPan.title.Thrust")); - double[] time = m.getTimePoints(); - double[] thrust = m.getThrustPoints(); - - for (int j = 0; j < time.length; j++) { - series.add(time[j], thrust[j]); - } - - dataset.addSeries(series); - - boolean selected = (i == index); - plot.getRenderer().setSeriesStroke(i, new BasicStroke(selected ? 3 : 1)); - plot.getRenderer().setSeriesPaint(i, getColor(i)); - } - - plot.setDataset(dataset); - } - - - private List<ThrustCurveMotor> getFilteredCurves() { - List<ThrustCurveMotor> motors = selectedMotorSet.getMotors(); - if (hideSimilarBox.isSelected()) { - List<ThrustCurveMotor> filtered = new ArrayList<ThrustCurveMotor>(motors.size()); - for (int i = 0; i < motors.size(); i++) { - ThrustCurveMotor m = motors.get(i); - if (m.equals(selectedMotor)) { - filtered.add(m); - continue; - } - - double similarity = MotorCorrelation.similarity(selectedMotor, m); - log.debug("Motor similarity: " + similarity); - if (similarity < MOTOR_SIMILARITY_THRESHOLD) { - filtered.add(m); - } - } - motors = filtered; - } - - Collections.sort(motors, MOTOR_COMPARATOR); - - return motors; - } - - - private void setComment(String s) { - s = s.trim(); - if (s.length() == 0) { - //// No description available. - comment.setText("No description available."); - comment.setFont(noCommentFont); - comment.setForeground(NO_COMMENT_COLOR); - } else { - comment.setText(s); - comment.setFont(withCommentFont); - comment.setForeground(WITH_COMMENT_COLOR); - } - comment.setCaretPosition(0); - } - - private void scrollSelectionVisible() { - if (selectedMotorSet != null) { - int index = table.convertRowIndexToView(model.getIndex(selectedMotorSet)); - System.out.println("index=" + index); - table.getSelectionModel().setSelectionInterval(index, index); - Rectangle rect = table.getCellRect(index, 0, true); - rect = new Rectangle(rect.x, rect.y - 100, rect.width, rect.height + 200); - table.scrollRectToVisible(rect); - } - } - - - public static Color getColor(int index) { - return (Color) CURVE_COLORS[index % CURVE_COLORS.length]; - } - - - /** - * Find the ThrustCurveMotorSet that contains a motor. - * - * @param motor the motor to look for. - * @return the ThrustCurveMotorSet, or null if not found. - */ - private ThrustCurveMotorSet findMotorSet(ThrustCurveMotor motor) { - for (ThrustCurveMotorSet set : database) { - if (set.getMotors().contains(motor)) { - return set; - } - } - - return null; - } - - - - /** - * Select the default motor from this ThrustCurveMotorSet. This uses primarily motors - * that the user has previously used, and secondarily a heuristic method of selecting which - * thrust curve seems to be better or more reliable. - * - * @param set the motor set - * @return the default motor in this set - */ - private ThrustCurveMotor selectMotor(ThrustCurveMotorSet set) { - if (set.getMotorCount() == 0) { - throw new BugException("Attempting to select motor from empty ThrustCurveMotorSet: " + set); - } - if (set.getMotorCount() == 1) { - return set.getMotors().get(0); - } - - - // Find which motor has been used the most recently - List<ThrustCurveMotor> list = set.getMotors(); - Preferences prefs = ((SwingPreferences) Application.getPreferences()).getNode(net.sf.openrocket.startup.Preferences.PREFERRED_THRUST_CURVE_MOTOR_NODE); - for (ThrustCurveMotor m : list) { - String digest = MotorDigest.digestMotor(m); - if (prefs.getBoolean(digest, false)) { - return m; - } - } - - // No motor has been used - Collections.sort(list, MOTOR_COMPARATOR); - return list.get(0); - } - - - /** - * Set the values in the delay combo box. If <code>reset</code> is <code>true</code> - * then sets the selected value as the value closest to selectedDelay, otherwise - * leaves selection alone. - */ - private void setDelays(boolean reset) { - if (selectedMotor == null) { - - //// None - delayBox.setModel(new DefaultComboBoxModel(new String[] { trans.get("TCMotorSelPan.delayBox.None") })); - delayBox.setSelectedIndex(0); - - } else { - - List<Double> delays = selectedMotorSet.getDelays(); - String[] delayStrings = new String[delays.size()]; - double currentDelay = selectedDelay; // Store current setting locally - - for (int i = 0; i < delays.size(); i++) { - //// None - delayStrings[i] = ThrustCurveMotor.getDelayString(delays.get(i), trans.get("TCMotorSelPan.delayBox.None")); - } - delayBox.setModel(new DefaultComboBoxModel(delayStrings)); - - if (reset) { - - // Find and set the closest value - double closest = Double.NaN; - for (int i = 0; i < delays.size(); i++) { - // if-condition to always become true for NaN - if (!(Math.abs(delays.get(i) - currentDelay) > Math.abs(closest - currentDelay))) { - closest = delays.get(i); - } - } - if (!Double.isNaN(closest)) { - selectedDelay = closest; - //// None - delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(closest, trans.get("TCMotorSelPan.delayBox.None"))); - } else { - delayBox.setSelectedItem("None"); - } - - } else { - - selectedDelay = currentDelay; - //// None - delayBox.setSelectedItem(ThrustCurveMotor.getDelayString(currentDelay, trans.get("TCMotorSelPan.delayBox.None"))); - - } - - } - } - - - - - ////////////////////// - - - private class CurveSelectionRenderer implements ListCellRenderer { - - private final ListCellRenderer renderer; - - public CurveSelectionRenderer(ListCellRenderer renderer) { - this.renderer = renderer; - } - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - - Component c = renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof MotorHolder) { - MotorHolder m = (MotorHolder) value; - c.setForeground(getColor(m.getIndex())); - } - - return c; - } - - } - - - //////// Row filters - - /** - * Abstract adapter class. - */ - private abstract class MotorRowFilter extends RowFilter<TableModel, Integer> { - @Override - public boolean include(RowFilter.Entry<? extends TableModel, ? extends Integer> entry) { - int index = entry.getIdentifier(); - ThrustCurveMotorSet m = model.getMotorSet(index); - return filterByDiameter(m) && filterByString(m); - } - - public abstract boolean filterByDiameter(ThrustCurveMotorSet m); - - - public boolean filterByString(ThrustCurveMotorSet m) { - main: for (String s : searchTerms) { - for (ThrustCurveMotorColumns col : ThrustCurveMotorColumns.values()) { - String str = col.getValue(m).toString().toLowerCase(); - if (str.indexOf(s) >= 0) - continue main; - } - return false; - } - return true; - } - } - - /** - * Show all motors. - */ - private class MotorRowFilterAll extends MotorRowFilter { - @Override - public boolean filterByDiameter(ThrustCurveMotorSet m) { - return true; - } - } - - /** - * Show motors smaller than the mount. - */ - private class MotorRowFilterSmaller extends MotorRowFilter { - @Override - public boolean filterByDiameter(ThrustCurveMotorSet m) { - return (m.getDiameter() <= diameter + 0.0004); - } - } - - /** - * Show motors that fit the mount. - */ - private class MotorRowFilterExact extends MotorRowFilter { - @Override - public boolean filterByDiameter(ThrustCurveMotorSet m) { - return ((m.getDiameter() <= diameter + 0.0004) && (m.getDiameter() >= diameter - 0.0015)); - } - } - - - /** - * Custom layered pane that sets the bounds of the components on every layout. - */ - public class CustomLayeredPane extends JLayeredPane { - @Override - public void doLayout() { - synchronized (getTreeLock()) { - int w = getWidth(); - int h = getHeight(); - chartPanel.setBounds(0, 0, w, h); - zoomIcon.setBounds(w - ZOOM_ICON_POSITION_NEGATIVE_X, ZOOM_ICON_POSITION_POSITIVE_Y, 50, 50); - } - } - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java b/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java deleted file mode 100644 index 6162e965..00000000 --- a/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.sf.openrocket.gui.dialogs.optimization; - -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.unit.Value; - -/** - * Value object for function evaluation information. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FunctionEvaluationData { - - private final Point point; - private final Value[] state; - private final Value domainReference; - private final Value parameterValue; - private final double goalValue; - - - public FunctionEvaluationData(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue) { - this.point = point; - this.state = state.clone(); - this.domainReference = domainReference; - this.parameterValue = parameterValue; - this.goalValue = goalValue; - } - - - /** - * Return the function evaluation point (in 0...1 range). - */ - public Point getPoint() { - return point; - } - - - /** - * Return the function evaluation state in SI units + units. - */ - public Value[] getState() { - return state; - } - - - /** - * Return the domain description. - */ - public Value getDomainReference() { - return domainReference; - } - - - /** - * Return the optimization parameter value (or NaN is outside of domain). - */ - public Value getParameterValue() { - return parameterValue; - } - - - /** - * Return the function goal value. - */ - public double getGoalValue() { - return goalValue; - } -} diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java b/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java deleted file mode 100644 index 033da286..00000000 --- a/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java +++ /dev/null @@ -1,1585 +0,0 @@ -package net.sf.openrocket.gui.dialogs.optimization; - -import java.awt.Component; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.text.Collator; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import javax.swing.BorderFactory; -import javax.swing.DefaultComboBoxModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSpinner; -import javax.swing.JTable; -import javax.swing.JToggleButton; -import javax.swing.ListSelectionModel; -import javax.swing.Timer; -import javax.swing.border.TitledBorder; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.DefaultTableCellRenderer; -import javax.swing.table.TableColumnModel; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.TreePath; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.CsvOptionPanel; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.DoubleCellEditor; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.components.UnitCellEditor; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.scalefigure.RocketFigure; -import net.sf.openrocket.gui.scalefigure.ScaleScrollPane; -import net.sf.openrocket.gui.util.FileHelper; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; -import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.optimization.rocketoptimization.domains.IdentitySimulationDomain; -import net.sf.openrocket.optimization.rocketoptimization.domains.StabilityDomain; -import net.sf.openrocket.optimization.rocketoptimization.goals.MaximizationGoal; -import net.sf.openrocket.optimization.rocketoptimization.goals.MinimizationGoal; -import net.sf.openrocket.optimization.rocketoptimization.goals.ValueSeekGoal; -import net.sf.openrocket.optimization.services.OptimizationServiceHelper; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.CaliberUnit; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.Named; -import net.sf.openrocket.util.TextUtil; - -import com.itextpdf.text.Font; - - -/** - * General rocket optimization dialog. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class GeneralOptimizationDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private static final Collator collator = Collator.getInstance(); - - - private static final String GOAL_MAXIMIZE = trans.get("goal.maximize"); - private static final String GOAL_MINIMIZE = trans.get("goal.minimize"); - private static final String GOAL_SEEK = trans.get("goal.seek"); - - private static final String START_TEXT = trans.get("btn.start"); - private static final String STOP_TEXT = trans.get("btn.stop"); - - - - private final List<OptimizableParameter> optimizationParameters = new ArrayList<OptimizableParameter>(); - private final Map<Object, List<SimulationModifier>> simulationModifiers = - new HashMap<Object, List<SimulationModifier>>(); - - - private final OpenRocketDocument baseDocument; - private OpenRocketDocument documentCopy; - - - private final JButton addButton; - private final JButton removeButton; - private final JButton removeAllButton; - - private final ParameterSelectionTableModel selectedModifierTableModel; - private final JTable selectedModifierTable; - private final DescriptionArea selectedModifierDescription; - private final SimulationModifierTree availableModifierTree; - - private final JComboBox simulationSelectionCombo; - private final JComboBox optimizationParameterCombo; - - private final JComboBox optimizationGoalCombo; - private final JSpinner optimizationGoalSpinner; - private final UnitSelector optimizationGoalUnitSelector; - private final DoubleModel optimizationSeekValue; - - private DoubleModel minimumStability; - private DoubleModel maximumStability; - private final JCheckBox minimumStabilitySelected; - private final JSpinner minimumStabilitySpinner; - private final UnitSelector minimumStabilityUnitSelector; - private final JCheckBox maximumStabilitySelected; - private final JSpinner maximumStabilitySpinner; - private final UnitSelector maximumStabilityUnitSelector; - - private final JLabel bestValueLabel; - private final JLabel stepCountLabel; - private final JLabel evaluationCountLabel; - private final JLabel stepSizeLabel; - - private final RocketFigure figure; - private final JToggleButton startButton; - private final JButton plotButton; - private final JButton saveButton; - - private final JButton applyButton; - private final JButton resetButton; - private final JButton closeButton; - - private final List<SimulationModifier> selectedModifiers = new ArrayList<SimulationModifier>(); - - /** List of components to disable while optimization is running */ - private final List<JComponent> disableComponents = new ArrayList<JComponent>(); - - /** Whether optimization is currently running or not */ - private boolean running = false; - /** The optimization worker that is running */ - private OptimizationWorker worker = null; - - - private double bestValue = Double.NaN; - private Unit bestValueUnit = Unit.NOUNIT2; - private int stepCount = 0; - private int evaluationCount = 0; - private double stepSize = 0; - - private final Map<Point, FunctionEvaluationData> evaluationHistory = new LinkedHashMap<Point, FunctionEvaluationData>(); - private final List<Point> optimizationPath = new LinkedList<Point>(); - - - private boolean updating = false; - - - /** - * Sole constructor. - * - * @param document the document - * @param parent the parent window - */ - public GeneralOptimizationDialog(OpenRocketDocument document, Window parent) { - super(parent, trans.get("title")); - - this.baseDocument = document; - this.documentCopy = document.copy(); - - loadOptimizationParameters(); - loadSimulationModifiers(); - - JPanel sub; - JLabel label; - JScrollPane scroll; - String tip; - - JPanel panel = new JPanel(new MigLayout("fill")); - - - ChangeListener clearHistoryChangeListener = new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - clearHistory(); - } - }; - ActionListener clearHistoryActionListener = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - clearHistory(); - } - }; - - - - //// Selected modifiers table - - selectedModifierTableModel = new ParameterSelectionTableModel(); - selectedModifierTable = new JTable(selectedModifierTableModel); - selectedModifierTable.setDefaultRenderer(Double.class, new DoubleCellRenderer()); - selectedModifierTable.setRowSelectionAllowed(true); - selectedModifierTable.setColumnSelectionAllowed(false); - selectedModifierTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - // Make sure spinner editor fits into the cell height - selectedModifierTable.setRowHeight(new JSpinner().getPreferredSize().height - 4); - - selectedModifierTable.setDefaultEditor(Double.class, new DoubleCellEditor()); - selectedModifierTable.setDefaultEditor(Unit.class, new UnitCellEditor() { - @Override - protected UnitGroup getUnitGroup(Unit value, int row, int column) { - return selectedModifiers.get(row).getUnitGroup(); - } - }); - - disableComponents.add(selectedModifierTable); - - selectedModifierTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - updateComponents(); - } - }); - - // Set column widths - TableColumnModel columnModel = selectedModifierTable.getColumnModel(); - columnModel.getColumn(0).setPreferredWidth(150); - columnModel.getColumn(1).setPreferredWidth(40); - columnModel.getColumn(2).setPreferredWidth(40); - columnModel.getColumn(3).setPreferredWidth(40); - - scroll = new JScrollPane(selectedModifierTable); - - label = new StyledLabel(trans.get("lbl.paramsToOptimize"), Style.BOLD); - disableComponents.add(label); - panel.add(label, "split 3, flowy"); - panel.add(scroll, "wmin 300lp, height 200lp, grow"); - selectedModifierDescription = new DescriptionArea(2, -3); - disableComponents.add(selectedModifierDescription); - panel.add(selectedModifierDescription, "growx"); - - - - //// Add/remove buttons - sub = new JPanel(new MigLayout("fill")); - - addButton = new JButton(Chars.LEFT_ARROW + " " + trans.get("btn.add") + " "); - addButton.setToolTipText(trans.get("btn.add.ttip")); - addButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SimulationModifier mod = getSelectedAvailableModifier(); - if (mod != null) { - addModifier(mod); - clearHistory(); - } else { - log.error("Attempting to add simulation modifier when none is selected"); - } - } - }); - disableComponents.add(addButton); - sub.add(addButton, "wrap para, sg button"); - - removeButton = new JButton(" " + trans.get("btn.remove") + " " + Chars.RIGHT_ARROW); - removeButton.setToolTipText(trans.get("btn.remove.ttip")); - removeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SimulationModifier mod = getSelectedModifier(); - if (mod == null) { - log.error("Attempting to remove simulation modifier when none is selected"); - return; - } - removeModifier(mod); - clearHistory(); - } - }); - disableComponents.add(removeButton); - sub.add(removeButton, "wrap para*2, sg button"); - - removeAllButton = new JButton(trans.get("btn.removeAll")); - removeAllButton.setToolTipText(trans.get("btn.removeAll.ttip")); - removeAllButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Removing all selected modifiers"); - selectedModifiers.clear(); - selectedModifierTableModel.fireTableDataChanged(); - availableModifierTree.repaint(); - clearHistory(); - } - }); - disableComponents.add(removeAllButton); - sub.add(removeAllButton, "wrap para, sg button"); - - panel.add(sub); - - - - //// Available modifier tree - availableModifierTree = new SimulationModifierTree(documentCopy.getRocket(), simulationModifiers, selectedModifiers); - availableModifierTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { - @Override - public void valueChanged(TreeSelectionEvent e) { - updateComponents(); - } - }); - - // Handle double-click - availableModifierTree.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - if (e.getClickCount() == 2) { - SimulationModifier mod = getSelectedAvailableModifier(); - if (mod != null) { - addModifier(mod); - clearHistory(); - } else { - log.user("Double-clicked non-available option"); - } - } - } - }); - - disableComponents.add(availableModifierTree); - scroll = new JScrollPane(availableModifierTree); - label = new StyledLabel(trans.get("lbl.availableParams"), Style.BOLD); - disableComponents.add(label); - panel.add(label, "split 2, flowy"); - panel.add(scroll, "width 300lp, height 200lp, grow, wrap para*2"); - - - - - //// Optimization options sub-panel - - sub = new JPanel(new MigLayout("fill")); - TitledBorder border = BorderFactory.createTitledBorder(trans.get("lbl.optimizationOpts")); - GUIUtil.changeFontStyle(border, Font.BOLD); - sub.setBorder(border); - disableComponents.add(sub); - - - //// Simulation to optimize - - label = new JLabel(trans.get("lbl.optimizeSim")); - tip = trans.get("lbl.optimizeSim.ttip"); - label.setToolTipText(tip); - disableComponents.add(label); - sub.add(label, ""); - - simulationSelectionCombo = new JComboBox(); - simulationSelectionCombo.setToolTipText(tip); - populateSimulations(); - simulationSelectionCombo.addActionListener(clearHistoryActionListener); - disableComponents.add(simulationSelectionCombo); - sub.add(simulationSelectionCombo, "growx, wrap unrel"); - - - - //// Value to optimize - label = new JLabel(trans.get("lbl.optimizeValue")); - tip = trans.get("lbl.optimizeValue.ttip"); - label.setToolTipText(tip); - disableComponents.add(label); - sub.add(label, ""); - - optimizationParameterCombo = new JComboBox(); - optimizationParameterCombo.setToolTipText(tip); - populateParameters(); - optimizationParameterCombo.addActionListener(clearHistoryActionListener); - disableComponents.add(optimizationParameterCombo); - sub.add(optimizationParameterCombo, "growx, wrap unrel"); - - - - //// Optimization goal - label = new JLabel(trans.get("lbl.optimizeGoal")); - tip = trans.get("lbl.optimizeGoal"); - label.setToolTipText(tip); - disableComponents.add(label); - sub.add(label, ""); - - optimizationGoalCombo = new JComboBox(new String[] { GOAL_MAXIMIZE, GOAL_MINIMIZE, GOAL_SEEK }); - optimizationGoalCombo.setToolTipText(tip); - optimizationGoalCombo.setEditable(false); - optimizationGoalCombo.addActionListener(clearHistoryActionListener); - disableComponents.add(optimizationGoalCombo); - sub.add(optimizationGoalCombo, "growx"); - - - //// Optimization custom value - optimizationSeekValue = new DoubleModel(0, UnitGroup.UNITS_NONE); - optimizationSeekValue.addChangeListener(clearHistoryChangeListener); - - optimizationGoalSpinner = new JSpinner(optimizationSeekValue.getSpinnerModel()); - tip = trans.get("lbl.optimizeGoalValue.ttip"); - optimizationGoalSpinner.setToolTipText(tip); - optimizationGoalSpinner.setEditor(new SpinnerEditor(optimizationGoalSpinner)); - disableComponents.add(optimizationGoalSpinner); - sub.add(optimizationGoalSpinner, "width 30lp"); - - optimizationGoalUnitSelector = new UnitSelector(optimizationSeekValue); - optimizationGoalUnitSelector.setToolTipText(tip); - disableComponents.add(optimizationGoalUnitSelector); - sub.add(optimizationGoalUnitSelector, "width 20lp, wrap unrel"); - - - panel.add(sub, "grow"); - - - - //// Required stability sub-panel - - sub = new JPanel(new MigLayout("fill")); - border = BorderFactory.createTitledBorder(trans.get("lbl.requireStability")); - GUIUtil.changeFontStyle(border, Font.BOLD); - sub.setBorder(border); - disableComponents.add(sub); - - - - double ref = CaliberUnit.calculateCaliber(baseDocument.getRocket()); - minimumStability = new DoubleModel(ref, UnitGroup.stabilityUnits(ref)); - maximumStability = new DoubleModel(5 * ref, UnitGroup.stabilityUnits(ref)); - minimumStability.addChangeListener(clearHistoryChangeListener); - maximumStability.addChangeListener(clearHistoryChangeListener); - - - //// Minimum stability - tip = trans.get("lbl.requireMinStability.ttip"); - minimumStabilitySelected = new JCheckBox(trans.get("lbl.requireMinStability")); - minimumStabilitySelected.setSelected(true); - minimumStabilitySelected.setToolTipText(tip); - minimumStabilitySelected.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - updateComponents(); - } - }); - disableComponents.add(minimumStabilitySelected); - sub.add(minimumStabilitySelected); - - minimumStabilitySpinner = new JSpinner(minimumStability.getSpinnerModel()); - minimumStabilitySpinner.setToolTipText(tip); - minimumStabilitySpinner.setEditor(new SpinnerEditor(minimumStabilitySpinner)); - disableComponents.add(minimumStabilitySpinner); - sub.add(minimumStabilitySpinner, "growx"); - - minimumStabilityUnitSelector = new UnitSelector(minimumStability); - minimumStabilityUnitSelector.setToolTipText(tip); - disableComponents.add(minimumStabilityUnitSelector); - sub.add(minimumStabilityUnitSelector, "growx, wrap unrel"); - - - //// Maximum stability - tip = trans.get("lbl.requireMaxStability.ttip"); - maximumStabilitySelected = new JCheckBox(trans.get("lbl.requireMaxStability")); - maximumStabilitySelected.setToolTipText(tip); - maximumStabilitySelected.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - updateComponents(); - } - }); - disableComponents.add(maximumStabilitySelected); - sub.add(maximumStabilitySelected); - - maximumStabilitySpinner = new JSpinner(maximumStability.getSpinnerModel()); - maximumStabilitySpinner.setToolTipText(tip); - maximumStabilitySpinner.setEditor(new SpinnerEditor(maximumStabilitySpinner)); - disableComponents.add(maximumStabilitySpinner); - sub.add(maximumStabilitySpinner, "growx"); - - maximumStabilityUnitSelector = new UnitSelector(maximumStability); - maximumStabilityUnitSelector.setToolTipText(tip); - disableComponents.add(maximumStabilityUnitSelector); - sub.add(maximumStabilityUnitSelector, "growx, wrap para"); - - - - // DescriptionArea desc = new DescriptionArea("Stability requirements are verified during each time step of the simulation.", - // 2, -2, false); - // desc.setViewportBorder(null); - // disableComponents.add(desc); - // sub.add(desc, "span, growx"); - - - panel.add(sub, "span 2, grow, wrap para*2"); - - - - - //// Rocket figure - figure = new RocketFigure(getSelectedSimulation().getConfiguration()); - figure.setBorderPixels(1, 1); - ScaleScrollPane figureScrollPane = new ScaleScrollPane(figure); - figureScrollPane.setFitting(true); - panel.add(figureScrollPane, "span, split, height 200lp, grow"); - - - sub = new JPanel(new MigLayout("fill")); - - - label = new JLabel(trans.get("status.bestValue")); - tip = trans.get("status.bestValue.ttip"); - label.setToolTipText(tip); - sub.add(label, "gapright unrel"); - - bestValueLabel = new JLabel(); - bestValueLabel.setToolTipText(tip); - sub.add(bestValueLabel, "wmin 60lp, wrap rel"); - - - label = new JLabel(trans.get("status.stepCount")); - tip = trans.get("status.stepCount.ttip"); - label.setToolTipText(tip); - sub.add(label, "gapright unrel"); - - stepCountLabel = new JLabel(); - stepCountLabel.setToolTipText(tip); - sub.add(stepCountLabel, "wrap rel"); - - - label = new JLabel(trans.get("status.evalCount")); - tip = trans.get("status.evalCount.ttip"); - label.setToolTipText(tip); - sub.add(label, "gapright unrel"); - - evaluationCountLabel = new JLabel(); - evaluationCountLabel.setToolTipText(tip); - sub.add(evaluationCountLabel, "wrap rel"); - - - label = new JLabel(trans.get("status.stepSize")); - tip = trans.get("status.stepSize.ttip"); - label.setToolTipText(tip); - sub.add(label, "gapright unrel"); - - stepSizeLabel = new JLabel(); - stepSizeLabel.setToolTipText(tip); - sub.add(stepSizeLabel, "wrap para"); - - - //// Start/Stop button - - startButton = new JToggleButton(START_TEXT); - startButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (updating) { - log.debug("Updating, ignoring event"); - return; - } - if (running) { - log.user("Stopping optimization"); - stopOptimization(); - } else { - log.user("Starting optimization"); - startOptimization(); - } - } - }); - sub.add(startButton, "span, growx, wrap para*2"); - - - plotButton = new JButton(trans.get("btn.plotPath")); - plotButton.setToolTipText(trans.get("btn.plotPath.ttip")); - plotButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Plotting optimization path, dimensionality=" + selectedModifiers.size()); - OptimizationPlotDialog dialog = new OptimizationPlotDialog( - Collections.unmodifiableList(optimizationPath), - Collections.unmodifiableMap(evaluationHistory), - Collections.unmodifiableList(selectedModifiers), - getSelectedParameter(), - UnitGroup.stabilityUnits(getSelectedSimulation().getRocket()), - GeneralOptimizationDialog.this); - dialog.setVisible(true); - } - }); - disableComponents.add(plotButton); - sub.add(plotButton, "span, growx, wrap"); - - - saveButton = new JButton(trans.get("btn.save")); - saveButton.setToolTipText(trans.get("btn.save.ttip")); - saveButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("User selected save path"); - savePath(); - } - }); - disableComponents.add(saveButton); - sub.add(saveButton, "span, growx"); - - - - panel.add(sub, "wrap para*2"); - - - - - //// Bottom buttons - - applyButton = new JButton(trans.get("btn.apply")); - applyButton.setToolTipText(trans.get("btn.apply.ttip")); - applyButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Applying optimization changes"); - applyDesign(); - } - }); - disableComponents.add(applyButton); - panel.add(applyButton, "span, split, gapright para, right"); - - resetButton = new JButton(trans.get("btn.reset")); - resetButton.setToolTipText(trans.get("btn.reset.ttip")); - resetButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Resetting optimization design"); - resetDesign(); - } - }); - disableComponents.add(resetButton); - panel.add(resetButton, "gapright para, right"); - - closeButton = new JButton(trans.get("btn.close")); - closeButton.setToolTipText(trans.get("btn.close.ttip")); - closeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Closing optimization dialog"); - stopOptimization(); - GeneralOptimizationDialog.this.dispose(); - } - }); - panel.add(closeButton, "right"); - - - this.add(panel); - clearHistory(); - updateComponents(); - GUIUtil.setDisposableDialogOptions(this, null); - } - - - private void startOptimization() { - if (running) { - log.info("Optimization already running"); - return; - } - - - if (selectedModifiers.isEmpty()) { - JOptionPane.showMessageDialog(this, trans.get("error.selectParams.text"), - trans.get("error.selectParams.title"), JOptionPane.ERROR_MESSAGE); - updating = true; - startButton.setSelected(false); - startButton.setText(START_TEXT); - updating = false; - return; - } - - - running = true; - - // Update the button status - updating = true; - startButton.setSelected(true); - startButton.setText(STOP_TEXT); - updating = false; - - - // Create a copy of the simulation (we're going to modify the original in the current thread) - Simulation simulation = getSelectedSimulation(); - Rocket rocketCopy = simulation.getRocket().copyWithOriginalID(); - simulation = simulation.duplicateSimulation(rocketCopy); - - OptimizableParameter parameter = getSelectedParameter(); - - OptimizationGoal goal; - String value = (String) optimizationGoalCombo.getSelectedItem(); - if (GOAL_MAXIMIZE.equals(value)) { - goal = new MaximizationGoal(); - } else if (GOAL_MINIMIZE.equals(value)) { - goal = new MinimizationGoal(); - } else if (GOAL_SEEK.equals(value)) { - goal = new ValueSeekGoal(optimizationSeekValue.getValue()); - } else { - throw new BugException("optimizationGoalCombo had invalid value: " + value); - } - - SimulationDomain domain; - if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) { - double min, max; - boolean minAbsolute, maxAbsolute; - - /* - * Make minAbsolute/maxAbsolute consistent with each other to produce reasonable - * result in plot tool tips. Yes, this is a bit ugly. - */ - - // Min stability - Unit unit = minimumStability.getCurrentUnit(); - if (unit instanceof CaliberUnit) { - min = unit.toUnit(minimumStability.getValue()); - minAbsolute = false; - } else { - min = minimumStability.getValue(); - minAbsolute = true; - } - - // Max stability - unit = maximumStability.getCurrentUnit(); - if (unit instanceof CaliberUnit) { - max = unit.toUnit(maximumStability.getValue()); - maxAbsolute = false; - } else { - max = maximumStability.getValue(); - maxAbsolute = true; - } - - - if (!minimumStabilitySelected.isSelected()) { - min = Double.NaN; - minAbsolute = maxAbsolute; - } - if (!maximumStabilitySelected.isSelected()) { - max = Double.NaN; - maxAbsolute = minAbsolute; - } - - domain = new StabilityDomain(min, minAbsolute, max, maxAbsolute); - } else { - domain = new IdentitySimulationDomain(); - } - - SimulationModifier[] modifiers = selectedModifiers.toArray(new SimulationModifier[0]); - - // Create and start the background worker - worker = new OptimizationWorker(simulation, parameter, goal, domain, modifiers) { - @Override - protected void done(OptimizationException exception) { - log.info("Optimization finished, exception=" + exception, exception); - - if (exception != null) { - JOptionPane.showMessageDialog(GeneralOptimizationDialog.this, - new Object[] { - trans.get("error.optimizationFailure.text"), - exception.getLocalizedMessage() - }, trans.get("error.optimizationFailure.title"), JOptionPane.ERROR_MESSAGE); - } - - worker = null; - stopOptimization(); - - // Disable the start/stop button for a short while after ending the simulation - // to prevent accidentally starting a new optimization when trying to stop it - startButton.setEnabled(false); - Timer timer = new Timer(750, new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - startButton.setEnabled(true); - } - }); - timer.setRepeats(false); - timer.start(); - updateComponents(); - } - - @Override - protected void functionEvaluated(List<FunctionEvaluationData> data) { - for (FunctionEvaluationData d : data) { - evaluationHistory.put(d.getPoint(), d); - evaluationCount++; - } - updateCounters(); - } - - @Override - protected void optimizationStepTaken(List<OptimizationStepData> data) { - - // Add starting point to the path - if (optimizationPath.isEmpty()) { - optimizationPath.add(data.get(0).getOldPoint()); - } - - // Add other points to the path - for (OptimizationStepData d : data) { - optimizationPath.add(d.getNewPoint()); - } - - // Get function value from the latest point - OptimizationStepData latest = data.get(data.size() - 1); - Point newPoint = latest.getNewPoint(); - - FunctionEvaluationData pointValue = evaluationHistory.get(newPoint); - if (pointValue != null && pointValue.getParameterValue() != null) { - bestValue = pointValue.getParameterValue().getValue(); - } else { - bestValue = Double.NaN; - } - - // Update the simulation - Simulation sim = getSelectedSimulation(); - for (int i = 0; i < newPoint.dim(); i++) { - try { - selectedModifiers.get(i).modify(sim, newPoint.get(i)); - } catch (OptimizationException e) { - throw new BugException("Simulation modifier failed to modify the base simulation " + - "modifier=" + selectedModifiers.get(i), e); - } - } - figure.updateFigure(); - - // Update other counter data - stepCount += data.size(); - stepSize = latest.getStepSize(); - updateCounters(); - } - }; - worker.start(); - - - clearHistory(); - - updateComponents(); - } - - private void stopOptimization() { - if (!running) { - log.info("Optimization not running"); - return; - } - - if (worker != null && worker.isAlive()) { - log.info("Worker still running, interrupting it and setting to null"); - worker.interrupt(); - worker = null; - return; - } - - running = false; - - // Update the button status - updating = true; - startButton.setSelected(false); - startButton.setText(START_TEXT); - updating = false; - - - updateComponents(); - } - - - - - /** - * Reset the current optimization history and values. This does not reset the design. - */ - private void clearHistory() { - evaluationHistory.clear(); - optimizationPath.clear(); - bestValue = Double.NaN; - bestValueUnit = getSelectedParameter().getUnitGroup().getDefaultUnit(); - stepCount = 0; - evaluationCount = 0; - stepSize = 0.5; - updateCounters(); - updateComponents(); - } - - - private void applyDesign() { - // TODO: MEDIUM: Apply also potential changes to simulations - Rocket src = getSelectedSimulation().getRocket().copyWithOriginalID(); - Rocket dest = baseDocument.getRocket(); - try { - baseDocument.startUndo(trans.get("undoText")); - dest.freeze(); - - // Remove all children - while (dest.getChildCount() > 0) { - dest.removeChild(0); - } - - // Move all children to the destination rocket - while (src.getChildCount() > 0) { - RocketComponent c = src.getChild(0); - src.removeChild(0); - dest.addChild(c); - } - - } finally { - dest.thaw(); - baseDocument.stopUndo(); - } - } - - - private void resetDesign() { - clearHistory(); - - documentCopy = baseDocument.copy(); - - loadOptimizationParameters(); - loadSimulationModifiers(); - - // Replace selected modifiers with corresponding new modifiers - List<SimulationModifier> newSelected = new ArrayList<SimulationModifier>(); - for (SimulationModifier original : selectedModifiers) { - List<SimulationModifier> newModifiers = simulationModifiers.get(original.getRelatedObject()); - if (newModifiers != null) { - int index = newModifiers.indexOf(original); - if (index >= 0) { - SimulationModifier updated = newModifiers.get(index); - updated.setMinValue(original.getMinValue()); - updated.setMaxValue(original.getMaxValue()); - newSelected.add(updated); - } - } - } - selectedModifiers.clear(); - selectedModifiers.addAll(newSelected); - selectedModifierTableModel.fireTableDataChanged(); - - // Update the available modifier tree - availableModifierTree.populateTree(documentCopy.getRocket(), simulationModifiers); - availableModifierTree.expandComponents(); - - - // Update selectable simulations - populateSimulations(); - - // Update selectable parameters - populateParameters(); - - } - - - private void populateSimulations() { - String current = null; - Object selection = simulationSelectionCombo.getSelectedItem(); - if (selection != null) { - current = selection.toString(); - } - - - List<Named<Simulation>> simulations = new ArrayList<Named<Simulation>>(); - Rocket rocket = documentCopy.getRocket(); - - for (Simulation s : documentCopy.getSimulations()) { - String id = s.getConfiguration().getMotorConfigurationID(); - String name = createSimulationName(s.getName(), rocket.getMotorConfigurationNameOrDescription(id)); - simulations.add(new Named<Simulation>(s, name)); - } - - for (String id : rocket.getMotorConfigurationIDs()) { - if (id == null) { - continue; - } - Simulation sim = new Simulation(rocket); - sim.getConfiguration().setMotorConfigurationID(id); - String name = createSimulationName(trans.get("basicSimulationName"), rocket.getMotorConfigurationNameOrDescription(id)); - simulations.add(new Named<Simulation>(sim, name)); - } - - - Simulation sim = new Simulation(rocket); - sim.getConfiguration().setMotorConfigurationID(null); - String name = createSimulationName(trans.get("noSimulationName"), rocket.getMotorConfigurationNameOrDescription(null)); - simulations.add(new Named<Simulation>(sim, name)); - - - simulationSelectionCombo.setModel(new DefaultComboBoxModel(simulations.toArray())); - - if (current != null) { - for (int i = 0; i < simulations.size(); i++) { - if (simulations.get(i).toString().equals(current)) { - simulationSelectionCombo.setSelectedIndex(i); - break; - } - } - } - } - - - private void populateParameters() { - String current = null; - Object selection = optimizationParameterCombo.getSelectedItem(); - if (selection != null) { - current = selection.toString(); - } else { - // Default to apogee altitude event if it is not the first one in the list - current = trans.get("MaximumAltitudeParameter.name"); - } - - List<Named<OptimizableParameter>> parameters = new ArrayList<Named<OptimizableParameter>>(); - for (OptimizableParameter p : optimizationParameters) { - parameters.add(new Named<OptimizableParameter>(p, p.getName())); - } - - optimizationParameterCombo.setModel(new DefaultComboBoxModel(parameters.toArray())); - - for (int i = 0; i < parameters.size(); i++) { - if (parameters.get(i).toString().equals(current)) { - optimizationParameterCombo.setSelectedIndex(i); - break; - } - } - } - - private void updateCounters() { - bestValueLabel.setText(bestValueUnit.toStringUnit(bestValue)); - stepCountLabel.setText("" + stepCount); - evaluationCountLabel.setText("" + evaluationCount); - stepSizeLabel.setText(UnitGroup.UNITS_RELATIVE.toStringUnit(stepSize)); - } - - - private void loadOptimizationParameters() { - optimizationParameters.clear(); - optimizationParameters.addAll(OptimizationServiceHelper.getOptimizableParameters(documentCopy)); - - if (optimizationParameters.isEmpty()) { - throw new BugException("No rocket optimization parameters found, distribution built wrong."); - } - - Collections.sort(optimizationParameters, new Comparator<OptimizableParameter>() { - @Override - public int compare(OptimizableParameter o1, OptimizableParameter o2) { - return o1.getName().compareTo(o2.getName()); - } - }); - } - - - private void loadSimulationModifiers() { - simulationModifiers.clear(); - - for (SimulationModifier m : OptimizationServiceHelper.getSimulationModifiers(documentCopy)) { - Object key = m.getRelatedObject(); - List<SimulationModifier> list = simulationModifiers.get(key); - if (list == null) { - list = new ArrayList<SimulationModifier>(); - simulationModifiers.put(key, list); - } - list.add(m); - } - - for (Object key : simulationModifiers.keySet()) { - List<SimulationModifier> list = simulationModifiers.get(key); - Collections.sort(list, new Comparator<SimulationModifier>() { - @Override - public int compare(SimulationModifier o1, SimulationModifier o2) { - return o1.getName().compareTo(o2.getName()); - } - }); - } - - } - - - - private void addModifier(SimulationModifier mod) { - if (!selectedModifiers.contains(mod)) { - log.user(1, "Adding simulation modifier " + mod); - selectedModifiers.add(mod); - Collections.sort(selectedModifiers, new SimulationModifierComparator()); - selectedModifierTableModel.fireTableDataChanged(); - availableModifierTree.repaint(); - } else { - log.user(1, "Attempting to add an already existing simulation modifier " + mod); - } - } - - - private void removeModifier(SimulationModifier mod) { - log.user(1, "Removing simulation modifier " + mod); - selectedModifiers.remove(mod); - selectedModifierTableModel.fireTableDataChanged(); - availableModifierTree.repaint(); - } - - - - /** - * Update the enabled status of all components in the dialog. - */ - private void updateComponents() { - boolean state; - - if (updating) { - log.debug("Ignoring updateComponents"); - return; - } - - log.debug("Running updateComponents()"); - - updating = true; - - - // First enable all components if optimization not running - if (!running) { - log.debug("Initially enabling all components"); - for (JComponent c : disableComponents) { - c.setEnabled(true); - } - } - - - // "Add" button - SimulationModifier mod = getSelectedAvailableModifier(); - state = (mod != null && !selectedModifiers.contains(mod)); - log.debug("addButton enabled: " + state); - addButton.setEnabled(state); - - // "Remove" button - state = (selectedModifierTable.getSelectedRow() >= 0); - log.debug("removeButton enabled: " + state); - removeButton.setEnabled(state); - - // "Remove all" button - state = (!selectedModifiers.isEmpty()); - log.debug("removeAllButton enabled: " + state); - removeAllButton.setEnabled(state); - - - // Optimization goal - String selected = (String) optimizationGoalCombo.getSelectedItem(); - state = GOAL_SEEK.equals(selected); - log.debug("optimizationGoalSpinner & UnitSelector enabled: " + state); - optimizationGoalSpinner.setVisible(state); - optimizationGoalUnitSelector.setVisible(state); - - - // Minimum/maximum stability options - state = minimumStabilitySelected.isSelected(); - log.debug("minimumStabilitySpinner & UnitSelector enabled: " + state); - minimumStabilitySpinner.setEnabled(state); - minimumStabilityUnitSelector.setEnabled(state); - - state = maximumStabilitySelected.isSelected(); - log.debug("maximumStabilitySpimmer & UnitSelector enabled: " + state); - maximumStabilitySpinner.setEnabled(state); - maximumStabilityUnitSelector.setEnabled(state); - - - // Plot button (enabled if path exists and dimensionality is 1 or 2) - state = (!optimizationPath.isEmpty() && (selectedModifiers.size() == 1 || selectedModifiers.size() == 2)); - log.debug("plotButton enabled: " + state + " optimizationPath.isEmpty=" + optimizationPath.isEmpty() + - " selectedModifiers.size=" + selectedModifiers.size()); - plotButton.setEnabled(state); - - // Save button (enabled if path exists) - state = (!evaluationHistory.isEmpty()); - log.debug("saveButton enabled: " + state); - saveButton.setEnabled(state); - - - // Last disable all components if optimization is running - if (running) { - log.debug("Disabling all components because optimization is running"); - for (JComponent c : disableComponents) { - c.setEnabled(false); - } - } - - - // Update description text - mod = getSelectedModifier(); - if (mod != null) { - selectedModifierDescription.setText(mod.getDescription()); - } else { - selectedModifierDescription.setText(""); - } - - - // Update the figure - figure.setConfiguration(getSelectedSimulation().getConfiguration()); - - updating = false; - } - - - private void savePath() { - - if (evaluationHistory.isEmpty()) { - throw new BugException("evaluation history is empty"); - } - - CsvOptionPanel csvOptions = new CsvOptionPanel(GeneralOptimizationDialog.class, - trans.get("export.header"), trans.get("export.header.ttip")); - - - JFileChooser chooser = new JFileChooser(); - chooser.setFileFilter(FileHelper.CSV_FILE_FILTER); - chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); - chooser.setAccessory(csvOptions); - - if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) - return; - - File file = chooser.getSelectedFile(); - if (file == null) - return; - - file = FileHelper.ensureExtension(file, "csv"); - if (!FileHelper.confirmWrite(file, this)) { - return; - } - - String fieldSeparator = csvOptions.getFieldSeparator(); - String commentCharacter = csvOptions.getCommentCharacter(); - boolean includeHeader = csvOptions.getSelectionOption(0); - csvOptions.storePreferences(); - - log.info("Saving optimization path to " + file + ", fieldSeparator=" + fieldSeparator + - ", commentCharacter=" + commentCharacter + ", includeHeader=" + includeHeader); - - try { - Writer writer = new BufferedWriter(new FileWriter(file)); - - // Write header - if (includeHeader) { - FunctionEvaluationData data = evaluationHistory.values().iterator().next(); - - writer.write(commentCharacter); - for (SimulationModifier mod : selectedModifiers) { - writer.write(mod.getRelatedObject().toString() + ": " + mod.getName() + " / " + - mod.getUnitGroup().getDefaultUnit().getUnit()); - writer.write(fieldSeparator); - } - if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) { - writer.write(trans.get("export.stability") + " / " + data.getDomainReference().getUnit().getUnit()); - writer.write(fieldSeparator); - } - writer.write(getSelectedParameter().getName() + " / " + - getSelectedParameter().getUnitGroup().getDefaultUnit().getUnit()); - - writer.write("\n"); - } - - - for (FunctionEvaluationData data : evaluationHistory.values()) { - Value[] state = data.getState(); - - for (int i = 0; i < state.length; i++) { - writer.write(TextUtil.doubleToString(state[i].getUnitValue())); - writer.write(fieldSeparator); - } - - if (minimumStabilitySelected.isSelected() || maximumStabilitySelected.isSelected()) { - writer.write(TextUtil.doubleToString(data.getDomainReference().getUnitValue())); - writer.write(fieldSeparator); - } - - if (data.getParameterValue() != null) { - writer.write(TextUtil.doubleToString(data.getParameterValue().getUnitValue())); - } else { - writer.write("N/A"); - } - writer.write("\n"); - } - - writer.close(); - log.info("File successfully saved"); - - } catch (IOException e) { - FileHelper.errorWriting(e, this); - } - - } - - /** - * Return the currently selected available simulation modifier from the modifier tree, - * or <code>null</code> if none selected. - */ - private SimulationModifier getSelectedAvailableModifier() { - TreePath treepath = availableModifierTree.getSelectionPath(); - if (treepath != null) { - Object o = ((DefaultMutableTreeNode) treepath.getLastPathComponent()).getUserObject(); - if (o instanceof SimulationModifier) { - return (SimulationModifier) o; - } - } - return null; - } - - /** - * Return the currently selected simulation. - * @return the selected simulation. - */ - @SuppressWarnings("unchecked") - private Simulation getSelectedSimulation() { - return ((Named<Simulation>) simulationSelectionCombo.getSelectedItem()).get(); - } - - - /** - * Return the currently selected simulation modifier from the table, - * or <code>null</code> if none selected. - * @return the selected modifier or <code>null</code>. - */ - private SimulationModifier getSelectedModifier() { - int row = selectedModifierTable.getSelectedRow(); - if (row < 0) { - return null; - } - row = selectedModifierTable.convertRowIndexToModel(row); - return selectedModifiers.get(row); - } - - - /** - * Return the currently selected optimization parameter. - * @return the selected optimization parameter. - */ - @SuppressWarnings("unchecked") - private OptimizableParameter getSelectedParameter() { - return ((Named<OptimizableParameter>) optimizationParameterCombo.getSelectedItem()).get(); - } - - - private Unit getModifierUnit(int index) { - return selectedModifiers.get(index).getUnitGroup().getDefaultUnit(); - } - - private String createSimulationName(String simulationName, String motorConfiguration) { - String name; - boolean hasParenthesis = motorConfiguration.matches("^[\\[\\(].*[\\]\\)]$"); - name = simulationName + " "; - if (!hasParenthesis) { - name += "("; - } - name += motorConfiguration; - if (!hasParenthesis) { - name += ")"; - } - return name; - } - - /** - * The table model for the parameter selection. - * - * [Body tube: Length] [min] [max] [unit] - */ - private class ParameterSelectionTableModel extends AbstractTableModel { - - private static final int PARAMETER = 0; - private static final int CURRENT = 1; - private static final int MIN = 2; - private static final int MAX = 3; - private static final int COUNT = 4; - - @Override - public int getColumnCount() { - return COUNT; - } - - @Override - public int getRowCount() { - return selectedModifiers.size(); - } - - @Override - public String getColumnName(int column) { - switch (column) { - case PARAMETER: - return trans.get("table.col.parameter"); - case CURRENT: - return trans.get("table.col.current"); - case MIN: - return trans.get("table.col.min"); - case MAX: - return trans.get("table.col.max"); - default: - throw new IndexOutOfBoundsException("column=" + column); - } - - } - - @Override - public Class<?> getColumnClass(int column) { - switch (column) { - case PARAMETER: - return String.class; - case CURRENT: - return Double.class; - case MIN: - return Double.class; - case MAX: - return Double.class; - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - @Override - public Object getValueAt(int row, int column) { - - SimulationModifier modifier = selectedModifiers.get(row); - - switch (column) { - case PARAMETER: - return modifier.getRelatedObject().toString() + ": " + modifier.getName(); - case CURRENT: - try { - return getModifierUnit(row).toUnit(modifier.getCurrentSIValue(getSelectedSimulation())); - } catch (OptimizationException e) { - throw new BugException("Could not read current SI value from modifier " + modifier, e); - } - case MIN: - return getModifierUnit(row).toUnit(modifier.getMinValue()); - case MAX: - return getModifierUnit(row).toUnit(modifier.getMaxValue()); - default: - throw new IndexOutOfBoundsException("column=" + column); - } - - } - - @Override - public void setValueAt(Object value, int row, int column) { - - if (row >= selectedModifiers.size()) { - throw new BugException("setValueAt with invalid row: value=" + value + " row=" + row + " column=" + column + - " selectedModifiers.size=" + selectedModifiers.size() + " selectedModifiers=" + selectedModifiers + - " selectedModifierTable.getRowCount=" + selectedModifierTable.getRowCount()); - } - - switch (column) { - case PARAMETER: - break; - - case MIN: - double min = (Double) value; - min = getModifierUnit(row).fromUnit(min); - selectedModifiers.get(row).setMinValue(min); - break; - - case CURRENT: - break; - - case MAX: - double max = (Double) value; - max = getModifierUnit(row).fromUnit(max); - selectedModifiers.get(row).setMaxValue(max); - break; - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - this.fireTableRowsUpdated(row, row); - - } - - @Override - public boolean isCellEditable(int row, int column) { - switch (column) { - case PARAMETER: - return false; - case CURRENT: - return false; - case MIN: - return true; - case MAX: - return true; - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - } - - - private class DoubleCellRenderer extends DefaultTableCellRenderer { - @Override - public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, - boolean hasFocus, int row, int column) { - - super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); - - double val = (Double) value; - Unit unit = getModifierUnit(row); - - val = unit.fromUnit(val); - this.setText(unit.toStringUnit(val)); - - return this; - } - } - - - private static class SimulationModifierComparator implements Comparator<SimulationModifier> { - - @Override - public int compare(SimulationModifier mod1, SimulationModifier mod2) { - Object rel1 = mod1.getRelatedObject(); - Object rel2 = mod2.getRelatedObject(); - - /* - * Primarily order by related object: - * - * - RocketComponents first - * - Two RocketComponents are ordered based on their position in the rocket - */ - if (!rel1.equals(rel2)) { - - if (rel1 instanceof RocketComponent) { - if (rel2 instanceof RocketComponent) { - - RocketComponent root = ((RocketComponent) rel1).getRoot(); - for (RocketComponent c : root) { - if (c.equals(rel1)) { - return -1; - } - if (c.equals(rel2)) { - return 1; - } - } - - throw new BugException("Error sorting modifiers, mod1=" + mod1 + " rel1=" + rel1 + - " mod2=" + mod2 + " rel2=" + rel2); - - } else { - return -1; - } - } else { - if (rel2 instanceof RocketComponent) { - return 1; - } - } - - } - - // Secondarily sort by name - return collator.compare(mod1.getName(), mod2.getName()); - } - } - - - -} diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java deleted file mode 100644 index 1df3214e..00000000 --- a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java +++ /dev/null @@ -1,435 +0,0 @@ -package net.sf.openrocket.gui.dialogs.optimization; - -import java.awt.Color; -import java.awt.Paint; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.LinearInterpolator; -import net.sf.openrocket.util.MathUtil; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.annotations.XYBoxAnnotation; -import org.jfree.chart.annotations.XYLineAnnotation; -import org.jfree.chart.annotations.XYPointerAnnotation; -import org.jfree.chart.axis.AxisLocation; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.labels.CustomXYToolTipGenerator; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.PaintScale; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; -import org.jfree.chart.renderer.xy.XYShapeRenderer; -import org.jfree.chart.title.PaintScaleLegend; -import org.jfree.data.xy.DefaultXYZDataset; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; -import org.jfree.ui.RectangleEdge; -import org.jfree.ui.TextAnchor; - -/** - * A class that plots the path of an optimization. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OptimizationPlotDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - private static final LinearInterpolator RED = new LinearInterpolator( - new double[] { 0.0, 1.0 }, new double[] { 0.0, 1.0 } - ); - private static final LinearInterpolator GREEN = new LinearInterpolator( - new double[] { 0.0, 1.0 }, new double[] { 0.0, 0.0 } - ); - private static final LinearInterpolator BLUE = new LinearInterpolator( - new double[] { 0.0, 1.0 }, new double[] { 1.0, 0.0 } - ); - - private static final Color OUT_OF_DOMAIN_COLOR = Color.BLACK; - - private static final Color PATH_COLOR = new Color(220, 0, 0); - - - public OptimizationPlotDialog(List<Point> path, Map<Point, FunctionEvaluationData> evaluations, - List<SimulationModifier> modifiers, OptimizableParameter parameter, UnitGroup stabilityUnit, Window parent) { - super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); - - - JPanel panel = new JPanel(new MigLayout("fill")); - - ChartPanel chart; - if (modifiers.size() == 1) { - chart = create1DPlot(path, evaluations, modifiers, parameter, stabilityUnit); - } else if (modifiers.size() == 2) { - chart = create2DPlot(path, evaluations, modifiers, parameter, stabilityUnit); - } else { - throw new IllegalArgumentException("Invalid dimensionality, dim=" + modifiers.size()); - } - chart.setBorder(BorderFactory.createLineBorder(Color.BLACK)); - panel.add(chart, "span, grow, wrap para"); - - - JLabel label = new StyledLabel(trans.get("lbl.zoomInstructions"), -2); - panel.add(label, ""); - - - JButton close = new JButton(trans.get("button.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - OptimizationPlotDialog.this.setVisible(false); - } - }); - panel.add(close, "right"); - - - this.add(panel); - - GUIUtil.setDisposableDialogOptions(this, close); - GUIUtil.rememberWindowSize(this); - } - - - - /** - * Create a 1D plot of the optimization path. - */ - private ChartPanel create1DPlot(List<Point> path, Map<Point, FunctionEvaluationData> evaluations, - List<SimulationModifier> modifiers, OptimizableParameter parameter, UnitGroup stabilityUnit) { - - SimulationModifier modX = modifiers.get(0); - Unit xUnit = modX.getUnitGroup().getDefaultUnit(); - Unit yUnit = parameter.getUnitGroup().getDefaultUnit(); - - // Create the optimization path (with autosort) - XYSeries series = new XYSeries(trans.get("plot1d.series"), true, true); - List<String> tooltips = new ArrayList<String>(); - for (Point p : evaluations.keySet()) { - FunctionEvaluationData data = evaluations.get(p); - if (data != null) { - if (data.getParameterValue() != null) { - Value[] state = data.getState(); - series.add(xUnit.toUnit(state[0].getValue()), yUnit.toUnit(data.getParameterValue().getValue())); - tooltips.add(getTooltip(data, parameter)); - } - } else { - log.error("Could not find evaluation data for point " + p); - } - } - - - String xLabel = modX.getRelatedObject().toString() + ": " + modX.getName() + " / " + xUnit.getUnit(); - String yLabel = parameter.getName() + " / " + yUnit.getUnit(); - - JFreeChart chart = ChartFactory.createXYLineChart( - trans.get("plot1d.title"), - xLabel, - yLabel, - null, - PlotOrientation.VERTICAL, - false, // Legend - true, // Tooltips - false); // Urls - - - // Set the scale of the plot to the limits - double x1 = xUnit.toUnit(modX.getMinValue()); - double x2 = xUnit.toUnit(modX.getMaxValue()); - - if (x1 < x2 - 0.0001) { - log.debug("Setting 1D plot domain axis x1=" + x1 + " x2=" + x2); - chart.getXYPlot().getDomainAxis().setRange(x1, x2); - } else { - log.warn("1D plot domain singular x1=" + x1 + " x2=" + x2 + ", not setting"); - } - - // Add lines to show optimization limits - XYLineAnnotation line = new XYLineAnnotation(x1, -1e19, x1, 1e19); - chart.getXYPlot().addAnnotation(line); - line = new XYLineAnnotation(x2, -1e19, x2, 1e19); - chart.getXYPlot().addAnnotation(line); - - // Mark the optimum point - Point optimum = path.get(path.size() - 1); - FunctionEvaluationData data = evaluations.get(optimum); - if (data != null) { - if (data.getParameterValue() != null) { - Value[] state = data.getState(); - double x = xUnit.toUnit(state[0].getValue()); - double y = yUnit.toUnit(data.getParameterValue().getValue()); - - XYPointerAnnotation text = new XYPointerAnnotation(trans.get("plot.label.optimum"), - x, y, Math.PI / 2); - text.setTextAnchor(TextAnchor.TOP_LEFT); - chart.getXYPlot().addAnnotation(text); - } - } else { - log.error("Could not find evaluation data for point " + optimum); - } - - - XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, true); - lineRenderer.setBaseShapesVisible(true); - lineRenderer.setSeriesShapesFilled(0, false); - //lineRenderer.setSeriesShape(0, shapeRenderer.getBaseShape()); - lineRenderer.setSeriesOutlinePaint(0, PATH_COLOR); - lineRenderer.setSeriesPaint(0, PATH_COLOR); - lineRenderer.setUseOutlinePaint(true); - CustomXYToolTipGenerator tooltipGenerator = new CustomXYToolTipGenerator(); - tooltipGenerator.addToolTipSeries(tooltips); - lineRenderer.setBaseToolTipGenerator(tooltipGenerator); - - XYPlot plot = chart.getXYPlot(); - - plot.setDataset(0, new XYSeriesCollection(series)); - plot.setRenderer(lineRenderer); - - - - return new ChartPanel(chart); - } - - /** - * Create a 2D plot of the optimization path. - */ - private ChartPanel create2DPlot(List<Point> path, Map<Point, FunctionEvaluationData> evaluations, - List<SimulationModifier> modifiers, OptimizableParameter parameter, UnitGroup stabilityUnit) { - - Unit parameterUnit = parameter.getUnitGroup().getDefaultUnit(); - - SimulationModifier modX = modifiers.get(0); - SimulationModifier modY = modifiers.get(1); - - Unit xUnit = modX.getUnitGroup().getDefaultUnit(); - Unit yUnit = modY.getUnitGroup().getDefaultUnit(); - - // Create the optimization path dataset - XYSeries pathSeries = new XYSeries(trans.get("plot2d.path"), false, true); - List<String> pathTooltips = new ArrayList<String>(); - for (Point p : path) { - FunctionEvaluationData data = evaluations.get(p); - if (data != null) { - Value[] state = data.getState(); - pathSeries.add(xUnit.toUnit(state[0].getValue()), yUnit.toUnit(state[1].getValue())); - pathTooltips.add(getTooltip(data, parameter)); - } else { - log.error("Could not find evaluation data for point " + p); - } - } - - - // Create evaluations dataset - double min = Double.POSITIVE_INFINITY; - double max = Double.NEGATIVE_INFINITY; - double[][] evals = new double[3][evaluations.size()]; - List<String> evalTooltips = new ArrayList<String>(); - - Iterator<FunctionEvaluationData> iterator = evaluations.values().iterator(); - for (int i = 0; i < evaluations.size(); i++) { - FunctionEvaluationData data = iterator.next(); - Value param = data.getParameterValue(); - double value; - if (param != null) { - value = parameterUnit.toUnit(data.getParameterValue().getValue()); - } else { - value = Double.NaN; - } - - Value[] state = data.getState(); - evals[0][i] = xUnit.toUnit(state[0].getValue()); - evals[1][i] = yUnit.toUnit(state[1].getValue()); - evals[2][i] = value; - - if (value < min) { - min = value; - } - if (value > max) { - max = value; - } - - evalTooltips.add(getTooltip(data, parameter)); - } - DefaultXYZDataset evalDataset = new DefaultXYZDataset(); - evalDataset.addSeries(trans.get("plot2d.evals"), evals); - - - - String xLabel = modX.getRelatedObject().toString() + ": " + modX.getName() + " / " + xUnit.getUnit(); - String yLabel = modY.getRelatedObject().toString() + ": " + modY.getName() + " / " + yUnit.getUnit(); - - JFreeChart chart = ChartFactory.createXYLineChart( - trans.get("plot2d.title"), - xLabel, - yLabel, - null, - //evalDataset, - PlotOrientation.VERTICAL, - true, // Legend - true, // Tooltips - false); // Urls - - - // Set the scale of the plot to the limits - double x1 = xUnit.toUnit(modX.getMinValue()); - double x2 = xUnit.toUnit(modX.getMaxValue()); - double y1 = yUnit.toUnit(modY.getMinValue()); - double y2 = yUnit.toUnit(modY.getMaxValue()); - - if (x1 < x2 - 0.0001) { - log.debug("Setting 2D plot domain axis to x1=" + x1 + " x2=" + x2); - chart.getXYPlot().getDomainAxis().setRange(x1, x2); - } else { - log.warn("2D plot has singular domain axis: x1=" + x1 + " x2=" + x2); - } - - if (y1 < y2 - 0.0001) { - log.debug("Setting 2D plot range axis to y1=" + y1 + " y2=" + y2); - chart.getXYPlot().getRangeAxis().setRange(y1, y2); - } else { - log.warn("2D plot has singular range axis: y1=" + y1 + " y2=" + y2); - } - - XYBoxAnnotation box = new XYBoxAnnotation(x1, y1, x2, y2); - chart.getXYPlot().addAnnotation(box); - - int n = pathSeries.getItemCount(); - XYPointerAnnotation text = new XYPointerAnnotation(trans.get("plot.label.optimum"), - (Double) pathSeries.getX(n - 1), (Double) pathSeries.getY(n - 1), -Math.PI / 5); - text.setTextAnchor(TextAnchor.BASELINE_LEFT); - chart.getXYPlot().addAnnotation(text); - - - if (min < max - 0.0001) { - log.debug("Setting gradient scale range to min=" + min + " max=" + max); - } else { - log.warn("2D plot has singular gradient scale, resetting to (0,1): min=" + min + " max=" + max); - min = 0; - max = 1; - } - - PaintScale paintScale = new GradientScale(min, max); - - XYShapeRenderer shapeRenderer = new XYShapeRenderer(); - shapeRenderer.setPaintScale(paintScale); - shapeRenderer.setUseFillPaint(true); - CustomXYToolTipGenerator tooltipGenerator = new CustomXYToolTipGenerator(); - tooltipGenerator.addToolTipSeries(evalTooltips); - shapeRenderer.setBaseToolTipGenerator(tooltipGenerator); - - - shapeRenderer.getLegendItem(0, 0); - - - XYLineAndShapeRenderer lineRenderer = new XYLineAndShapeRenderer(true, true); - lineRenderer.setBaseShapesVisible(true); - lineRenderer.setSeriesShapesFilled(0, false); - lineRenderer.setSeriesShape(0, shapeRenderer.getBaseShape()); - lineRenderer.setSeriesOutlinePaint(0, PATH_COLOR); - lineRenderer.setSeriesPaint(0, PATH_COLOR); - lineRenderer.setUseOutlinePaint(true); - tooltipGenerator = new CustomXYToolTipGenerator(); - tooltipGenerator.addToolTipSeries(pathTooltips); - lineRenderer.setBaseToolTipGenerator(tooltipGenerator); - - - XYPlot plot = chart.getXYPlot(); - - plot.setDataset(0, new XYSeriesCollection(pathSeries)); - plot.setRenderer(lineRenderer); - - plot.setDataset(1, evalDataset); - plot.setRenderer(1, shapeRenderer); - - - // Add value scale - NumberAxis numberAxis = new NumberAxis(parameter.getName() + " / " + parameterUnit.getUnit()); - PaintScaleLegend scale = new PaintScaleLegend(paintScale, numberAxis); - scale.setPosition(RectangleEdge.RIGHT); - scale.setMargin(4.0D, 4.0D, 40.0D, 4.0D); - scale.setAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); - chart.addSubtitle(scale); - - - return new ChartPanel(chart); - } - - - - private String getTooltip(FunctionEvaluationData data, OptimizableParameter parameter) { - String ttip = "<html>"; - if (data.getParameterValue() != null) { - ttip += parameter.getName() + ": " + - parameter.getUnitGroup().getDefaultUnit().toStringUnit(data.getParameterValue().getValue()); - ttip += "<br>"; - } - if (data.getDomainReference() != null) { - ttip += trans.get("plot.ttip.stability") + " " + data.getDomainReference(); - } - return ttip; - } - - private class GradientScale implements PaintScale { - - private final double min; - private final double max; - - public GradientScale(double min, double max) { - this.min = min; - this.max = max; - } - - @Override - public Paint getPaint(double value) { - if (Double.isNaN(value)) { - return OUT_OF_DOMAIN_COLOR; - } - - value = MathUtil.map(value, min, max, 0.0, 1.0); - value = MathUtil.clamp(value, 0.0, 1.0); - - float r = (float) RED.getValue(value); - float g = (float) GREEN.getValue(value); - float b = (float) BLUE.getValue(value); - - return new Color(r, g, b); - } - - @Override - public double getLowerBound() { - return min; - } - - @Override - public double getUpperBound() { - return max; - } - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java deleted file mode 100644 index ec1ece5a..00000000 --- a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.sf.openrocket.gui.dialogs.optimization; - -import net.sf.openrocket.optimization.general.Point; - -/** - * Value object for optimization step data. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OptimizationStepData { - - private final Point oldPoint; - private final double oldValue; - private final Point newPoint; - private final double newValue; - private final double stepSize; - - - public OptimizationStepData(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - this.oldPoint = oldPoint; - this.oldValue = oldValue; - this.newPoint = newPoint; - this.newValue = newValue; - this.stepSize = stepSize; - } - - - public Point getOldPoint() { - return oldPoint; - } - - - public double getOldValue() { - return oldValue; - } - - - public Point getNewPoint() { - return newPoint; - } - - - public double getNewValue() { - return newValue; - } - - - public double getStepSize() { - return stepSize; - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java deleted file mode 100644 index eda1f9ae..00000000 --- a/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java +++ /dev/null @@ -1,234 +0,0 @@ -package net.sf.openrocket.gui.dialogs.optimization; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.LinkedBlockingQueue; - -import javax.swing.SwingUtilities; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.FunctionOptimizer; -import net.sf.openrocket.optimization.general.OptimizationController; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.ParallelExecutorCache; -import net.sf.openrocket.optimization.general.ParallelFunctionCache; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer; -import net.sf.openrocket.optimization.general.onedim.GoldenSectionSearchOptimizer; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; -import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationFunction; -import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationListener; -import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.BugException; - -/** - * A background worker that runs the optimization in the background. It supports providing - * evaluation and step counter information via listeners that are executed on the EDT. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class OptimizationWorker extends Thread implements OptimizationController, RocketOptimizationListener { - - /* - * Note: This is implemented as a separate Thread object instead of a SwingWorker because - * the SwingWorker cannot be interrupted in any way except by canceling the task, which - * makes it impossible to wait for its exiting (SwingWorker.get() throws a CancellationException - * if cancel() has been called). - * - * SwingWorker also seems to miss some chunks that have been provided to process() when the - * thread ends. - * - * Nothing of this is documented, of course... - */ - - private static final LogHelper log = Application.getLogger(); - - /** Notify listeners every this many milliseconds */ - private static final long PURGE_TIMEOUT = 500; - /** End optimization when step size is below this threshold */ - private static final double STEP_SIZE_LIMIT = 0.005; - - private final FunctionOptimizer optimizer; - private final RocketOptimizationFunction function; - - private final Simulation simulation; - private final SimulationModifier[] modifiers; - - private final ParallelFunctionCache cache; - - - private final LinkedBlockingQueue<FunctionEvaluationData> evaluationQueue = - new LinkedBlockingQueue<FunctionEvaluationData>(); - private final LinkedBlockingQueue<OptimizationStepData> stepQueue = - new LinkedBlockingQueue<OptimizationStepData>(); - private volatile long lastPurge = 0; - - private OptimizationException optimizationException = null; - - - /** - * Sole constructor - * @param simulation the simulation - * @param parameter the optimization parameter - * @param goal the optimization goal - * @param domain the optimization domain - * @param modifiers the simulation modifiers - */ - public OptimizationWorker(Simulation simulation, OptimizableParameter parameter, - OptimizationGoal goal, SimulationDomain domain, SimulationModifier... modifiers) { - - this.simulation = simulation; - this.modifiers = modifiers.clone(); - - function = new RocketOptimizationFunction(simulation, parameter, goal, domain, modifiers); - function.addRocketOptimizationListener(this); - - cache = new ParallelExecutorCache(1); - cache.setFunction(function); - - if (modifiers.length == 1) { - optimizer = new GoldenSectionSearchOptimizer(cache); - } else { - optimizer = new MultidirectionalSearchOptimizer(cache); - } - } - - - @Override - public void run() { - try { - - double[] current = new double[modifiers.length]; - for (int i = 0; i < modifiers.length; i++) { - current[i] = modifiers[i].getCurrentScaledValue(simulation); - } - Point initial = new Point(current); - - optimizer.optimize(initial, this); - - } catch (OptimizationException e) { - this.optimizationException = e; - } finally { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - lastPurge = System.currentTimeMillis() + 24L * 3600L * 1000L; - processQueue(); - done(optimizationException); - } - }); - } - } - - /** - * This method is called after the optimization has ended, either normally, when interrupted - * or by throwing an exception. This method is called on the EDT, like the done() method of SwingWorker. - * <p> - * All data chunks to the listeners will be guaranteed to have been processed before calling done(). - * - * @param exception a possible optimization exception that occurred, or <code>null</code> for normal exit. - */ - protected abstract void done(OptimizationException exception); - - - /** - * This method is called for each function evaluation that has taken place. - * This method is called on the EDT. - * - * @param data the data accumulated since the last call - */ - protected abstract void functionEvaluated(List<FunctionEvaluationData> data); - - /** - * This method is called after each step taken by the optimization algorithm. - * This method is called on the EDT. - * - * @param data the data accumulated since the last call - */ - protected abstract void optimizationStepTaken(List<OptimizationStepData> data); - - - /** - * Publishes data to the listeners. The queue is purged every PURGE_TIMEOUT milliseconds. - * - * @param data the data to publish to the listeners - */ - private synchronized void publish(FunctionEvaluationData evaluation, OptimizationStepData step) { - - if (evaluation != null) { - evaluationQueue.add(evaluation); - } - if (step != null) { - stepQueue.add(step); - } - - // Add a method to the EDT to process the queue data - long now = System.currentTimeMillis(); - if (lastPurge + PURGE_TIMEOUT <= now) { - lastPurge = now; - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - processQueue(); - } - }); - } - - } - - - /** - * Process the queue and call the listeners. This method must always be called from the EDT. - */ - private void processQueue() { - - if (!SwingUtilities.isEventDispatchThread()) { - throw new BugException("processQueue called from non-EDT"); - } - - - List<FunctionEvaluationData> evaluations = new ArrayList<FunctionEvaluationData>(); - evaluationQueue.drainTo(evaluations); - if (!evaluations.isEmpty()) { - functionEvaluated(evaluations); - } - - - List<OptimizationStepData> steps = new ArrayList<OptimizationStepData>(); - stepQueue.drainTo(steps); - if (!steps.isEmpty()) { - optimizationStepTaken(steps); - } - } - - - - - /* - * NOTE: The stepTaken and evaluated methods may be called from other - * threads than the EDT or the SwingWorker thread! - */ - - @Override - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - publish(null, new OptimizationStepData(oldPoint, oldValue, newPoint, newValue, stepSize)); - - if (stepSize < STEP_SIZE_LIMIT) { - log.info("stepSize=" + stepSize + " is below limit, ending optimization"); - return false; - } else { - return true; - } - } - - @Override - public void evaluated(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue) { - publish(new FunctionEvaluationData(point, state, domainReference, parameterValue, goalValue), null); - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java b/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java deleted file mode 100644 index 1e7e47f0..00000000 --- a/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java +++ /dev/null @@ -1,193 +0,0 @@ -package net.sf.openrocket.gui.dialogs.optimization; - -import java.awt.Color; -import java.awt.Component; -import java.awt.Font; -import java.util.Enumeration; -import java.util.List; -import java.util.Map; - -import javax.swing.JTree; -import javax.swing.ToolTipManager; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreePath; - -import net.sf.openrocket.gui.components.BasicTree; -import net.sf.openrocket.gui.main.ComponentIcons; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.TextUtil; - -/** - * A tree that displays the simulation modifiers in a tree structure. - * <p> - * All nodes in the model are instances of DefaultMutableTreeNode. The user objects - * within are either of type RocketComponent, String or SimulationModifier. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationModifierTree extends BasicTree { - - private final List<SimulationModifier> selectedModifiers; - - /** - * Sole constructor. - * - * @param rocket the rocket. - * @param simulationModifiers the simulation modifiers, ordered and mapped by components - * @param selectedModifiers a list of the currently selected modifiers (may be modified). - */ - public SimulationModifierTree(Rocket rocket, Map<Object, List<SimulationModifier>> simulationModifiers, - List<SimulationModifier> selectedModifiers) { - this.selectedModifiers = selectedModifiers; - - populateTree(rocket, simulationModifiers); - this.setCellRenderer(new ComponentModifierTreeRenderer()); - - // Enable tooltips for this component - ToolTipManager.sharedInstance().registerComponent(this); - - expandComponents(); - } - - - /** - * Populate the simulation modifier tree from the provided information. This can be used to update - * the tree. - */ - public void populateTree(Rocket rocket, Map<Object, List<SimulationModifier>> simulationModifiers) { - - DefaultMutableTreeNode baseNode = new DefaultMutableTreeNode(rocket); - populateTree(baseNode, rocket, simulationModifiers); - - this.setModel(new DefaultTreeModel(baseNode)); - } - - - private static void populateTree(DefaultMutableTreeNode node, RocketComponent component, - Map<Object, List<SimulationModifier>> simulationModifiers) { - - // Add modifiers (if any) - List<SimulationModifier> modifiers = simulationModifiers.get(component); - if (modifiers != null) { - DefaultMutableTreeNode modifierNode; - - if (component.getChildCount() > 0) { - modifierNode = new DefaultMutableTreeNode("Optimization parameters"); - node.add(modifierNode); - } else { - modifierNode = node; - } - - for (SimulationModifier m : modifiers) { - modifierNode.add(new DefaultMutableTreeNode(m)); - } - } - - // Add child components - for (RocketComponent c : component.getChildren()) { - DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(c); - node.add(newNode); - populateTree(newNode, c, simulationModifiers); - } - - } - - - /** - * Expand the rocket components, but not the modifiers. - */ - @SuppressWarnings("rawtypes") - public void expandComponents() { - DefaultMutableTreeNode baseNode = (DefaultMutableTreeNode) this.getModel().getRoot(); - - Enumeration enumeration = baseNode.breadthFirstEnumeration(); - - while (enumeration.hasMoreElements()) { - DefaultMutableTreeNode node = (DefaultMutableTreeNode) enumeration.nextElement(); - Object object = node.getUserObject(); - if (object instanceof RocketComponent) { - this.makeVisible(new TreePath(node.getPath())); - } - } - } - - - - - public class ComponentModifierTreeRenderer extends DefaultTreeCellRenderer { - private Font componentFont; - private Font stringFont; - private Font modifierFont; - - @Override - public Component getTreeCellRendererComponent( - JTree tree, - Object value, - boolean sel, - boolean expanded, - boolean leaf, - int row, - boolean hasFocus2) { - - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus2); - - if (componentFont == null) { - makeFonts(); - } - - - // Customize based on line type - - Object object = ((DefaultMutableTreeNode) value).getUserObject(); - - // Set icon (for rocket components, null for others) - setIcon(ComponentIcons.getSmallIcon(object.getClass())); - - - // Set text color/style - if (object instanceof RocketComponent) { - setForeground(Color.GRAY); - setFont(componentFont); - - // Set tooltip - RocketComponent c = (RocketComponent) object; - String comment = c.getComment().trim(); - if (comment.length() > 0) { - comment = TextUtil.htmlEncode(comment); - comment = "<html>" + comment.replace("\n", "<br>"); - this.setToolTipText(comment); - } else { - this.setToolTipText(null); - } - } else if (object instanceof String) { - setForeground(Color.GRAY); - setFont(stringFont); - } else if (object instanceof SimulationModifier) { - - if (selectedModifiers.contains(object)) { - setForeground(Color.GRAY); - } else { - setForeground(Color.BLACK); - } - setFont(modifierFont); - setText(((SimulationModifier) object).getName()); - setToolTipText(((SimulationModifier) object).getDescription()); - } - - return this; - } - - private void makeFonts() { - Font font = getFont(); - componentFont = font.deriveFont(Font.ITALIC); - stringFont = font; - modifierFont = font.deriveFont(Font.BOLD); - } - - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java b/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java deleted file mode 100644 index a106ba32..00000000 --- a/src/net/sf/openrocket/gui/dialogs/preferences/MaterialEditPanel.java +++ /dev/null @@ -1,392 +0,0 @@ -package net.sf.openrocket.gui.dialogs.preferences; - -import java.awt.Color; -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.Iterator; - -import javax.swing.JButton; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.SwingUtilities; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.table.DefaultTableCellRenderer; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.database.Database; -import net.sf.openrocket.database.Databases; -import net.sf.openrocket.gui.adaptors.Column; -import net.sf.openrocket.gui.adaptors.ColumnTableModel; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.dialogs.CustomMaterialDialog; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.unit.Value; - -public class MaterialEditPanel extends JPanel { - - private final JTable table; - - private final JButton addButton; - private final JButton editButton; - private final JButton deleteButton; - private final JButton revertButton; - private static final Translator trans = Application.getTranslator(); - - - public MaterialEditPanel() { - super(new MigLayout("fill")); - - - // TODO: LOW: Create sorter that keeps material types always in order - final ColumnTableModel model = new ColumnTableModel( - //// Material - new Column(trans.get("matedtpan.col.Material")) { - @Override - public Object getValueAt(int row) { - return getMaterial(row).getName(); - } - }, - //// Type - new Column(trans.get("matedtpan.col.Type")) { - @Override - public Object getValueAt(int row) { - return getMaterial(row).getType().toString(); - } - - @Override - public int getDefaultWidth() { - return 15; - } - }, - //// Density - new Column(trans.get("matedtpan.col.Density")) { - @Override - public Object getValueAt(int row) { - Material m = getMaterial(row); - double d = m.getDensity(); - switch (m.getType()) { - case LINE: - return UnitGroup.UNITS_DENSITY_LINE.toValue(d); - - case SURFACE: - return UnitGroup.UNITS_DENSITY_SURFACE.toValue(d); - - case BULK: - return UnitGroup.UNITS_DENSITY_BULK.toValue(d); - - default: - throw new IllegalStateException("Material type " + m.getType()); - } - } - - @Override - public int getDefaultWidth() { - return 15; - } - - @Override - public Class<?> getColumnClass() { - return Value.class; - } - } - ) { - @Override - public int getRowCount() { - return Databases.BULK_MATERIAL.size() + Databases.SURFACE_MATERIAL.size() + - Databases.LINE_MATERIAL.size(); - } - }; - - table = new JTable(model); - model.setColumnWidths(table.getColumnModel()); - table.setAutoCreateRowSorter(true); - table.setDefaultRenderer(Object.class, new MaterialCellRenderer()); - this.add(new JScrollPane(table), "w 200px, h 100px, grow 100"); - - - //// New button - addButton = new JButton(trans.get("matedtpan.but.new")); - //// Add a new material - addButton.setToolTipText(trans.get("matedtpan.col.but.ttip.New")); - addButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - CustomMaterialDialog dialog = new CustomMaterialDialog( - SwingUtilities.getWindowAncestor(MaterialEditPanel.this), - //// Add a custom material - null, false, trans.get("matedtpan.title.Addcustmaterial")); - dialog.setVisible(true); - if (dialog.getOkClicked()) { - Material mat = dialog.getMaterial(); - getDatabase(mat).add(mat); - model.fireTableDataChanged(); - setButtonStates(); - } - } - }); - this.add(addButton, "gap rel rel para para, w 70lp, split 5, flowy, growx 1, top"); - - //// Edit button - editButton = new JButton(trans.get("matedtpan.but.edit")); - //// Edit an existing material - editButton.setToolTipText(trans.get("matedtpan.but.ttip.edit")); - editButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int sel = table.getSelectedRow(); - if (sel < 0) - return; - sel = table.convertRowIndexToModel(sel); - Material m = getMaterial(sel); - - CustomMaterialDialog dialog; - if (m.isUserDefined()) { - dialog = new CustomMaterialDialog( - SwingUtilities.getWindowAncestor(MaterialEditPanel.this), - //// Edit material - m, false, trans.get("matedtpan.title.Editmaterial")); - } else { - dialog = new CustomMaterialDialog( - SwingUtilities.getWindowAncestor(MaterialEditPanel.this), - //// Add a custom material - m, false, trans.get("matedtpan.title.Addcustmaterial"), - //// The built-in materials cannot be modified. - trans.get("matedtpan.title2.Editmaterial")); - } - - dialog.setVisible(true); - - if (dialog.getOkClicked()) { - if (m.isUserDefined()) { - getDatabase(m).remove(m); - } - Material mat = dialog.getMaterial(); - getDatabase(mat).add(mat); - model.fireTableDataChanged(); - setButtonStates(); - } - } - }); - this.add(editButton, "gap rel rel para para, growx 1, top"); - - //// Delete button - deleteButton = new JButton(trans.get("matedtpan.but.delete")); - //// Delete a user-defined material - deleteButton.setToolTipText(trans.get("matedtpan.but.ttip.delete")); - deleteButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int sel = table.getSelectedRow(); - if (sel < 0) - return; - sel = table.convertRowIndexToModel(sel); - Material m = getMaterial(sel); - if (!m.isUserDefined()) - return; - getDatabase(m).remove(m); - model.fireTableDataChanged(); - setButtonStates(); - } - }); - this.add(deleteButton, "gap rel rel para para, growx 1, top"); - - - this.add(new JPanel(), "grow 1"); - - //// Revert all button - revertButton = new JButton(trans.get("matedtpan.but.revertall")); - //// Delete all user-defined materials - revertButton.setToolTipText(trans.get("matedtpan.but.ttip.revertall")); - revertButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int sel = JOptionPane.showConfirmDialog(MaterialEditPanel.this, - //// Delete all user-defined materials? - trans.get("matedtpan.title.Deletealluser-defined"), - //// Revert all? - trans.get("matedtpan.title.Revertall"), - JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - if (sel == JOptionPane.YES_OPTION) { - Iterator<Material> iterator; - - iterator = Databases.LINE_MATERIAL.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUserDefined()) - iterator.remove(); - } - - iterator = Databases.SURFACE_MATERIAL.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUserDefined()) - iterator.remove(); - } - - iterator = Databases.BULK_MATERIAL.iterator(); - while (iterator.hasNext()) { - if (iterator.next().isUserDefined()) - iterator.remove(); - } - model.fireTableDataChanged(); - setButtonStates(); - } - } - }); - this.add(revertButton, "gap rel rel para para, growx 1, bottom, wrap unrel"); - - setButtonStates(); - table.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - setButtonStates(); - } - }); - table.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - editButton.doClick(); - } - } - }); - - //// <html><i>Editing materials will not affect existing - //// rocket designs.</i> - this.add(new StyledLabel(trans.get("matedtpan.lbl.edtmaterials"), -2, Style.ITALIC), "span"); - - - } - - - private Database<Material> getDatabase(Material m) { - switch (m.getType()) { - case BULK: - return Databases.BULK_MATERIAL; - - case SURFACE: - return Databases.SURFACE_MATERIAL; - - case LINE: - return Databases.LINE_MATERIAL; - - default: - throw new IllegalArgumentException("Material type invalid, m=" + m); - } - } - - - private void setButtonStates() { - int sel = table.getSelectedRow(); - - // Add button always enabled - addButton.setEnabled(true); - - // Edit button enabled if a material is selected - editButton.setEnabled(sel >= 0); - - // Delete button enabled if a user-defined material is selected - if (sel >= 0) { - int modelRow = table.convertRowIndexToModel(sel); - deleteButton.setEnabled(getMaterial(modelRow).isUserDefined()); - } else { - deleteButton.setEnabled(false); - } - - // Revert button enabled if any user-defined material exists - boolean found = false; - - for (Material m : Databases.BULK_MATERIAL) { - if (m.isUserDefined()) { - found = true; - break; - } - } - if (!found) { - for (Material m : Databases.SURFACE_MATERIAL) { - if (m.isUserDefined()) { - found = true; - break; - } - } - } - if (!found) { - for (Material m : Databases.LINE_MATERIAL) { - if (m.isUserDefined()) { - found = true; - break; - } - } - } - revertButton.setEnabled(found); - - } - - private Material getMaterial(int origRow) { - int row = origRow; - int n; - - n = Databases.BULK_MATERIAL.size(); - if (row < n) { - return Databases.BULK_MATERIAL.get(row); - } - row -= n; - - n = Databases.SURFACE_MATERIAL.size(); - if (row < n) { - return Databases.SURFACE_MATERIAL.get(row); - } - row -= n; - - n = Databases.LINE_MATERIAL.size(); - if (row < n) { - return Databases.LINE_MATERIAL.get(row); - } - throw new IndexOutOfBoundsException("row=" + origRow + " while material count" + - " bulk:" + Databases.BULK_MATERIAL.size() + - " surface:" + Databases.SURFACE_MATERIAL.size() + - " line:" + Databases.LINE_MATERIAL.size()); - } - - - private class MaterialCellRenderer extends DefaultTableCellRenderer { - - /* (non-Javadoc) - * @see javax.swing.table.DefaultTableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int) - */ - @Override - public Component getTableCellRendererComponent(JTable table, Object value, - boolean isSelected, boolean hasFocus, int row, int column) { - Component c = super.getTableCellRendererComponent(table, value, isSelected, - hasFocus, row, column); - if (c instanceof JLabel) { - JLabel label = (JLabel) c; - Material m = getMaterial(row); - - if (isSelected) { - if (m.isUserDefined()) - label.setForeground(table.getSelectionForeground()); - else - label.setForeground(Color.GRAY); - } else { - if (m.isUserDefined()) - label.setForeground(table.getForeground()); - else - label.setForeground(Color.GRAY); - } - } - return c; - } - - } - -} diff --git a/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java b/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java deleted file mode 100644 index 986c4a40..00000000 --- a/src/net/sf/openrocket/gui/dialogs/preferences/PreferencesDialog.java +++ /dev/null @@ -1,693 +0,0 @@ -package net.sf.openrocket.gui.dialogs.preferences; - -import java.awt.Dialog; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Locale; - -import javax.swing.AbstractListModel; -import javax.swing.ComboBoxModel; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JFileChooser; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.JTabbedPane; -import javax.swing.JTextField; -import javax.swing.Timer; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.communication.UpdateInfo; -import net.sf.openrocket.communication.UpdateInfoRetriever; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.dialogs.UpdateInfoDialog; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.gui.util.SimpleFileFilter; -import net.sf.openrocket.l10n.L10N; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.Preferences; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.Named; -import net.sf.openrocket.util.Utils; - - -public class PreferencesDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - - private final List<DefaultUnitSelector> unitSelectors = new ArrayList<DefaultUnitSelector>(); - - private File defaultDirectory = null; - private static final Translator trans = Application.getTranslator(); - - private PreferencesDialog(Window parent) { - //// Preferences - super(parent, trans.get("pref.dlg.title.Preferences"), Dialog.ModalityType.APPLICATION_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill, gap unrel", "[grow]", "[grow][]")); - - JTabbedPane tabbedPane = new JTabbedPane(); - panel.add(tabbedPane, "grow, wrap"); - - //// Units and Default units - tabbedPane.addTab(trans.get("pref.dlg.tab.Units"), null, unitsPane(), - trans.get("pref.dlg.tab.Defaultunits")); - //// Materials and Custom materials - tabbedPane.addTab(trans.get("pref.dlg.tab.Materials"), null, new MaterialEditPanel(), - trans.get("pref.dlg.tab.Custommaterials")); - //// Options and Miscellaneous options - tabbedPane.addTab(trans.get("pref.dlg.tab.Options"), null, optionsPane(), - trans.get("pref.dlg.tab.Miscellaneousoptions")); - - //// Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent arg0) { - PreferencesDialog.this.setVisible(false); - PreferencesDialog.this.dispose(); - } - }); - panel.add(close, "span, right, tag close"); - - this.setContentPane(panel); - pack(); - this.setLocationRelativeTo(null); - - this.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - ((SwingPreferences) Application.getPreferences()).storeDefaultUnits(); - } - }); - - GUIUtil.setDisposableDialogOptions(this, close); - } - - - private JPanel optionsPane() { - JPanel panel = new JPanel(new MigLayout("fillx, ins 30lp n n n")); - - - //// Language selector - Locale userLocale = null; - { - String locale = Application.getPreferences().getString("locale", null); - userLocale = L10N.toLocale(locale); - } - List<Named<Locale>> locales = new ArrayList<Named<Locale>>(); - for (Locale l : SwingPreferences.getSupportedLocales()) { - locales.add(new Named<Locale>(l, l.getDisplayLanguage())); - } - Collections.sort(locales); - locales.add(0, new Named<Locale>(null, trans.get("languages.default"))); - - final JComboBox languageCombo = new JComboBox(locales.toArray()); - for (int i = 0; i < locales.size(); i++) { - if (Utils.equals(userLocale, locales.get(i).get())) { - languageCombo.setSelectedIndex(i); - } - } - languageCombo.addActionListener(new ActionListener() { - @Override - @SuppressWarnings("unchecked") - public void actionPerformed(ActionEvent e) { - Named<Locale> selection = (Named<Locale>) languageCombo.getSelectedItem(); - Locale l = selection.get(); - Application.getPreferences().putString(Preferences.USER_LOCAL, l == null ? null : l.toString()); - } - }); - panel.add(new JLabel(trans.get("lbl.language")), "gapright para"); - panel.add(languageCombo, "wrap rel, growx, sg combos"); - - panel.add(new StyledLabel(trans.get("PreferencesDialog.lbl.languageEffect"), -3, Style.ITALIC), "span, wrap para*2"); - - - //// Position to insert new body components: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Positiontoinsert")), "gapright para"); - panel.add(new JComboBox(new PrefChoiseSelector(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, - //// Always ask - //// Insert in middle - //// Add to end - trans.get("pref.dlg.PrefChoiseSelector1"), - trans.get("pref.dlg.PrefChoiseSelector2"), - trans.get("pref.dlg.PrefChoiseSelector3"))), "wrap para, growx, sg combos"); - - //// Confirm deletion of simulations: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Confirmdeletion"))); - panel.add(new JComboBox(new PrefBooleanSelector(Preferences.CONFIRM_DELETE_SIMULATION, - //// Delete - //// Confirm - trans.get("pref.dlg.PrefBooleanSelector1"), - trans.get("pref.dlg.PrefBooleanSelector2"), true)), "wrap 40lp, growx, sg combos"); - - //// User-defined thrust curves: - panel.add(new JLabel(trans.get("pref.dlg.lbl.User-definedthrust")), "spanx, wrap"); - final JTextField field = new JTextField(); - List<File> files = ((SwingPreferences) Application.getPreferences()).getUserThrustCurveFiles(); - String str = ""; - for (File file : files) { - if (str.length() > 0) { - str += ";"; - } - str += file.getAbsolutePath(); - } - field.setText(str); - field.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void removeUpdate(DocumentEvent e) { - changed(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - changed(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - changed(); - } - - private void changed() { - String text = field.getText(); - List<File> list = new ArrayList<File>(); - for (String s : text.split(";")) { - s = s.trim(); - if (s.length() > 0) { - list.add(new File(s)); - } - } - ((SwingPreferences) Application.getPreferences()).setUserThrustCurveFiles(list); - } - }); - panel.add(field, "w 100px, gapright unrel, spanx, growx, split"); - - //// Add button - JButton button = new JButton(trans.get("pref.dlg.but.add")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - JFileChooser chooser = new JFileChooser(); - SimpleFileFilter filter = - new SimpleFileFilter( - //// All thrust curve files (*.eng; *.rse; *.zip; directories) - trans.get("pref.dlg.Allthrustcurvefiles"), - true, "eng", "rse", "zip"); - chooser.addChoosableFileFilter(filter); - //// RASP motor files (*.eng) - chooser.addChoosableFileFilter(new SimpleFileFilter(trans.get("pref.dlg.RASPfiles"), - true, "eng")); - //// RockSim engine files (*.rse) - chooser.addChoosableFileFilter(new SimpleFileFilter(trans.get("pref.dlg.RockSimfiles"), - true, "rse")); - //// ZIP archives (*.zip) - chooser.addChoosableFileFilter(new SimpleFileFilter(trans.get("pref.dlg.ZIParchives"), - true, "zip")); - chooser.setFileFilter(filter); - chooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); - if (defaultDirectory != null) { - chooser.setCurrentDirectory(defaultDirectory); - } - - //// Add - int returnVal = chooser.showDialog(PreferencesDialog.this, trans.get("pref.dlg.Add")); - if (returnVal == JFileChooser.APPROVE_OPTION) { - log.user("Adding user thrust curve: " + chooser.getSelectedFile()); - defaultDirectory = chooser.getCurrentDirectory(); - String text = field.getText().trim(); - if (text.length() > 0) { - text += ";"; - } - text += chooser.getSelectedFile().getAbsolutePath(); - field.setText(text); - } - } - }); - panel.add(button, "gapright unrel"); - - //// Reset button - button = new JButton(trans.get("pref.dlg.but.reset")); - - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - // First one sets to the default, but does not un-set the pref - field.setText(((SwingPreferences)Application.getPreferences()).getDefaultUserThrustCurveFile().getAbsolutePath()); - ((SwingPreferences) Application.getPreferences()).setUserThrustCurveFiles(null); - } - }); - panel.add(button, "wrap"); - - //// Add directories, RASP motor files (*.eng), RockSim engine files (*.rse) or ZIP archives separated by a semicolon (;) to load external thrust curves. Changes will take effect the next time you start OpenRocket. - DescriptionArea desc = new DescriptionArea(trans.get("pref.dlg.DescriptionArea.Adddirectories"), 3, -3, false); - desc.setBackground(getBackground()); - panel.add(desc, "spanx, growx, wrap 40lp"); - - - - //// Check for software updates at startup - final JCheckBox softwareUpdateBox = - new JCheckBox(trans.get("pref.dlg.checkbox.Checkupdates")); - softwareUpdateBox.setSelected( Application.getPreferences().getCheckUpdates()); - softwareUpdateBox.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Application.getPreferences().setCheckUpdates(softwareUpdateBox.isSelected()); - } - }); - panel.add(softwareUpdateBox); - - //// Check now button - button = new JButton(trans.get("pref.dlg.but.checknow")); - //// Check for software updates now - button.setToolTipText(trans.get("pref.dlg.ttip.Checkupdatesnow")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - checkForUpdates(); - } - }); - panel.add(button, "right, wrap"); - - - return panel; - } - - private JPanel unitsPane() { - JPanel panel = new JPanel(new MigLayout("", "[][]40lp[][]")); - JComboBox combo; - - //// Select your preferred units: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Selectprefunits")), "span, wrap paragraph"); - - - //// Rocket dimensions: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Rocketdimensions"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH)); - panel.add(combo, "sizegroup boxes"); - - //// Line density: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Linedensity"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_LINE)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Motor dimensions: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Motordimensions"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MOTOR_DIMENSIONS)); - panel.add(combo, "sizegroup boxes"); - - //// Surface density: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Surfacedensity"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_SURFACE)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Distance: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Distance"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE)); - panel.add(combo, "sizegroup boxes"); - - //// Bulk density:: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Bulkdensity"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_BULK)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Velocity: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Velocity"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_VELOCITY)); - panel.add(combo, "sizegroup boxes"); - - //// Surface roughness: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Surfaceroughness"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROUGHNESS)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Acceleration: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Acceleration"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ACCELERATION)); - panel.add(combo, "sizegroup boxes"); - - //// Area: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Area"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_AREA)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Mass: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Mass"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MASS)); - panel.add(combo, "sizegroup boxes"); - - //// Angle: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Angle"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ANGLE)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Force: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Force"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE)); - panel.add(combo, "sizegroup boxes"); - - //// Roll rate: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Rollrate"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROLL)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Total impulse: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Totalimpulse"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_IMPULSE)); - panel.add(combo, "sizegroup boxes"); - - //// Temperature: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Temperature"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_TEMPERATURE)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Moment of inertia: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Momentofinertia"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_INERTIA)); - panel.add(combo, "sizegroup boxes"); - - //// Pressure: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Pressure"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_PRESSURE)); - panel.add(combo, "sizegroup boxes, wrap"); - - - //// Stability: - panel.add(new JLabel(trans.get("pref.dlg.lbl.Stability"))); - combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_STABILITY)); - panel.add(combo, "sizegroup boxes, wrap para"); - - - - //// Default metric button - JButton button = new JButton(trans.get("pref.dlg.but.defaultmetric")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - UnitGroup.setDefaultMetricUnits(); - for (DefaultUnitSelector s : unitSelectors) - s.fireChange(); - } - }); - panel.add(button, "spanx, split 2, grow"); - - //// Default imperial button - button = new JButton(trans.get("pref.dlg.but.defaultimperial")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - UnitGroup.setDefaultImperialUnits(); - for (DefaultUnitSelector s : unitSelectors) - s.fireChange(); - } - }); - panel.add(button, "grow, wrap para"); - - //// The effects will take place the next time you open a window. - panel.add(new StyledLabel( - trans.get("pref.dlg.lbl.effect1"), -2, Style.ITALIC), - "spanx, wrap"); - - - return panel; - } - - - - - - private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel { - - private final UnitGroup group; - - public DefaultUnitSelector(UnitGroup group) { - this.group = group; - unitSelectors.add(this); - } - - @Override - public Object getSelectedItem() { - return group.getDefaultUnit(); - } - - @Override - public void setSelectedItem(Object item) { - if (item == null) { - // Clear selection - huh? - return; - } - if (!(item instanceof Unit)) { - throw new IllegalArgumentException("Illegal argument " + item); - } - group.setDefaultUnit(group.getUnitIndex((Unit) item)); - } - - @Override - public Object getElementAt(int index) { - return group.getUnit(index); - } - - @Override - public int getSize() { - return group.getUnitCount(); - } - - - public void fireChange() { - this.fireContentsChanged(this, 0, this.getSize()); - } - } - - - - private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel { - private final String preference; - private final String[] descriptions; - - public PrefChoiseSelector(String preference, String... descriptions) { - this.preference = preference; - this.descriptions = descriptions; - } - - @Override - public Object getSelectedItem() { - return descriptions[Application.getPreferences().getChoice(preference, descriptions.length, 0)]; - } - - @Override - public void setSelectedItem(Object item) { - if (item == null) { - // Clear selection - huh? - return; - } - if (!(item instanceof String)) { - throw new IllegalArgumentException("Illegal argument " + item); - } - int index; - for (index = 0; index < descriptions.length; index++) { - if (((String) item).equalsIgnoreCase(descriptions[index])) - break; - } - if (index >= descriptions.length) { - throw new IllegalArgumentException("Illegal argument " + item); - } - - Application.getPreferences().putChoice(preference, index); - } - - @Override - public Object getElementAt(int index) { - return descriptions[index]; - } - - @Override - public int getSize() { - return descriptions.length; - } - } - - - private class PrefBooleanSelector extends AbstractListModel implements ComboBoxModel { - private final String preference; - private final String trueDesc, falseDesc; - private final boolean def; - - public PrefBooleanSelector(String preference, String falseDescription, - String trueDescription, boolean defaultState) { - this.preference = preference; - this.trueDesc = trueDescription; - this.falseDesc = falseDescription; - this.def = defaultState; - } - - @Override - public Object getSelectedItem() { - if (Application.getPreferences().getBoolean(preference, def)) { - return trueDesc; - } else { - return falseDesc; - } - } - - @Override - public void setSelectedItem(Object item) { - if (item == null) { - // Clear selection - huh? - return; - } - if (!(item instanceof String)) { - throw new IllegalArgumentException("Illegal argument " + item); - } - - if (trueDesc.equals(item)) { - Application.getPreferences().putBoolean(preference, true); - } else if (falseDesc.equals(item)) { - Application.getPreferences().putBoolean(preference, false); - } else { - throw new IllegalArgumentException("Illegal argument " + item); - } - } - - @Override - public Object getElementAt(int index) { - switch (index) { - case 0: - return def ? trueDesc : falseDesc; - - case 1: - return def ? falseDesc : trueDesc; - - default: - throw new IndexOutOfBoundsException("Boolean asked for index=" + index); - } - } - - @Override - public int getSize() { - return 2; - } - } - - - private void checkForUpdates() { - final UpdateInfoRetriever retriever = new UpdateInfoRetriever(); - retriever.start(); - - - // Progress dialog - final JDialog dialog = new JDialog(this, ModalityType.APPLICATION_MODAL); - JPanel panel = new JPanel(new MigLayout()); - - //// Checking for updates... - panel.add(new JLabel(trans.get("pref.dlg.lbl.Checkingupdates")), "wrap"); - - JProgressBar bar = new JProgressBar(); - bar.setIndeterminate(true); - panel.add(bar, "growx, wrap para"); - - //// Cancel button - JButton cancel = new JButton(trans.get("dlg.but.cancel")); - cancel.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - dialog.dispose(); - } - }); - panel.add(cancel, "right"); - dialog.add(panel); - - GUIUtil.setDisposableDialogOptions(dialog, cancel); - - - // Timer to monitor progress - final Timer timer = new Timer(100, null); - final long startTime = System.currentTimeMillis(); - - ActionListener listener = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (!retriever.isRunning() || startTime + 10000 < System.currentTimeMillis()) { - timer.stop(); - dialog.dispose(); - } - } - }; - timer.addActionListener(listener); - timer.start(); - - - // Wait for action - dialog.setVisible(true); - - - // Check result - UpdateInfo info = retriever.getUpdateInfo(); - if (info == null) { - JOptionPane.showMessageDialog(this, - //// An error occurred while communicating with the server. - trans.get("pref.dlg.lbl.msg1"), - //// Unable to retrieve update information - trans.get("pref.dlg.lbl.msg2"), JOptionPane.WARNING_MESSAGE, null); - } else if (info.getLatestVersion() == null || - info.getLatestVersion().equals("") || - BuildProperties.getVersion().equalsIgnoreCase(info.getLatestVersion())) { - JOptionPane.showMessageDialog(this, - //// You are running the latest version of OpenRocket. - trans.get("pref.dlg.lbl.msg3"), - //// No updates available - trans.get("pref.dlg.lbl.msg4"), JOptionPane.INFORMATION_MESSAGE, null); - } else { - UpdateInfoDialog infoDialog = new UpdateInfoDialog(info); - infoDialog.setVisible(true); - if (infoDialog.isReminderSelected()) { - Application.getPreferences().putString(SwingPreferences.LAST_UPDATE, ""); - } else { - Application.getPreferences().putString(SwingPreferences.LAST_UPDATE, info.getLatestVersion()); - } - } - - } - - - //////// Singleton implementation //////// - - private static PreferencesDialog dialog = null; - - public static void showPreferences(Window parent) { - if (dialog != null) { - dialog.dispose(); - } - dialog = new PreferencesDialog(parent); - dialog.setVisible(true); - } - - -} diff --git a/src/net/sf/openrocket/gui/figureelements/CGCaret.java b/src/net/sf/openrocket/gui/figureelements/CGCaret.java deleted file mode 100644 index 14a8a677..00000000 --- a/src/net/sf/openrocket/gui/figureelements/CGCaret.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.sf.openrocket.gui.figureelements; - -import java.awt.Color; -import java.awt.geom.Area; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Rectangle2D; - -/** - * A mark indicating the position of the center of gravity. It is a blue circle with every - * second quarter filled with blue. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class CGCaret extends Caret { - private static final float RADIUS = 7; - - private static Area caret = null; - - /** - * Create a new CGCaret at the specified coordinates. - */ - public CGCaret(double x, double y) { - super(x,y); - } - - /** - * Returns the Area corresponding to the caret. The Area object is created only once, - * after which the object is cloned for new copies. - */ - @Override - protected Area getCaret() { - if (caret != null) { - return (Area)caret.clone(); - } - - Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS); - caret = new Area(e); - - Area a; - a = new Area(new Rectangle2D.Float(-RADIUS,-RADIUS,RADIUS,RADIUS)); - caret.subtract(a); - a = new Area(new Rectangle2D.Float(0,0,RADIUS,RADIUS)); - caret.subtract(a); - - a = new Area(new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS)); - a.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f, - 2*0.9f*RADIUS,2*0.9f*RADIUS))); - caret.add(a); - - return (Area) caret.clone(); - } - - /** - * Return the color of the caret (blue). - */ - @Override - protected Color getColor() { - return Color.BLUE; - } - -} diff --git a/src/net/sf/openrocket/gui/figureelements/CPCaret.java b/src/net/sf/openrocket/gui/figureelements/CPCaret.java deleted file mode 100644 index 09e9cceb..00000000 --- a/src/net/sf/openrocket/gui/figureelements/CPCaret.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.sf.openrocket.gui.figureelements; - -import java.awt.Color; -import java.awt.geom.Area; -import java.awt.geom.Ellipse2D; - -/** - * A mark indicating the position of the center of pressure. It is a red filled circle - * inside a slightly larger red circle. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class CPCaret extends Caret { - private static final float RADIUS = 7; - - private static Area caret = null; - - /** - * Create a new CPCaret at the specified coordinates. - */ - public CPCaret(double x, double y) { - super(x,y); - } - - /** - * Returns the Area object of the caret. The Area object is created only once, - * after which new copies are cloned from it. - */ - @Override - protected Area getCaret() { - if (caret != null) { - return (Area)caret.clone(); - } - - Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS); - caret = new Area(e); - - caret.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f, - 2*0.9f*RADIUS,2*0.9f*RADIUS))); - - caret.add(new Area(new Ellipse2D.Float(-RADIUS*0.75f,-RADIUS*0.75f, - 2*0.75f*RADIUS,2*0.75f*RADIUS))); - - return (Area) caret.clone(); - } - - - /** - * Return the color of the caret (red). - */ - @Override - protected Color getColor() { - return Color.RED; - } -} diff --git a/src/net/sf/openrocket/gui/figureelements/Caret.java b/src/net/sf/openrocket/gui/figureelements/Caret.java deleted file mode 100644 index b82731ff..00000000 --- a/src/net/sf/openrocket/gui/figureelements/Caret.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.sf.openrocket.gui.figureelements; - - -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.geom.AffineTransform; -import java.awt.geom.Area; - -public abstract class Caret implements FigureElement { - private double x,y; - - /** - * Creates a new caret at the specified coordinates. - */ - public Caret(double x, double y) { - this.x = x; - this.y = y; - } - - /** - * Sets the position of the caret to the new coordinates. - */ - public void setPosition(double x, double y) { - this.x = x; - this.y = y; - } - - /** - * Paints the caret to the Graphics2D element. - */ - public void paint(Graphics2D g2, double scale) { - Area caret = getCaret(); - AffineTransform t = new AffineTransform(1.0/scale, 0, 0, 1.0/scale, x, y); - caret.transform(t); - - g2.setColor(getColor()); - g2.fill(caret); - } - - - public void paint(Graphics2D g2, double scale, Rectangle visible) { - throw new UnsupportedOperationException("paint() with rectangle unsupported."); - } - - /** - * Return the Area object corresponding to the mark. - */ - protected abstract Area getCaret(); - - /** - * Return the color to be used when drawing the mark. - */ - protected abstract Color getColor(); -} diff --git a/src/net/sf/openrocket/gui/figureelements/FigureElement.java b/src/net/sf/openrocket/gui/figureelements/FigureElement.java deleted file mode 100644 index 953d1918..00000000 --- a/src/net/sf/openrocket/gui/figureelements/FigureElement.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.sf.openrocket.gui.figureelements; - -import java.awt.Graphics2D; -import java.awt.Rectangle; - -public interface FigureElement { - - public void paint(Graphics2D g2, double scale); - - public void paint(Graphics2D g2, double scale, Rectangle visible); - -} diff --git a/src/net/sf/openrocket/gui/figureelements/RocketInfo.java b/src/net/sf/openrocket/gui/figureelements/RocketInfo.java deleted file mode 100644 index 39fb32a5..00000000 --- a/src/net/sf/openrocket/gui/figureelements/RocketInfo.java +++ /dev/null @@ -1,439 +0,0 @@ -package net.sf.openrocket.gui.figureelements; - -import static net.sf.openrocket.util.Chars.ALPHA; -import static net.sf.openrocket.util.Chars.THETA; - -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.font.GlyphVector; -import java.awt.geom.Rectangle2D; - -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.MathUtil; - - -/** - * A <code>FigureElement</code> that draws text at different positions in the figure - * with general data about the rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocketInfo implements FigureElement { - - private static final Translator trans = Application.getTranslator(); - // Margin around the figure edges, pixels - private static final int MARGIN = 8; - - // Font to use - private static final Font FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 11); - private static final Font SMALLFONT = new Font(Font.SANS_SERIF, Font.PLAIN, 9); - - - private final Caret cpCaret = new CPCaret(0,0); - private final Caret cgCaret = new CGCaret(0,0); - - private final Configuration configuration; - private final UnitGroup stabilityUnits; - - private double cg = 0, cp = 0; - private double length = 0, diameter = 0; - private double mass = 0; - private double aoa = Double.NaN, theta = Double.NaN, mach = Application.getPreferences().getDefaultMach(); - - private WarningSet warnings = null; - - private boolean calculatingData = false; - private FlightData flightData = null; - - private Graphics2D g2 = null; - private float line = 0; - private float x1, x2, y1, y2; - - - - - - public RocketInfo(Configuration configuration) { - this.configuration = configuration; - this.stabilityUnits = UnitGroup.stabilityUnits(configuration); - } - - - @Override - public void paint(Graphics2D g2, double scale) { - throw new UnsupportedOperationException("paint() must be called with coordinates"); - } - - @Override - public void paint(Graphics2D g2, double scale, Rectangle visible) { - this.g2 = g2; - this.line = FONT.getLineMetrics("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", - g2.getFontRenderContext()).getHeight(); - - x1 = visible.x + MARGIN; - x2 = visible.x + visible.width - MARGIN; - y1 = visible.y + line ; - y2 = visible.y + visible.height - MARGIN; - - drawMainInfo(); - drawStabilityInfo(); - drawWarnings(); - drawFlightInformation(); - } - - - public void setCG(double cg) { - this.cg = cg; - } - - public void setCP(double cp) { - this.cp = cp; - } - - public void setLength(double length) { - this.length = length; - } - - public void setDiameter(double diameter) { - this.diameter = diameter; - } - - public void setMass(double mass) { - this.mass = mass; - } - - public void setWarnings(WarningSet warnings) { - this.warnings = warnings.clone(); - } - - public void setAOA(double aoa) { - this.aoa = aoa; - } - - public void setTheta(double theta) { - this.theta = theta; - } - - public void setMach(double mach) { - this.mach = mach; - } - - - public void setFlightData(FlightData data) { - this.flightData = data; - } - - public void setCalculatingData(boolean calc) { - this.calculatingData = calc; - } - - - - - private void drawMainInfo() { - GlyphVector name = createText(configuration.getRocket().getName()); - GlyphVector lengthLine = createText( - //// Length - trans.get("RocketInfo.lengthLine.Length") +" " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(length) + - //// , max. diameter - trans.get("RocketInfo.lengthLine.maxdiameter") +" " + - UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(diameter)); - - String massText; - if (configuration.hasMotors()) - //// Mass with motors - massText = trans.get("RocketInfo.massText1") +" "; - else - //// Mass with no motors - massText = trans.get("RocketInfo.massText2") +" "; - - massText += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(mass); - - GlyphVector massLine = createText(massText); - - - g2.setColor(Color.BLACK); - - g2.drawGlyphVector(name, x1, y1); - g2.drawGlyphVector(lengthLine, x1, y1+line); - g2.drawGlyphVector(massLine, x1, y1+2*line); - - } - - - private void drawStabilityInfo() { - String at; - //// at M= - at = trans.get("RocketInfo.at")+UnitGroup.UNITS_COEFFICIENT.getDefaultUnit().toStringUnit(mach); - if (!Double.isNaN(aoa)) { - at += " "+ALPHA+"=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(aoa); - } - if (!Double.isNaN(theta)) { - at += " "+THETA+"=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(theta); - } - - GlyphVector cgValue = createText( - getCg()); - GlyphVector cpValue = createText( - getCp()); - GlyphVector stabValue = createText( - getStability()); - //// CG: - GlyphVector cgText = createText(trans.get("RocketInfo.cgText") +" "); - //// CP: - GlyphVector cpText = createText(trans.get("RocketInfo.cpText") +" "); - //// Stability: - GlyphVector stabText = createText(trans.get("RocketInfo.stabText") + " "); - GlyphVector atText = createSmallText(at); - - Rectangle2D cgRect = cgValue.getVisualBounds(); - Rectangle2D cpRect = cpValue.getVisualBounds(); - Rectangle2D cgTextRect = cgText.getVisualBounds(); - Rectangle2D cpTextRect = cpText.getVisualBounds(); - Rectangle2D stabRect = stabValue.getVisualBounds(); - Rectangle2D stabTextRect = stabText.getVisualBounds(); - Rectangle2D atTextRect = atText.getVisualBounds(); - - double unitWidth = MathUtil.max(cpRect.getWidth(), cgRect.getWidth(), - stabRect.getWidth()); - double textWidth = Math.max(cpTextRect.getWidth(), cgTextRect.getWidth()); - - - g2.setColor(Color.BLACK); - - g2.drawGlyphVector(stabValue, (float)(x2-stabRect.getWidth()), y1); - g2.drawGlyphVector(cgValue, (float)(x2-cgRect.getWidth()), y1+line); - g2.drawGlyphVector(cpValue, (float)(x2-cpRect.getWidth()), y1+2*line); - - g2.drawGlyphVector(stabText, (float)(x2-unitWidth-stabTextRect.getWidth()), y1); - g2.drawGlyphVector(cgText, (float)(x2-unitWidth-cgTextRect.getWidth()), y1+line); - g2.drawGlyphVector(cpText, (float)(x2-unitWidth-cpTextRect.getWidth()), y1+2*line); - - cgCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+line-0.3*line); - cgCaret.paint(g2, 1.7); - - cpCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+2*line-0.3*line); - cpCaret.paint(g2, 1.7); - - float atPos; - if (unitWidth + textWidth + 10 > atTextRect.getWidth()) { - atPos = (float)(x2-(unitWidth+textWidth+10+atTextRect.getWidth())/2); - } else { - atPos = (float)(x2 - atTextRect.getWidth()); - } - - g2.setColor(Color.GRAY); - g2.drawGlyphVector(atText, atPos, y1 + 3*line); - - } - - /** - * Get the mass, in default mass units. - * - * @return the mass - */ - public double getMass() { - return mass; - } - - /** - * Get the mass in specified mass units. - * - * @param u UnitGroup.MASS - * - * @return the mass - */ - public String getMass(Unit u) { - return u.toStringUnit(mass); - } - - /** - * Get the stability, in calibers. - * - * @return the current stability margin - */ - public String getStability () { - return stabilityUnits.getDefaultUnit().toStringUnit(cp-cg); - } - - /** - * Get the center of pressure in default length units. - * - * @return the distance from the tip to the center of pressure, in default length units - */ - public String getCp () { - return getCp(UnitGroup.UNITS_LENGTH.getDefaultUnit()); - } - - /** - * Get the center of pressure in default length units. - * - * @param u UnitGroup.LENGTH - * - * @return the distance from the tip to the center of pressure, in default length units - */ - public String getCp (Unit u) { - return u.toStringUnit(cp); - } - - /** - * Get the center of gravity in default length units. - * - * @return the distance from the tip to the center of gravity, in default length units - */ - public String getCg () { - return getCg(UnitGroup.UNITS_LENGTH.getDefaultUnit()); - } - - /** - * Get the center of gravity in specified length units. - * - * @param u UnitGroup.LENGTH - * @return the distance from the tip to the center of gravity, in specified units - */ - public String getCg (Unit u) { - return u.toStringUnit(cg); - } - - /** - * Get the flight data for the current motor configuration. - * - * @return flight data, or null - */ - public FlightData getFlightData () { - return flightData; - } - - private void drawWarnings() { - if (warnings == null || warnings.isEmpty()) - return; - - GlyphVector[] texts = new GlyphVector[warnings.size()+1]; - double max = 0; - - //// Warning: - texts[0] = createText(trans.get("RocketInfo.Warning")); - int i=1; - for (Warning w: warnings) { - texts[i] = createText(w.toString()); - i++; - } - - for (GlyphVector v: texts) { - Rectangle2D rect = v.getVisualBounds(); - if (rect.getWidth() > max) - max = rect.getWidth(); - } - - - float y = y2 - line * warnings.size(); - g2.setColor(new Color(255,0,0,130)); - - for (GlyphVector v: texts) { - Rectangle2D rect = v.getVisualBounds(); - g2.drawGlyphVector(v, (float)(x2 - max/2 - rect.getWidth()/2), y); - y += line; - } - } - - - private void drawFlightInformation() { - double height = drawFlightData(); - - if (calculatingData) { - //// Calculating... - GlyphVector calculating = createText(trans.get("RocketInfo.Calculating")); - g2.setColor(Color.BLACK); - g2.drawGlyphVector(calculating, x1, (float)(y2-height)); - } - } - - - private double drawFlightData() { - if (flightData == null) - return 0; - - double width=0; - - //// Apogee: - GlyphVector apogee = createText(trans.get("RocketInfo.Apogee")+" "); - //// Max. velocity: - GlyphVector maxVelocity = createText(trans.get("RocketInfo.Maxvelocity") +" "); - //// Max. acceleration: - GlyphVector maxAcceleration = createText(trans.get("RocketInfo.Maxacceleration") + " "); - - GlyphVector apogeeValue, velocityValue, accelerationValue; - if (!Double.isNaN(flightData.getMaxAltitude())) { - apogeeValue = createText( - UnitGroup.UNITS_DISTANCE.toStringUnit(flightData.getMaxAltitude())); - } else { - //// N/A - apogeeValue = createText(trans.get("RocketInfo.apogeeValue")); - } - if (!Double.isNaN(flightData.getMaxVelocity())) { - velocityValue = createText( - UnitGroup.UNITS_VELOCITY.toStringUnit(flightData.getMaxVelocity()) + - //// (Mach - " " +trans.get("RocketInfo.Mach") +" " + - UnitGroup.UNITS_COEFFICIENT.toString(flightData.getMaxMachNumber()) + ")"); - } else { - //// N/A - velocityValue = createText(trans.get("RocketInfo.velocityValue")); - } - if (!Double.isNaN(flightData.getMaxAcceleration())) { - accelerationValue = createText( - UnitGroup.UNITS_ACCELERATION.toStringUnit(flightData.getMaxAcceleration())); - } else { - //// N/A - accelerationValue = createText(trans.get("RocketInfo.accelerationValue")); - } - - Rectangle2D rect; - rect = apogee.getVisualBounds(); - width = MathUtil.max(width, rect.getWidth()); - - rect = maxVelocity.getVisualBounds(); - width = MathUtil.max(width, rect.getWidth()); - - rect = maxAcceleration.getVisualBounds(); - width = MathUtil.max(width, rect.getWidth()); - - width += 5; - - if (!calculatingData) - g2.setColor(new Color(0,0,127)); - else - g2.setColor(new Color(0,0,127,127)); - - - g2.drawGlyphVector(apogee, (float)x1, (float)(y2-2*line)); - g2.drawGlyphVector(maxVelocity, (float)x1, (float)(y2-line)); - g2.drawGlyphVector(maxAcceleration, (float)x1, (float)(y2)); - - g2.drawGlyphVector(apogeeValue, (float)(x1+width), (float)(y2-2*line)); - g2.drawGlyphVector(velocityValue, (float)(x1+width), (float)(y2-line)); - g2.drawGlyphVector(accelerationValue, (float)(x1+width), (float)(y2)); - - return 3*line; - } - - - - private GlyphVector createText(String text) { - return FONT.createGlyphVector(g2.getFontRenderContext(), text); - } - - private GlyphVector createSmallText(String text) { - return SMALLFONT.createGlyphVector(g2.getFontRenderContext(), text); - } - -} diff --git a/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java b/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java deleted file mode 100644 index baff4a5b..00000000 --- a/src/net/sf/openrocket/gui/help/tours/GuidedTourSelectionDialog.java +++ /dev/null @@ -1,182 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.List; - -import javax.swing.AbstractListModel; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JEditorPane; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.ListSelectionModel; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.StyleSheet; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.components.StyledLabel.Style; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Named; - -public class GuidedTourSelectionDialog extends JDialog { - - private static final Translator trans = Application.getTranslator(); - - - - private final SlideSetManager slideSetManager; - private final List<String> tourNames; - - private SlideShowDialog slideShowDialog; - - private JList tourList; - private JEditorPane tourDescription; - private JLabel tourLength; - - - public GuidedTourSelectionDialog(Window parent) { - super(parent, trans.get("title"), ModalityType.MODELESS); - - slideSetManager = SlideSetManager.getSlideSetManager(); - tourNames = slideSetManager.getSlideSetNames(); - - JPanel panel = new JPanel(new MigLayout("fill")); - - panel.add(new StyledLabel(trans.get("lbl.selectTour"), Style.BOLD), "spanx, wrap rel"); - - tourList = new JList(new TourListModel()); - tourList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - tourList.getSelectionModel().addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - updateText(); - } - }); - tourList.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - startTour(); - } - } - }); - panel.add(new JScrollPane(tourList), "grow, gapright unrel, w 200lp, h 250lp"); - - - - // Sub-panel containing description and start button - JPanel sub = new JPanel(new MigLayout("fill, ins 0")); - sub.add(new StyledLabel(trans.get("lbl.description"), -1), "wrap rel"); - - tourDescription = new JEditorPane("text/html", ""); - tourDescription.setEditable(false); - StyleSheet ss = slideSetManager.getSlideSet(tourNames.get(0)).getStyleSheet(); - ((HTMLDocument) tourDescription.getDocument()).getStyleSheet().addStyleSheet(ss); - sub.add(new JScrollPane(tourDescription), "grow, wrap rel"); - - tourLength = new StyledLabel(-1); - sub.add(tourLength, "wrap unrel"); - - JButton start = new JButton(trans.get("btn.start")); - start.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - startTour(); - } - }); - sub.add(start, "growx"); - - panel.add(sub, "grow, wrap para, w 350lp, h 250lp"); - - - - JButton close = new JButton(trans.get("button.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - GuidedTourSelectionDialog.this.dispose(); - } - }); - panel.add(close, "spanx, right"); - - this.add(panel); - GUIUtil.setDisposableDialogOptions(this, close); - GUIUtil.rememberWindowPosition(this); - tourList.setSelectedIndex(0); - } - - - private void startTour() { - SlideSet ss = getSelectedSlideSet(); - if (ss == null) { - return; - } - - if (slideShowDialog != null && !slideShowDialog.isVisible()) { - closeTour(); - } - - if (slideShowDialog == null) { - slideShowDialog = new SlideShowDialog(this); - } - - slideShowDialog.setSlideSet(ss, 0); - slideShowDialog.setVisible(true); - } - - - private void closeTour() { - if (slideShowDialog != null) { - slideShowDialog.dispose(); - slideShowDialog = null; - } - } - - - private void updateText() { - SlideSet ss = getSelectedSlideSet(); - if (ss != null) { - tourDescription.setText(ss.getDescription()); - tourLength.setText(trans.get("lbl.length") + " " + ss.getSlideCount()); - } else { - tourDescription.setText(""); - tourLength.setText(trans.get("lbl.length")); - } - } - - - @SuppressWarnings("unchecked") - private SlideSet getSelectedSlideSet() { - return ((Named<SlideSet>) tourList.getSelectedValue()).get(); - } - - private class TourListModel extends AbstractListModel { - - @Override - public Object getElementAt(int index) { - String name = tourNames.get(index); - SlideSet set = slideSetManager.getSlideSet(name); - return new Named<SlideSet>(set, set.getTitle()); - } - - @Override - public int getSize() { - return tourNames.size(); - } - - } - - - -} diff --git a/src/net/sf/openrocket/gui/help/tours/Slide.java b/src/net/sf/openrocket/gui/help/tours/Slide.java deleted file mode 100644 index c3c1fab8..00000000 --- a/src/net/sf/openrocket/gui/help/tours/Slide.java +++ /dev/null @@ -1,78 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.lang.ref.SoftReference; -import java.net.URL; - -import javax.imageio.ImageIO; - -/** - * An individual slide in a guided tour. It contains a image (or reference to an - * image file) plus a text description (in HTML). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Slide { - private static final String NO_IMAGE = "none"; - - private final String imageFile; - private SoftReference<BufferedImage> imageReference = null; - - private final String text; - - - - public Slide(String imageFile, String text) { - this.imageFile = imageFile; - this.text = text; - } - - - - public BufferedImage getImage() { - - if (imageFile.equals(NO_IMAGE)) { - return new BufferedImage(0, 0, BufferedImage.TYPE_INT_ARGB); - } - - // Check the cache - if (imageReference != null) { - BufferedImage image = imageReference.get(); - if (image != null) { - return image; - } - } - - // Otherwise load and cache - BufferedImage image = loadImage(); - imageReference = new SoftReference<BufferedImage>(image); - - return image; - } - - public String getText() { - return text; - } - - - - private BufferedImage loadImage() { - BufferedImage img; - - try { - URL url = ClassLoader.getSystemResource(imageFile); - if (url != null) { - img = ImageIO.read(url); - } else { - //FIXME - img = null; - } - } catch (IOException e) { - // FIXME - img = null; - } - - return img; - } -} diff --git a/src/net/sf/openrocket/gui/help/tours/SlideSet.java b/src/net/sf/openrocket/gui/help/tours/SlideSet.java deleted file mode 100644 index 459bda23..00000000 --- a/src/net/sf/openrocket/gui/help/tours/SlideSet.java +++ /dev/null @@ -1,62 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.text.html.StyleSheet; - -/** - * A set of slides that composes a tour. - * - * A slide set contains a (localized, plain-text) title for the tour, a (possibly - * multiline, HTML-formatted) description and a number of slides. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SlideSet { - - private String title = ""; - private String description = ""; - private final List<Slide> slides = new ArrayList<Slide>(); - private StyleSheet styleSheet = new StyleSheet(); - - - - public String getTitle() { - return title; - } - - public void setTitle(String name) { - this.title = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(String description) { - this.description = description; - } - - - public Slide getSlide(int index) { - return this.slides.get(index); - } - - public void addSlide(Slide slide) { - this.slides.add(slide); - } - - public int getSlideCount() { - return this.slides.size(); - } - - public StyleSheet getStyleSheet() { - return styleSheet; - } - - public void setStyleSheet(StyleSheet styleSheet) { - this.styleSheet = styleSheet; - } - -} diff --git a/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java b/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java deleted file mode 100644 index 1a32cb47..00000000 --- a/src/net/sf/openrocket/gui/help/tours/SlideSetLoader.java +++ /dev/null @@ -1,173 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.sf.openrocket.util.BugException; - -/** - * Class that loads a slide set from a file. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SlideSetLoader { - - private static final Pattern NEW_SLIDE_PATTERN = Pattern.compile("^\\[(.*)\\]$"); - - private final String baseDir; - private TextLineReader source; - private Locale locale; - - - - - /** - * Constructor. - * - * @param baseDir The base directory from which to load from. It is prepended to the loaded - * file names and image file names. - */ - public SlideSetLoader(String baseDir) { - this(baseDir, Locale.getDefault()); - } - - - /** - * Constructor. - * - * @param baseDir The base directory from which to load from. It is prepended to the loaded - * file names and image file names. - * @param locale The locale for which the files are loaded. - */ - public SlideSetLoader(String baseDir, Locale locale) { - if (baseDir.length() > 0 && !baseDir.endsWith("/")) { - baseDir = baseDir + "/"; - } - this.baseDir = baseDir; - this.locale = locale; - } - - - /** - * Load a slide set from a file. The base directory is prepended to the - * file name first. - * - * @param filename the file to read in the base directory. - * @return the slide set - */ - public SlideSet load(String filename) throws IOException { - String file = baseDir + filename; - InputStream in = getLocalizedFile(file); - - try { - InputStreamReader reader = new InputStreamReader(in, "UTF-8"); - return load(reader); - } finally { - in.close(); - } - } - - - private InputStream getLocalizedFile(String filename) throws IOException { - for (String file : generateLocalizedFiles(filename)) { - InputStream in = ClassLoader.getSystemResourceAsStream(file); - if (in != null) { - return in; - } - } - throw new FileNotFoundException("File '" + filename + "' not found."); - } - - private List<String> generateLocalizedFiles(String filename) { - String base, ext; - int index = filename.lastIndexOf('.'); - if (index >= 0) { - base = filename.substring(0, index); - ext = filename.substring(index); - } else { - base = filename; - ext = ""; - } - - - List<String> list = new ArrayList<String>(); - list.add(base + "_" + locale.getLanguage() + "_" + locale.getCountry() + "_" + locale.getVariant() + ext); - list.add(base + "_" + locale.getLanguage() + "_" + locale.getCountry() + ext); - list.add(base + "_" + locale.getLanguage() + ext); - list.add(base + ext); - return list; - } - - - /** - * Load slide set from a reader. - * - * @param reader the reader to read from. - * @return the slide set. - */ - public SlideSet load(Reader reader) throws IOException { - source = new TextLineReader(reader); - - // Read title and description - String title = source.next(); - StringBuilder desc = new StringBuilder(); - while (!nextLineStartsSlide()) { - if (desc.length() > 0) { - desc.append('\n'); - } - desc.append(source.next()); - } - - // Create the slide set - SlideSet set = new SlideSet(); - set.setTitle(title); - set.setDescription(desc.toString()); - - - // Read the slides - while (source.hasNext()) { - Slide s = readSlide(); - set.addSlide(s); - } - - return set; - } - - - private Slide readSlide() { - - String imgLine = source.next(); - Matcher matcher = NEW_SLIDE_PATTERN.matcher(imgLine); - if (!matcher.matches()) { - throw new BugException("Line did not match new slide pattern: " + imgLine); - } - - String imageFile = matcher.group(1); - - StringBuffer desc = new StringBuffer(); - while (source.hasNext() && !nextLineStartsSlide()) { - if (desc.length() > 0) { - desc.append('\n'); - } - desc.append(source.next()); - } - - return new Slide(baseDir + imageFile, desc.toString()); - } - - - - private boolean nextLineStartsSlide() { - return NEW_SLIDE_PATTERN.matcher(source.peek()).matches(); - } - - -} diff --git a/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java b/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java deleted file mode 100644 index 0d9e3815..00000000 --- a/src/net/sf/openrocket/gui/help/tours/SlideSetManager.java +++ /dev/null @@ -1,165 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import javax.swing.text.html.StyleSheet; - -import net.sf.openrocket.util.BugException; - -/** - * A manager that loads a number of slide sets from a defined base directory - * and provides access to them. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SlideSetManager { - private static final String TOURS_BASE_DIR = "datafiles/tours"; - - private static final String TOURS_FILE = "tours.txt"; - private static final String STYLESHEET_FILE = "style.css"; - - private static SlideSetManager slideSetManager = null; - - - private final String baseDir; - private final Map<String, SlideSet> slideSets = new LinkedHashMap<String, SlideSet>(); - - - /** - * Sole constructor. - * - * @param baseDir the base directory containing the tours and style files. - */ - public SlideSetManager(String baseDir) { - if (baseDir.length() > 0 && !baseDir.endsWith("/")) { - baseDir = baseDir + "/"; - } - this.baseDir = baseDir; - } - - - /** - * Load all the tours. - */ - public void load() throws IOException { - slideSets.clear(); - - List<String> tours = loadTourList(); - StyleSheet styleSheet = loadStyleSheet(); - - for (String fileAndDir : tours) { - String base; - String file; - - String fullFileAndDir = baseDir + fileAndDir; - int index = fullFileAndDir.lastIndexOf('/'); - if (index >= 0) { - base = fullFileAndDir.substring(0, index); - file = fullFileAndDir.substring(index + 1); - } else { - base = ""; - file = ""; - } - - SlideSetLoader loader = new SlideSetLoader(base); - SlideSet set = loader.load(file); - set.setStyleSheet(styleSheet); - slideSets.put(fileAndDir, set); - } - - } - - - /** - * Return a set containing all the slide set names. - */ - public List<String> getSlideSetNames() { - return new ArrayList<String>(slideSets.keySet()); - } - - /** - * Retrieve an individual slide set. - * - * @param name the name of the slide set to retrieve. - * @return the slide set (never null) - * @throws IllegalArgumentException if the slide set with the name does not exist. - */ - public SlideSet getSlideSet(String name) { - SlideSet s = slideSets.get(name); - if (s == null) { - throw new IllegalArgumentException("Slide set with name '" + name + "' not found."); - } - return s; - } - - - private List<String> loadTourList() throws IOException { - InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + TOURS_FILE); - if (in == null) { - throw new FileNotFoundException("File '" + baseDir + TOURS_FILE + "' not found."); - } - - try { - - List<String> tours = new ArrayList<String>(); - TextLineReader reader = new TextLineReader(in); - while (reader.hasNext()) { - tours.add(reader.next()); - } - return tours; - - } finally { - in.close(); - } - } - - - private StyleSheet loadStyleSheet() throws IOException { - InputStream in = ClassLoader.getSystemResourceAsStream(baseDir + STYLESHEET_FILE); - if (in == null) { - throw new FileNotFoundException("File '" + baseDir + STYLESHEET_FILE + "' not found."); - } - - try { - - StyleSheet ss = new StyleSheet(); - InputStreamReader reader = new InputStreamReader(in, "UTF-8"); - ss.loadRules(reader, null); - return ss; - - } finally { - in.close(); - } - - } - - - - /** - * Return a singleton implementation that has loaded the default tours. - */ - public static SlideSetManager getSlideSetManager() { - if (slideSetManager == null) { - try { - SlideSetManager ssm = new SlideSetManager(TOURS_BASE_DIR); - ssm.load(); - - if (ssm.getSlideSetNames().isEmpty()) { - throw new FileNotFoundException("No tours found."); - } - - slideSetManager = ssm; - } catch (IOException e) { - throw new BugException(e); - } - } - return slideSetManager; - } -} diff --git a/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java b/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java deleted file mode 100644 index 842e4a86..00000000 --- a/src/net/sf/openrocket/gui/help/tours/SlideShowComponent.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.awt.Dimension; - -import javax.swing.JEditorPane; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.event.HyperlinkListener; -import javax.swing.text.html.HTMLDocument; -import javax.swing.text.html.StyleSheet; - -import net.sf.openrocket.gui.components.ImageDisplayComponent; - -/** - * Component that displays a single slide, with the image on top and - * text below it. The portions are resizeable. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SlideShowComponent extends JSplitPane { - - private final int WIDTH = 600; - private final int HEIGHT_IMAGE = 400; - private final int HEIGHT_TEXT = 100; - - private final ImageDisplayComponent imageDisplay; - private final JEditorPane textPane; - - - public SlideShowComponent() { - super(VERTICAL_SPLIT); - - imageDisplay = new ImageDisplayComponent(); - imageDisplay.setPreferredSize(new Dimension(WIDTH, HEIGHT_IMAGE)); - this.setLeftComponent(imageDisplay); - - textPane = new JEditorPane("text/html", ""); - textPane.setEditable(false); - textPane.setPreferredSize(new Dimension(WIDTH, HEIGHT_TEXT)); - - JScrollPane scrollPanel = new JScrollPane(textPane); - this.setRightComponent(scrollPanel); - - this.setResizeWeight(((double) HEIGHT_IMAGE) / (HEIGHT_IMAGE + HEIGHT_TEXT)); - } - - - - public void setSlide(Slide slide) { - this.imageDisplay.setImage(slide.getImage()); - this.textPane.setText(slide.getText()); - this.textPane.setCaretPosition(0); - } - - - /** - * Replace the current HTML style sheet with a new style sheet. - */ - public void setStyleSheet(StyleSheet newStyleSheet) { - HTMLDocument doc = (HTMLDocument) textPane.getDocument(); - StyleSheet base = doc.getStyleSheet(); - StyleSheet[] linked = base.getStyleSheets(); - if (linked != null) { - for (StyleSheet ss : linked) { - base.removeStyleSheet(ss); - } - } - - base.addStyleSheet(newStyleSheet); - } - - - public void addHyperlinkListener(HyperlinkListener listener) { - textPane.addHyperlinkListener(listener); - } - -} diff --git a/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java b/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java deleted file mode 100644 index 61936b91..00000000 --- a/src/net/sf/openrocket/gui/help/tours/SlideShowDialog.java +++ /dev/null @@ -1,173 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.KeyEvent; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JButton; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JPanel; -import javax.swing.JRootPane; -import javax.swing.KeyStroke; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Chars; - -public class SlideShowDialog extends JDialog { - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private SlideShowComponent slideShowComponent; - private SlideSet slideSet; - private int position; - - private JButton nextButton; - private JButton prevButton; - private JButton closeButton; - - - public SlideShowDialog(Window parent) { - super(parent, ModalityType.MODELESS); - - JPanel panel = new JPanel(new MigLayout("fill")); - - slideShowComponent = new SlideShowComponent(); - slideShowComponent.addHyperlinkListener(new SlideShowLinkListener(parent)); - panel.add(slideShowComponent, "spanx, grow, wrap para"); - - - JPanel sub = new JPanel(new MigLayout("ins 0, fill")); - - prevButton = new JButton(Chars.LEFT_ARROW + " " + trans.get("btn.prev")); - prevButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Clicked previous button"); - setPosition(position - 1); - } - }); - sub.add(prevButton, "left"); - - - - nextButton = new JButton(trans.get("btn.next") + " " + Chars.RIGHT_ARROW); - nextButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Clicked next button"); - setPosition(position + 1); - } - }); - sub.add(nextButton, "left, gapleft para"); - - - sub.add(new JPanel(), "growx"); - - - closeButton = new JButton(trans.get("button.close")); - closeButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SlideShowDialog.this.dispose(); - } - }); - sub.add(closeButton, "right"); - - - panel.add(sub, "growx"); - - this.add(panel); - updateEnabled(); - addKeyActions(); - GUIUtil.setDisposableDialogOptions(this, nextButton); - nextButton.grabFocus(); - GUIUtil.rememberWindowPosition(this); - GUIUtil.rememberWindowSize(this); - this.setAlwaysOnTop(true); - } - - public void setSlideSet(SlideSet slideSet, int position) { - this.slideSet = slideSet; - this.setTitle(slideSet.getTitle() + " " + Chars.EMDASH + " OpenRocket"); - slideShowComponent.setStyleSheet(slideSet.getStyleSheet()); - setPosition(position); - } - - public void setPosition(int position) { - if (this.slideSet == null) { - throw new BugException("setPosition called when slideSet is null"); - } - - if (position < 0 || position >= slideSet.getSlideCount()) { - throw new BugException("position exceeds slide count, position=" + position + - " slideCount=" + slideSet.getSlideCount()); - } - - this.position = position; - slideShowComponent.setSlide(slideSet.getSlide(position)); - updateEnabled(); - } - - - private void updateEnabled() { - if (slideSet == null) { - prevButton.setEnabled(false); - nextButton.setEnabled(false); - return; - } - - prevButton.setEnabled(position > 0); - nextButton.setEnabled(position < slideSet.getSlideCount() - 1); - } - - - - - - private void addKeyActions() { - Action next = new AbstractAction() { - @Override - public void actionPerformed(ActionEvent event) { - log.user("Key action for next slide"); - if (position < slideSet.getSlideCount() - 1) { - setPosition(position + 1); - } - } - }; - - Action previous = new AbstractAction() { - @Override - public void actionPerformed(ActionEvent event) { - log.user("Key action for previous slide"); - if (position > 0) { - setPosition(position - 1); - } - } - }; - - String nextKey = "slide:next"; - String prevKey = "slide:previous"; - - JRootPane root = this.getRootPane(); - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0), nextKey); - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_RIGHT, 0), nextKey); - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0), prevKey); - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_KP_LEFT, 0), prevKey); - - root.getActionMap().put(nextKey, next); - root.getActionMap().put(prevKey, previous); - } - - - -} diff --git a/src/net/sf/openrocket/gui/help/tours/SlideShowLinkListener.java b/src/net/sf/openrocket/gui/help/tours/SlideShowLinkListener.java deleted file mode 100644 index 5fc1d888..00000000 --- a/src/net/sf/openrocket/gui/help/tours/SlideShowLinkListener.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.awt.Desktop; -import java.awt.Window; -import java.net.URL; - -import javax.swing.event.HyperlinkEvent; -import javax.swing.event.HyperlinkEvent.EventType; -import javax.swing.event.HyperlinkListener; - -import net.sf.openrocket.startup.Application; - -public class SlideShowLinkListener implements HyperlinkListener { - - private final Window parent; - - public SlideShowLinkListener(Window parent) { - this.parent = parent; - } - - @Override - public void hyperlinkUpdate(HyperlinkEvent event) { - - if (event.getEventType() != EventType.ACTIVATED) { - return; - } - - URL url = event.getURL(); - if (url != null && (url.getProtocol().equalsIgnoreCase("http") || url.getProtocol().equals("https"))) { - - if (Desktop.isDesktopSupported()) { - try { - Desktop.getDesktop().browse(url.toURI()); - } catch (Exception e) { - // Ignore - } - } - - } else { - - String name = event.getDescription(); - try { - SlideSet ss = SlideSetManager.getSlideSetManager().getSlideSet(name); - - SlideShowDialog dialog = new SlideShowDialog(parent); - dialog.setSlideSet(ss, 0); - dialog.setVisible(true); - } catch (IllegalArgumentException e) { - Application.getExceptionHandler().handleErrorCondition("Guided tour '" + name + "' not found."); - } - - } - - } -} diff --git a/src/net/sf/openrocket/gui/help/tours/TextLineReader.java b/src/net/sf/openrocket/gui/help/tours/TextLineReader.java deleted file mode 100644 index fd3ddaf0..00000000 --- a/src/net/sf/openrocket/gui/help/tours/TextLineReader.java +++ /dev/null @@ -1,120 +0,0 @@ -package net.sf.openrocket.gui.help.tours; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; -import java.util.Iterator; -import java.util.NoSuchElementException; - -import net.sf.openrocket.util.BugException; - -/** - * Read from a Reader object one line at a time, ignoring blank lines, - * preceding and trailing whitespace and comment lines starting with '#'. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class TextLineReader implements Iterator<String> { - - private static final Charset UTF8 = Charset.forName("UTF-8"); - - - - private final BufferedReader reader; - - private String next = null; - - /** - * Read from an input stream with UTF-8 character encoding. - */ - public TextLineReader(InputStream inputStream) { - this(new InputStreamReader(inputStream, UTF8)); - } - - - /** - * Read from a reader. - */ - public TextLineReader(Reader reader) { - if (reader instanceof BufferedReader) { - this.reader = (BufferedReader) reader; - } else { - this.reader = new BufferedReader(reader); - } - } - - - /** - * Test whether the file has more lines available. - */ - @Override - public boolean hasNext() { - if (next != null) { - return true; - } - - try { - next = readLine(); - } catch (IOException e) { - throw new BugException(e); - } - - return next != null; - } - - - /** - * Retrieve the next non-blank, non-comment line. - */ - @Override - public String next() { - if (hasNext()) { - String ret = next; - next = null; - return ret; - } - - throw new NoSuchElementException("End of file reached"); - } - - - /** - * Peek what the next line would be. - */ - public String peek() { - if (hasNext()) { - return next; - } - - throw new NoSuchElementException("End of file reached"); - } - - - private String readLine() throws IOException { - - while (true) { - // Read the next line - String line = reader.readLine(); - if (line == null) { - return null; - } - - // Check whether to accept the line - line = line.trim(); - if (line.length() > 0 && line.charAt(0) != '#') { - return line; - } - } - - } - - - @Override - public void remove() { - throw new UnsupportedOperationException("Remove not supported"); - } - -} diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java deleted file mode 100644 index b6bab939..00000000 --- a/src/net/sf/openrocket/gui/main/BasicFrame.java +++ /dev/null @@ -1,1499 +0,0 @@ -package net.sf.openrocket.gui.main; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.file.GeneralRocketLoader; -import net.sf.openrocket.file.RocketLoadException; -import net.sf.openrocket.file.RocketLoader; -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.file.openrocket.OpenRocketSaver; -import net.sf.openrocket.file.rocksim.export.RocksimSaver; -import net.sf.openrocket.gui.StorageOptionChooser; -import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; -import net.sf.openrocket.gui.dialogs.AboutDialog; -import net.sf.openrocket.gui.dialogs.BugReportDialog; -import net.sf.openrocket.gui.dialogs.ComponentAnalysisDialog; -import net.sf.openrocket.gui.dialogs.DebugLogDialog; -import net.sf.openrocket.gui.dialogs.DetailDialog; -import net.sf.openrocket.gui.dialogs.ExampleDesignDialog; -import net.sf.openrocket.gui.dialogs.LicenseDialog; -import net.sf.openrocket.gui.dialogs.MotorDatabaseLoadingDialog; -import net.sf.openrocket.gui.dialogs.PrintDialog; -import net.sf.openrocket.gui.dialogs.ScaleDialog; -import net.sf.openrocket.gui.dialogs.SwingWorkerDialog; -import net.sf.openrocket.gui.dialogs.WarningDialog; -import net.sf.openrocket.gui.dialogs.optimization.GeneralOptimizationDialog; -import net.sf.openrocket.gui.dialogs.preferences.PreferencesDialog; -import net.sf.openrocket.gui.help.tours.GuidedTourSelectionDialog; -import net.sf.openrocket.gui.main.componenttree.ComponentTree; -import net.sf.openrocket.gui.scalefigure.RocketPanel; -import net.sf.openrocket.gui.util.FileHelper; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.gui.util.OpenFileWorker; -import net.sf.openrocket.gui.util.SaveFileWorker; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Stage; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MemoryManagement; -import net.sf.openrocket.util.MemoryManagement.MemoryData; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.TestRockets; - -import javax.swing.*; -import javax.swing.border.BevelBorder; -import javax.swing.border.TitledBorder; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.DefaultTreeSelectionModel; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLDecoder; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.ExecutionException; - -public class BasicFrame extends JFrame { - private static final LogHelper log = Application.getLogger(); - - /** - * The RocketLoader instance used for loading all rocket designs. - */ - private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader(); - - private static final RocketSaver ROCKET_SAVER = new OpenRocketSaver(); - - private static final Translator trans = Application.getTranslator(); - - public static final int COMPONENT_TAB = 0; - public static final int SIMULATION_TAB = 1; - - - /** - * List of currently open frames. When the list goes empty - * it is time to exit the application. - */ - private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>(); - - - /** - * Whether "New" and "Open" should replace this frame. - * Should be set to false on the first rocket modification. - */ - private boolean replaceable = false; - - - - private final OpenRocketDocument document; - private final Rocket rocket; - - private JTabbedPane tabbedPane; - private RocketPanel rocketpanel; - private ComponentTree tree = null; - - private final DocumentSelectionModel selectionModel; - private final TreeSelectionModel componentSelectionModel; - private final ListSelectionModel simulationSelectionModel; - - /** Actions available for rocket modifications */ - private final RocketActions actions; - - - - /** - * Sole constructor. Creates a new frame based on the supplied document - * and adds it to the current frames list. - * - * @param document the document to show. - */ - public BasicFrame(OpenRocketDocument document) { - log.debug("Instantiating new BasicFrame"); - - this.document = document; - this.rocket = document.getRocket(); - this.rocket.getDefaultConfiguration().setAllStages(); - - - // Set replaceable flag to false at first modification - rocket.addComponentChangeListener(new ComponentChangeListener() { - @Override - public void componentChanged(ComponentChangeEvent e) { - replaceable = false; - BasicFrame.this.rocket.removeComponentChangeListener(this); - } - }); - - - // Create the component tree selection model that will be used - componentSelectionModel = new DefaultTreeSelectionModel(); - componentSelectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); - - // Obtain the simulation selection model that will be used - SimulationPanel simulationPanel = new SimulationPanel(document); - simulationSelectionModel = simulationPanel.getSimulationListSelectionModel(); - - // Combine into a DocumentSelectionModel - selectionModel = new DocumentSelectionModel(document); - selectionModel.attachComponentTreeSelectionModel(componentSelectionModel); - selectionModel.attachSimulationListSelectionModel(simulationSelectionModel); - - - actions = new RocketActions(document, selectionModel, this); - - - log.debug("Constructing the BasicFrame UI"); - - // The main vertical split pane - JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true); - vertical.setResizeWeight(0.5); - this.add(vertical); - - - // The top tabbed pane - tabbedPane = new JTabbedPane(); - //// Rocket design - tabbedPane.addTab(trans.get("BasicFrame.tab.Rocketdesign"), null, designTab()); - //// Flight simulations - tabbedPane.addTab(trans.get("BasicFrame.tab.Flightsim"), null, simulationPanel); - - vertical.setTopComponent(tabbedPane); - - - - // Bottom segment, rocket figure - - rocketpanel = new RocketPanel(document); - vertical.setBottomComponent(rocketpanel); - - rocketpanel.setSelectionModel(tree.getSelectionModel()); - - - createMenu(); - - - rocket.addComponentChangeListener(new ComponentChangeListener() { - @Override - public void componentChanged(ComponentChangeEvent e) { - setTitle(); - } - }); - - setTitle(); - this.pack(); - - - // Set initial window size - Dimension size = Toolkit.getDefaultToolkit().getScreenSize(); - size.width = size.width * 9 / 10; - size.height = size.height * 9 / 10; - this.setSize(size); - - // Remember changed size - GUIUtil.rememberWindowSize(this); - - this.setLocationByPlatform(true); - - GUIUtil.setWindowIcons(this); - - this.validate(); - vertical.setDividerLocation(0.4); - setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); - addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - closeAction(); - } - }); - - frames.add(this); - log.debug("BasicFrame instantiation complete"); - } - - - /** - * Construct the "Rocket design" tab. This contains a horizontal split pane - * with the left component the design tree and the right component buttons - * for adding components. - */ - private JComponent designTab() { - JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); - horizontal.setResizeWeight(0.5); - - - // Upper-left segment, component tree - - JPanel panel = new JPanel(new MigLayout("fill, flowy", "", "[grow]")); - - tree = new ComponentTree(document); - tree.setSelectionModel(componentSelectionModel); - - // Remove JTree key events that interfere with menu accelerators - InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null); - im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null); - - - - // Double-click opens config dialog - MouseListener ml = new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - int selRow = tree.getRowForLocation(e.getX(), e.getY()); - TreePath selPath = tree.getPathForLocation(e.getX(), e.getY()); - if (selRow != -1) { - if ((e.getClickCount() == 2) && !ComponentConfigDialog.isDialogVisible()) { - // Double-click - RocketComponent c = (RocketComponent) selPath.getLastPathComponent(); - ComponentConfigDialog.showDialog(BasicFrame.this, - BasicFrame.this.document, c); - } - } - } - }; - tree.addMouseListener(ml); - - // Update dialog when selection is changed - componentSelectionModel.addTreeSelectionListener(new TreeSelectionListener() { - @Override - public void valueChanged(TreeSelectionEvent e) { - // Scroll tree to the selected item - TreePath path = componentSelectionModel.getSelectionPath(); - if (path == null) - return; - tree.scrollPathToVisible(path); - - if (!ComponentConfigDialog.isDialogVisible()) - return; - RocketComponent c = (RocketComponent) path.getLastPathComponent(); - ComponentConfigDialog.showDialog(BasicFrame.this, - BasicFrame.this.document, c); - } - }); - - // Place tree inside scroll pane - JScrollPane scroll = new JScrollPane(tree); - panel.add(scroll, "spany, grow, wrap"); - - - // Buttons - JButton button = new JButton(actions.getMoveUpAction()); - panel.add(button, "sizegroup buttons, aligny 65%"); - - button = new JButton(actions.getMoveDownAction()); - panel.add(button, "sizegroup buttons, aligny 0%"); - - button = new JButton(actions.getEditAction()); - panel.add(button, "sizegroup buttons"); - - button = new JButton(actions.getNewStageAction()); - panel.add(button, "sizegroup buttons"); - - button = new JButton(actions.getDeleteAction()); - button.setIcon(null); - button.setMnemonic(0); - panel.add(button, "sizegroup buttons"); - - horizontal.setLeftComponent(panel); - - - // Upper-right segment, component addition buttons - - panel = new JPanel(new MigLayout("fill, insets 0", "[0::]")); - - scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - scroll.setViewportView(new ComponentAddButtons(document, componentSelectionModel, - scroll.getViewport())); - scroll.setBorder(null); - scroll.setViewportBorder(null); - - TitledBorder border = BorderFactory.createTitledBorder(trans.get("BasicFrame.title.Addnewcomp")); - GUIUtil.changeFontStyle(border, Font.BOLD); - scroll.setBorder(border); - - panel.add(scroll, "grow"); - - horizontal.setRightComponent(panel); - - return horizontal; - } - - - - /** - * Return the currently selected rocket component, or <code>null</code> if none selected. - */ - private RocketComponent getSelectedComponent() { - TreePath path = componentSelectionModel.getSelectionPath(); - if (path == null) - return null; - tree.scrollPathToVisible(path); - - return (RocketComponent) path.getLastPathComponent(); - } - - - /** - * Creates the menu for the window. - */ - private void createMenu() { - JMenuBar menubar = new JMenuBar(); - JMenu menu; - JMenuItem item; - - //// File - menu = new JMenu(trans.get("main.menu.file")); - menu.setMnemonic(KeyEvent.VK_F); - //// File-handling related tasks - menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.desc")); - menubar.add(menu); - - //// New - item = new JMenuItem(trans.get("main.menu.file.new"), KeyEvent.VK_N); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK)); - item.setMnemonic(KeyEvent.VK_N); - //// Create a new rocket design - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.new.desc")); - item.setIcon(Icons.FILE_NEW); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("New... selected"); - newAction(); - if (replaceable) { - log.info("Closing previous window"); - closeAction(); - } - } - }); - menu.add(item); - - //// Open... - item = new JMenuItem(trans.get("main.menu.file.open"), KeyEvent.VK_O); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK)); - //// Open a rocket design - item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Openrocketdesign")); - item.setIcon(Icons.FILE_OPEN); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Open... selected"); - openAction(); - } - }); - menu.add(item); - - //// Open example... - item = new JMenuItem(trans.get("main.menu.file.openExample")); - //// Open an example rocket design - item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Openexamplerocketdesign")); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, - ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK)); - item.setIcon(Icons.FILE_OPEN_EXAMPLE); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Open example... selected"); - URL[] urls = ExampleDesignDialog.selectExampleDesigns(BasicFrame.this); - if (urls != null) { - for (URL u : urls) { - log.user("Opening example " + u); - open(u, BasicFrame.this); - } - } - } - }); - menu.add(item); - - menu.addSeparator(); - - //// Save - item = new JMenuItem(trans.get("main.menu.file.save"), KeyEvent.VK_S); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK)); - //// Save the current rocket design - item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.SavecurRocketdesign")); - item.setIcon(Icons.FILE_SAVE); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Save selected"); - saveAction(); - } - }); - menu.add(item); - - //// Save as... - item = new JMenuItem(trans.get("main.menu.file.saveAs"), KeyEvent.VK_A); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, - ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK)); - //// Save the current rocket design to a new file - item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.SavecurRocketdesnewfile")); - item.setIcon(Icons.FILE_SAVE_AS); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Save as... selected"); - saveAsAction(); - } - }); - menu.add(item); - - //// Print... - item = new JMenuItem(trans.get("main.menu.file.print"), KeyEvent.VK_P); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.CTRL_MASK)); - //// Print parts list and fin template - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.file.print.desc")); - item.setIcon(Icons.FILE_PRINT); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Print action selected"); - printAction(); - } - }); - menu.add(item); - - - menu.addSeparator(); - - //// Close - item = new JMenuItem(trans.get("main.menu.file.close"), KeyEvent.VK_C); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK)); - //// Close the current rocket design - item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Closedesign")); - item.setIcon(Icons.FILE_CLOSE); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Close selected"); - closeAction(); - } - }); - menu.add(item); - - menu.addSeparator(); - - //// Quit - item = new JMenuItem(trans.get("main.menu.file.quit"), KeyEvent.VK_Q); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK)); - //// Quit the program - item.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.item.Quitprogram")); - item.setIcon(Icons.FILE_QUIT); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Quit selected"); - quitAction(); - } - }); - menu.add(item); - - - - //// Edit - menu = new JMenu(trans.get("main.menu.edit")); - menu.setMnemonic(KeyEvent.VK_E); - //// Rocket editing - menu.getAccessibleContext().setAccessibleDescription(trans.get("BasicFrame.menu.Rocketedt")); - menubar.add(menu); - - - Action action = UndoRedoAction.newUndoAction(document); - item = new JMenuItem(action); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK)); - item.setMnemonic(KeyEvent.VK_U); - //// Undo the previous operation - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.undo.desc")); - - menu.add(item); - - action = UndoRedoAction.newRedoAction(document); - item = new JMenuItem(action); - item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK)); - item.setMnemonic(KeyEvent.VK_R); - //// Redo the previously undone operation - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.redo.desc")); - menu.add(item); - - menu.addSeparator(); - - - item = new JMenuItem(actions.getCutAction()); - menu.add(item); - - item = new JMenuItem(actions.getCopyAction()); - menu.add(item); - - item = new JMenuItem(actions.getPasteAction()); - menu.add(item); - - item = new JMenuItem(actions.getDeleteAction()); - menu.add(item); - - menu.addSeparator(); - - - - item = new JMenuItem(trans.get("main.menu.edit.resize")); - item.setIcon(Icons.EDIT_SCALE); - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.resize.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Scale... selected"); - ScaleDialog dialog = new ScaleDialog(document, getSelectedComponent(), BasicFrame.this); - dialog.setVisible(true); - dialog.dispose(); - } - }); - menu.add(item); - - - - //// Preferences - item = new JMenuItem(trans.get("main.menu.edit.preferences")); - item.setIcon(Icons.PREFERENCES); - //// Setup the application preferences - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.edit.preferences.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Preferences selected"); - PreferencesDialog.showPreferences(BasicFrame.this); - } - }); - menu.add(item); - - - - - //// Analyze - menu = new JMenu(trans.get("main.menu.analyze")); - menu.setMnemonic(KeyEvent.VK_A); - //// Analyzing the rocket - menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.desc")); - menubar.add(menu); - - //// Component analysis - item = new JMenuItem(trans.get("main.menu.analyze.componentAnalysis"), KeyEvent.VK_C); - //// Analyze the rocket components separately - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.componentAnalysis.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Component analysis selected"); - ComponentAnalysisDialog.showDialog(rocketpanel); - } - }); - menu.add(item); - - - item = new JMenuItem(trans.get("main.menu.analyze.optimization"), KeyEvent.VK_O); - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.analyze.optimization.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Rocket optimization selected"); - new GeneralOptimizationDialog(document, BasicFrame.this).setVisible(true); - } - }); - menu.add(item); - - - - //// Debug - // (shown if openrocket.debug.menu is defined) - if (System.getProperty("openrocket.debug.menu") != null) { - menubar.add(makeDebugMenu()); - } - - - - //// Help - - menu = new JMenu(trans.get("main.menu.help")); - menu.setMnemonic(KeyEvent.VK_H); - menu.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.desc")); - menubar.add(menu); - - - // Guided tours - - item = new JMenuItem(trans.get("main.menu.help.tours"), KeyEvent.VK_L); - // TODO: Icon - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.tours.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Guided tours selected"); - // FIXME: Singleton - new GuidedTourSelectionDialog(BasicFrame.this).setVisible(true); - } - }); - menu.add(item); - - menu.addSeparator(); - - - //// License - item = new JMenuItem(trans.get("main.menu.help.license"), KeyEvent.VK_L); - item.setIcon(Icons.HELP_LICENSE); - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.license.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("License selected"); - new LicenseDialog(BasicFrame.this).setVisible(true); - } - }); - menu.add(item); - - menu.addSeparator(); - - //// Bug report - item = new JMenuItem(trans.get("main.menu.help.bugReport"), KeyEvent.VK_B); - item.setIcon(Icons.HELP_BUG_REPORT); - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.bugReport.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Bug report selected"); - BugReportDialog.showBugReportDialog(BasicFrame.this); - } - }); - menu.add(item); - - //// Debug log - item = new JMenuItem(trans.get("main.menu.help.debugLog")); - item.setIcon(Icons.HELP_DEBUG_LOG); - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.debugLog.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Debug log selected"); - new DebugLogDialog(BasicFrame.this).setVisible(true); - } - }); - menu.add(item); - - menu.addSeparator(); - - //// About - item = new JMenuItem(trans.get("main.menu.help.about"), KeyEvent.VK_A); - item.setIcon(Icons.HELP_ABOUT); - item.getAccessibleContext().setAccessibleDescription(trans.get("main.menu.help.about.desc")); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("About selected"); - new AboutDialog(BasicFrame.this).setVisible(true); - } - }); - menu.add(item); - - - this.setJMenuBar(menubar); - } - - private JMenu makeDebugMenu() { - JMenu menu; - JMenuItem item; - - /* - * This menu is intentionally left untranslated. - */ - - //// Debug menu - menu = new JMenu("Debug"); - //// OpenRocket debugging tasks - menu.getAccessibleContext().setAccessibleDescription("OpenRocket debugging tasks"); - - //// What is this menu? - item = new JMenuItem("What is this menu?"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("What is this menu? selected"); - JOptionPane.showMessageDialog(BasicFrame.this, - new Object[] { - "The 'Debug' menu includes actions for testing and debugging " + - "OpenRocket.", " ", - "The menu is made visible by defining the system property " + - "'openrocket.debug.menu' when starting OpenRocket.", - "It should not be visible by default." }, - "Debug menu", JOptionPane.INFORMATION_MESSAGE); - } - }); - menu.add(item); - - menu.addSeparator(); - - //// Create test rocket - item = new JMenuItem("Create test rocket"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Create test rocket selected"); - JTextField field = new JTextField(); - int sel = JOptionPane.showOptionDialog(BasicFrame.this, new Object[] { - "Input text key to generate random rocket:", - field - }, "Generate random test rocket", JOptionPane.DEFAULT_OPTION, - JOptionPane.QUESTION_MESSAGE, null, new Object[] { - "Random", "OK" - }, "OK"); - - Rocket r; - if (sel == 0) { - r = new TestRockets(null).makeTestRocket(); - } else if (sel == 1) { - r = new TestRockets(field.getText()).makeTestRocket(); - } else { - return; - } - - OpenRocketDocument doc = new OpenRocketDocument(r); - doc.setSaved(true); - BasicFrame frame = new BasicFrame(doc); - frame.setVisible(true); - } - }); - menu.add(item); - - - - item = new JMenuItem("Create 'Iso-Haisu'"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Create Iso-Haisu selected"); - Rocket r = TestRockets.makeIsoHaisu(); - OpenRocketDocument doc = new OpenRocketDocument(r); - doc.setSaved(true); - BasicFrame frame = new BasicFrame(doc); - frame.setVisible(true); - } - }); - menu.add(item); - - - item = new JMenuItem("Create 'Big Blue'"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Create Big Blue selected"); - Rocket r = TestRockets.makeBigBlue(); - OpenRocketDocument doc = new OpenRocketDocument(r); - doc.setSaved(true); - BasicFrame frame = new BasicFrame(doc); - frame.setVisible(true); - } - }); - menu.add(item); - - menu.addSeparator(); - - - item = new JMenuItem("Memory statistics"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Memory statistics selected"); - - // Get discarded but remaining objects (this also runs System.gc multiple times) - List<MemoryData> objects = MemoryManagement.getRemainingCollectableObjects(); - StringBuilder sb = new StringBuilder(); - sb.append("Objects that should have been garbage-collected but have not been:\n"); - int count = 0; - for (MemoryData data : objects) { - Object o = data.getReference().get(); - if (o == null) - continue; - sb.append("Age ").append(System.currentTimeMillis() - data.getRegistrationTime()) - .append(" ms: ").append(o).append('\n'); - count++; - // Explicitly null the strong reference to avoid possibility of invisible references - o = null; - } - sb.append("Total: " + count); - - // Get basic memory stats - System.gc(); - long max = Runtime.getRuntime().maxMemory(); - long free = Runtime.getRuntime().freeMemory(); - long used = max - free; - String[] stats = new String[4]; - stats[0] = "Memory usage:"; - stats[1] = String.format(" Max memory: %.1f MB", max / 1024.0 / 1024.0); - stats[2] = String.format(" Used memory: %.1f MB (%.0f%%)", used / 1024.0 / 1024.0, 100.0 * used / max); - stats[3] = String.format(" Free memory: %.1f MB (%.0f%%)", free / 1024.0 / 1024.0, 100.0 * free / max); - - - DetailDialog.showDetailedMessageDialog(BasicFrame.this, stats, sb.toString(), - "Memory statistics", JOptionPane.INFORMATION_MESSAGE); - } - }); - menu.add(item); - - //// Exhaust memory - item = new JMenuItem("Exhaust memory"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Exhaust memory selected"); - LinkedList<byte[]> data = new LinkedList<byte[]>(); - int count = 0; - final int bytesPerArray = 10240; - try { - while (true) { - byte[] array = new byte[bytesPerArray]; - for (int i = 0; i < bytesPerArray; i++) { - array[i] = (byte) i; - } - data.add(array); - count++; - } - } catch (OutOfMemoryError error) { - data = null; - long size = bytesPerArray * (long) count; - String s = String.format("OutOfMemory occurred after %d iterations (approx. %.1f MB consumed)", - count, size / 1024.0 / 1024.0); - log.debug(s, error); - JOptionPane.showMessageDialog(BasicFrame.this, s); - } - } - }); - menu.add(item); - - - menu.addSeparator(); - - //// Exception here - item = new JMenuItem("Exception here"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Exception here selected"); - throw new RuntimeException("Testing exception from menu action listener"); - } - }); - menu.add(item); - - item = new JMenuItem("Exception from EDT"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Exception from EDT selected"); - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - throw new RuntimeException("Testing exception from " + - "later invoked EDT thread"); - } - }); - } - }); - menu.add(item); - - item = new JMenuItem("Exception from other thread"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Exception from other thread selected"); - new Thread() { - @Override - public void run() { - throw new RuntimeException("Testing exception from newly created thread"); - } - }.start(); - } - }); - menu.add(item); - - item = new JMenuItem("OutOfMemoryError here"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("OutOfMemoryError here selected"); - throw new OutOfMemoryError("Testing OutOfMemoryError from menu action listener"); - } - }); - menu.add(item); - - - menu.addSeparator(); - - - item = new JMenuItem("Test popup"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - log.user("Test popup selected"); - JPanel panel = new JPanel(); - panel.add(new JTextField(40)); - panel.add(new JSpinner()); - JPopupMenu popup = new JPopupMenu(); - popup.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED)); - popup.add(panel); - popup.show(BasicFrame.this, -50, 100); - } - }); - menu.add(item); - - - - - return menu; - } - - - /** - * Select the tab on the main pane. - * - * @param tab one of {@link #COMPONENT_TAB} or {@link #SIMULATION_TAB}. - */ - public void selectTab(int tab) { - tabbedPane.setSelectedIndex(tab); - } - - - - private void openAction() { - JFileChooser chooser = new JFileChooser(); - - chooser.addChoosableFileFilter(FileHelper.ALL_DESIGNS_FILTER); - chooser.addChoosableFileFilter(FileHelper.OPENROCKET_DESIGN_FILTER); - chooser.addChoosableFileFilter(FileHelper.ROCKSIM_DESIGN_FILTER); - chooser.setFileFilter(FileHelper.ALL_DESIGNS_FILTER); - - chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); - chooser.setMultiSelectionEnabled(true); - chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); - int option = chooser.showOpenDialog(this); - if (option != JFileChooser.APPROVE_OPTION) { - log.user("Decided not to open files, option=" + option); - return; - } - - ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); - - File[] files = chooser.getSelectedFiles(); - log.user("Opening files " + Arrays.toString(files)); - - for (File file : files) { - log.info("Opening file: " + file); - if (open(file, this)) { - - // Close previous window if replacing - if (replaceable && document.isSaved()) { - // We are replacing the frame, make new window have current location - BasicFrame newFrame = frames.get(frames.size() - 1); - newFrame.setLocation(this.getLocation()); - - log.info("Closing window because it is replaceable"); - closeAction(); - replaceable = false; - } - } - } - } - - - /** - * Open a file based on a URL. - * @param url the file to open. - * @param parent the parent window for dialogs. - * @return <code>true</code> if opened successfully. - */ - private static boolean open(URL url, BasicFrame parent) { - String filename = null; - - // First figure out the file name from the URL - - // Try using URI.getPath(); - try { - URI uri = url.toURI(); - filename = uri.getPath(); - } catch (URISyntaxException ignore) { - } - - // Try URL-decoding the URL - if (filename == null) { - try { - filename = URLDecoder.decode(url.toString(), "UTF-8"); - } catch (UnsupportedEncodingException ignore) { - } - } - - // Last resort - if (filename == null) { - filename = ""; - } - - // Remove path from filename - if (filename.lastIndexOf('/') >= 0) { - filename = filename.substring(filename.lastIndexOf('/') + 1); - } - - - // Open the file - log.info("Opening file from url=" + url + " filename=" + filename); - try { - InputStream is = url.openStream(); - if (open(is, filename, parent)) { - // Close previous window if replacing - if (parent.replaceable && parent.document.isSaved()) { - parent.closeAction(); - parent.replaceable = false; - } - } - } catch (IOException e) { - log.warn("Error opening file" + e); - JOptionPane.showMessageDialog(parent, - "An error occurred while opening the file " + filename, - "Error loading file", JOptionPane.ERROR_MESSAGE); - } - - return false; - } - - - /** - * Open the specified file from an InputStream in a new design frame. If an error - * occurs, an error dialog is shown and <code>false</code> is returned. - * - * @param stream the stream to load from. - * @param filename the file name to display in dialogs (not set to the document). - * @param parent the parent component for which a progress dialog is opened. - * @return whether the file was successfully loaded and opened. - */ - private static boolean open(InputStream stream, String filename, Window parent) { - OpenFileWorker worker = new OpenFileWorker(stream, ROCKET_LOADER); - return open(worker, filename, null, parent); - } - - - /** - * Open the specified file in a new design frame. If an error occurs, an error - * dialog is shown and <code>false</code> is returned. - * - * @param file the file to open. - * @param parent the parent component for which a progress dialog is opened. - * @return whether the file was successfully loaded and opened. - */ - public static boolean open(File file, Window parent) { - OpenFileWorker worker = new OpenFileWorker(file, ROCKET_LOADER); - return open(worker, file.getName(), file, parent); - } - - - /** - * Open the specified file using the provided worker. - * - * @param worker the OpenFileWorker that loads the file. - * @param filename the file name to display in dialogs. - * @param file the File to set the document to (may be null). - * @param parent - * @return - */ - private static boolean open(OpenFileWorker worker, String filename, File file, Window parent) { - - MotorDatabaseLoadingDialog.check(parent); - - // Open the file in a Swing worker thread - log.info("Starting OpenFileWorker"); - if (!SwingWorkerDialog.runWorker(parent, "Opening file", "Reading " + filename + "...", worker)) { - // User cancelled the operation - log.info("User cancelled the OpenFileWorker"); - return false; - } - - - // Handle the document - OpenRocketDocument doc = null; - try { - - doc = worker.get(); - - } catch (ExecutionException e) { - - Throwable cause = e.getCause(); - - if (cause instanceof FileNotFoundException) { - - log.warn("File not found", cause); - JOptionPane.showMessageDialog(parent, - "File not found: " + filename, - "Error opening file", JOptionPane.ERROR_MESSAGE); - return false; - - } else if (cause instanceof RocketLoadException) { - - log.warn("Error loading the file", cause); - JOptionPane.showMessageDialog(parent, - "Unable to open file '" + filename + "': " - + cause.getMessage(), - "Error opening file", JOptionPane.ERROR_MESSAGE); - return false; - - } else { - - throw new BugException("Unknown error when opening file", e); - - } - - } catch (InterruptedException e) { - throw new BugException("EDT was interrupted", e); - } - - if (doc == null) { - throw new BugException("Document loader returned null"); - } - - - // Show warnings - WarningSet warnings = worker.getRocketLoader().getWarnings(); - if (!warnings.isEmpty()) { - log.info("Warnings while reading file: " + warnings); - WarningDialog.showWarnings(parent, - new Object[] { - //// The following problems were encountered while opening - trans.get("BasicFrame.WarningDialog.txt1") + " " + filename + ".", - //// Some design features may not have been loaded correctly. - trans.get("BasicFrame.WarningDialog.txt2") - }, - //// Warnings while opening file - trans.get("BasicFrame.WarningDialog.title"), warnings); - } - - - // Set document state - doc.setFile(file); - doc.setSaved(true); - - - // Open the frame - log.debug("Opening new frame with the document"); - BasicFrame frame = new BasicFrame(doc); - frame.setVisible(true); - - return true; - } - - - - - - private boolean saveAction() { - File file = document.getFile(); - if (file == null) { - log.info("Document does not contain file, opening save as dialog instead"); - return saveAsAction(); - } - log.info("Saving document to " + file); - - // Saving RockSim designs is not supported - if (FileHelper.ROCKSIM_DESIGN_FILTER.accept(file)) { - file = new File(file.getAbsolutePath().replaceAll(".[rR][kK][tT](.[gG][zZ])?$", - ".ork")); - - log.info("Attempting to save in RockSim format, renaming to " + file); - int option = JOptionPane.showConfirmDialog(this, new Object[] { - "Saving designs in RockSim format is not supported.", - "Save in OpenRocket format instead (" + file.getName() + ")?" - }, "Save " + file.getName(), JOptionPane.YES_NO_OPTION, - JOptionPane.QUESTION_MESSAGE, null); - if (option != JOptionPane.YES_OPTION) { - log.user("User chose not to save"); - return false; - } - - document.setFile(file); - } - return saveAs(file); - } - - - private boolean saveAsAction() { - File file = null; - - // TODO: HIGH: what if *.rkt chosen? - StorageOptionChooser storageChooser = - new StorageOptionChooser(document, document.getDefaultStorageOptions()); - JFileChooser chooser = new JFileChooser(); - chooser.addChoosableFileFilter(FileHelper.OPENROCKET_DESIGN_FILTER); - chooser.addChoosableFileFilter(FileHelper.ROCKSIM_DESIGN_FILTER); - chooser.setFileFilter(FileHelper.OPENROCKET_DESIGN_FILTER); - chooser.setCurrentDirectory(((SwingPreferences) Application.getPreferences()).getDefaultDirectory()); - chooser.setAccessory(storageChooser); - if (document.getFile() != null) - chooser.setSelectedFile(document.getFile()); - - int option = chooser.showSaveDialog(BasicFrame.this); - if (option != JFileChooser.APPROVE_OPTION) { - log.user("User decided not to save, option=" + option); - return false; - } - - file = chooser.getSelectedFile(); - if (file == null) { - log.user("User did not select a file"); - return false; - } - - ((SwingPreferences) Application.getPreferences()).setDefaultDirectory(chooser.getCurrentDirectory()); - storageChooser.storeOptions(document.getDefaultStorageOptions()); - - if (chooser.getFileFilter().equals(FileHelper.OPENROCKET_DESIGN_FILTER)) { - file = FileHelper.ensureExtension(file, "ork"); - if (!FileHelper.confirmWrite(file, this)) { - return false; - } - - return saveAs(file); - } - else if (chooser.getFileFilter().equals(FileHelper.ROCKSIM_DESIGN_FILTER)) { - file = FileHelper.ensureExtension(file, "rkt"); - if (!FileHelper.confirmWrite(file, this)) { - return false; - } - - try { - new RocksimSaver().save(file, document); - return true; - } catch (IOException e) { - return false; - } - } - else { - return false; - } - } - - private boolean saveAs(File file) { - log.info("Saving document as " + file); - boolean saved = false; - - if (!StorageOptionChooser.verifyStorageOptions(document, this)) { - // User cancelled the dialog - log.user("User cancelled saving in storage options dialog"); - return false; - } - - - SaveFileWorker worker = new SaveFileWorker(document, file, ROCKET_SAVER); - - if (!SwingWorkerDialog.runWorker(this, "Saving file", - "Writing " + file.getName() + "...", worker)) { - - // User cancelled the save - log.user("User cancelled the save, deleting the file"); - file.delete(); - return false; - } - - try { - worker.get(); - document.setFile(file); - document.setSaved(true); - saved = true; - setTitle(); - } catch (ExecutionException e) { - - Throwable cause = e.getCause(); - - if (cause instanceof IOException) { - log.warn("An I/O error occurred while saving " + file, cause); - JOptionPane.showMessageDialog(this, new String[] { - "An I/O error occurred while saving:", - e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE); - return false; - } else { - Reflection.handleWrappedException(e); - } - - } catch (InterruptedException e) { - throw new BugException("EDT was interrupted", e); - } - - return saved; - } - - - private boolean closeAction() { - if (!document.isSaved()) { - log.info("Confirming whether to save the design"); - ComponentConfigDialog.hideDialog(); - int result = JOptionPane.showConfirmDialog(this, - trans.get("BasicFrame.dlg.lbl1") + rocket.getName() + - trans.get("BasicFrame.dlg.lbl2") + " " + - trans.get("BasicFrame.dlg.lbl3"), - trans.get("BasicFrame.dlg.title"), JOptionPane.YES_NO_CANCEL_OPTION, - JOptionPane.QUESTION_MESSAGE); - if (result == JOptionPane.YES_OPTION) { - // Save - log.user("User requested file save"); - if (!saveAction()) { - log.info("File save was interrupted, not closing"); - return false; - } - } else if (result == JOptionPane.NO_OPTION) { - // Don't save: No-op - log.user("User requested to discard design"); - } else { - // Cancel or close - log.user("User cancelled closing, result=" + result); - return false; - } - } - - // Rocket has been saved or discarded - log.debug("Disposing window"); - this.dispose(); - - ComponentConfigDialog.hideDialog(); - ComponentAnalysisDialog.hideDialog(); - - frames.remove(this); - if (frames.isEmpty()) { - log.info("Last frame closed, exiting"); - System.exit(0); - } - return true; - } - - - - /** - * - */ - public void printAction() { - new PrintDialog(this, document).setVisible(true); - } - - /** - * Open a new design window with a basic rocket+stage. - */ - public static void newAction() { - log.info("New action initiated"); - - Rocket rocket = new Rocket(); - Stage stage = new Stage(); - //// Sustainer - stage.setName(trans.get("BasicFrame.StageName.Sustainer")); - rocket.addChild(stage); - OpenRocketDocument doc = new OpenRocketDocument(rocket); - doc.setSaved(true); - - BasicFrame frame = new BasicFrame(doc); - frame.replaceable = true; - frame.setVisible(true); - ComponentConfigDialog.showDialog(frame, doc, rocket); - } - - /** - * Quit the application. Confirms saving unsaved designs. The action of File->Quit. - */ - public static void quitAction() { - log.info("Quit action initiated"); - for (int i = frames.size() - 1; i >= 0; i--) { - log.debug("Closing frame " + frames.get(i)); - if (!frames.get(i).closeAction()) { - // Close canceled - log.info("Quit was cancelled"); - return; - } - } - // Should not be reached, but just in case - log.error("Should already have exited application"); - System.exit(0); - } - - - /** - * Set the title of the frame, taking into account the name of the rocket, file it - * has been saved to (if any) and saved status. - */ - private void setTitle() { - File file = document.getFile(); - boolean saved = document.isSaved(); - String title; - - title = rocket.getName(); - if (file != null) { - title = title + " (" + file.getName() + ")"; - } - if (!saved) - title = "*" + title; - - setTitle(title); - } - - - - /** - * Find a currently open BasicFrame containing the specified rocket. This method - * can be used to map a Rocket to a BasicFrame from GUI methods. - * - * @param rocket the Rocket. - * @return the corresponding BasicFrame, or <code>null</code> if none found. - */ - public static BasicFrame findFrame(Rocket rocket) { - for (BasicFrame f : frames) { - if (f.rocket == rocket) { - log.debug("Found frame " + f + " for rocket " + rocket); - return f; - } - } - log.debug("Could not find frame for rocket " + rocket); - return null; - } - - /** - * Find a currently open document by the rocket object. This method can be used - * to map a Rocket to OpenRocketDocument from GUI methods. - * - * @param rocket the Rocket. - * @return the corresponding OpenRocketDocument, or <code>null</code> if not found. - */ - public static OpenRocketDocument findDocument(Rocket rocket) { - BasicFrame frame = findFrame(rocket); - if (frame != null) { - return frame.document; - } else { - return null; - } - } -} diff --git a/src/net/sf/openrocket/gui/main/ClipboardListener.java b/src/net/sf/openrocket/gui/main/ClipboardListener.java deleted file mode 100644 index 87fd4456..00000000 --- a/src/net/sf/openrocket/gui/main/ClipboardListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.sf.openrocket.gui.main; - -public interface ClipboardListener { - - public void clipboardChanged(); - -} diff --git a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java b/src/net/sf/openrocket/gui/main/ComponentAddButtons.java deleted file mode 100644 index fa92a671..00000000 --- a/src/net/sf/openrocket/gui/main/ComponentAddButtons.java +++ /dev/null @@ -1,651 +0,0 @@ -package net.sf.openrocket.gui.main; - - -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Rectangle; -import java.awt.event.ActionEvent; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -import javax.swing.Icon; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JViewport; -import javax.swing.Scrollable; -import javax.swing.SwingConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; -import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.BodyComponent; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.ShockCord; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.Preferences; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Pair; -import net.sf.openrocket.util.Reflection; - -/** - * A component that contains addition buttons to add different types of rocket components - * to a rocket. It enables and disables buttons according to the current selection of a - * TreeSelectionModel. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class ComponentAddButtons extends JPanel implements Scrollable { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private static final int ROWS = 3; - private static final int MAXCOLS = 6; - private static final String BUTTONPARAM = "grow, sizegroup buttons"; - - private static final int GAP = 5; - private static final int EXTRASPACE = 0; - - private final ComponentButton[][] buttons; - - private final OpenRocketDocument document; - private final TreeSelectionModel selectionModel; - private final JViewport viewport; - private final MigLayout layout; - - private final int width, height; - - - public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model, - JViewport viewport) { - - super(); - String constaint = "[min!]"; - for (int i = 1; i < MAXCOLS; i++) - constaint = constaint + GAP + "[min!]"; - - layout = new MigLayout("fill", constaint); - setLayout(layout); - this.document = document; - this.selectionModel = model; - this.viewport = viewport; - - buttons = new ComponentButton[ROWS][]; - int row = 0; - - //////////////////////////////////////////// - - //// Body components and fin sets - addButtonRow(trans.get("compaddbuttons.Bodycompandfinsets"), row, - //// Nose cone - new BodyComponentButton(NoseCone.class, trans.get("compaddbuttons.Nosecone")), - //// Body tube - new BodyComponentButton(BodyTube.class, trans.get("compaddbuttons.Bodytube")), - //// Transition - new BodyComponentButton(Transition.class, trans.get("compaddbuttons.Transition")), - //// Trapezoidal - new FinButton(TrapezoidFinSet.class, trans.get("compaddbuttons.Trapezoidal")), // TODO: MEDIUM: freer fin placing - //// Elliptical - new FinButton(EllipticalFinSet.class, trans.get("compaddbuttons.Elliptical")), - //// Freeform - new FinButton(FreeformFinSet.class, trans.get("compaddbuttons.Freeform")), - //// Launch lug - new FinButton(LaunchLug.class, trans.get("compaddbuttons.Launchlug"))); - - row++; - - - ///////////////////////////////////////////// - - //// Inner component - addButtonRow(trans.get("compaddbuttons.Innercomponent"), row, - //// Inner tube - new ComponentButton(InnerTube.class, trans.get("compaddbuttons.Innertube")), - //// Coupler - new ComponentButton(TubeCoupler.class, trans.get("compaddbuttons.Coupler")), - //// Centering\nring - new ComponentButton(CenteringRing.class, trans.get("compaddbuttons.Centeringring")), - //// Bulkhead - new ComponentButton(Bulkhead.class, trans.get("compaddbuttons.Bulkhead")), - //// Engine\nblock - new ComponentButton(EngineBlock.class, trans.get("compaddbuttons.Engineblock"))); - - row++; - - //////////////////////////////////////////// - - //// Mass objects - addButtonRow(trans.get("compaddbuttons.Massobjects"), row, - //// Parachute - new ComponentButton(Parachute.class, trans.get("compaddbuttons.Parachute")), - //// Streamer - new ComponentButton(Streamer.class, trans.get("compaddbuttons.Streamer")), - //// Shock cord - new ComponentButton(ShockCord.class, trans.get("compaddbuttons.Shockcord")), - // new ComponentButton("Motor clip"), - // new ComponentButton("Payload"), - //// Mass\ncomponent - new ComponentButton(MassComponent.class, trans.get("compaddbuttons.Masscomponent"))); - - - // Get maximum button size - int w = 0, h = 0; - - for (row = 0; row < buttons.length; row++) { - for (int col = 0; col < buttons[row].length; col++) { - Dimension d = buttons[row][col].getPreferredSize(); - if (d.width > w) - w = d.width; - if (d.height > h) - h = d.height; - } - } - - // Set all buttons to maximum size - width = w; - height = h; - Dimension d = new Dimension(width, height); - for (row = 0; row < buttons.length; row++) { - for (int col = 0; col < buttons[row].length; col++) { - buttons[row][col].setMinimumSize(d); - buttons[row][col].setPreferredSize(d); - buttons[row][col].getComponent(0).validate(); - } - } - - // Add viewport listener if viewport provided - if (viewport != null) { - viewport.addChangeListener(new ChangeListener() { - private int oldWidth = -1; - - public void stateChanged(ChangeEvent e) { - Dimension d = ComponentAddButtons.this.viewport.getExtentSize(); - if (d.width != oldWidth) { - oldWidth = d.width; - flowButtons(); - } - } - }); - } - - add(new JPanel(), "grow"); - } - - - /** - * Adds a row of buttons to the panel. - * @param label Label placed before the row - * @param row Row number - * @param b List of ComponentButtons to place on the row - */ - private void addButtonRow(String label, int row, ComponentButton... b) { - if (row > 0) - add(new JLabel(label), "span, gaptop unrel, wrap"); - else - add(new JLabel(label), "span, gaptop 0, wrap"); - - int col = 0; - buttons[row] = new ComponentButton[b.length]; - - for (int i = 0; i < b.length; i++) { - buttons[row][col] = b[i]; - if (i < b.length - 1) - add(b[i], BUTTONPARAM); - else - add(b[i], BUTTONPARAM + ", wrap"); - col++; - } - } - - - /** - * Flows the buttons in all rows of the panel. If a button would come too close - * to the right edge of the viewport, "newline" is added to its constraints flowing - * it to the next line. - */ - private void flowButtons() { - if (viewport == null) - return; - - int w; - - Dimension d = viewport.getExtentSize(); - - for (int row = 0; row < buttons.length; row++) { - w = 0; - for (int col = 0; col < buttons[row].length; col++) { - w += GAP + width; - String param = BUTTONPARAM + ",width " + width + "!,height " + height + "!"; - - if (w + EXTRASPACE > d.width) { - param = param + ",newline"; - w = GAP + width; - } - if (col == buttons[row].length - 1) - param = param + ",wrap"; - layout.setComponentConstraints(buttons[row][col], param); - } - } - revalidate(); - } - - - - /** - * Class for a component button. - */ - private class ComponentButton extends JButton implements TreeSelectionListener { - protected Class<? extends RocketComponent> componentClass = null; - private Constructor<? extends RocketComponent> constructor = null; - - /** Only label, no icon. */ - public ComponentButton(String text) { - this(text, null, null); - } - - /** - * Constructor with icon and label. The icon and label are placed into the button. - * The label may contain "\n" as a newline. - */ - public ComponentButton(String text, Icon enabled, Icon disabled) { - super(); - setLayout(new MigLayout("fill, flowy, insets 0, gap 0", "", "")); - - add(new JLabel(), "push, sizegroup spacing"); - - // Add Icon - if (enabled != null) { - JLabel label = new JLabel(enabled); - if (disabled != null) - label.setDisabledIcon(disabled); - add(label, "growx"); - } - - // Add labels - String[] l = text.split("\n"); - for (int i = 0; i < l.length; i++) { - add(new StyledLabel(l[i], SwingConstants.CENTER, -3.0f), "growx"); - } - - add(new JLabel(), "push, sizegroup spacing"); - - valueChanged(null); // Update enabled status - selectionModel.addTreeSelectionListener(this); - } - - - /** - * Main constructor that should be used. The generated component type is specified - * and the text. The icons are fetched based on the component type. - */ - public ComponentButton(Class<? extends RocketComponent> c, String text) { - this(text, ComponentIcons.getLargeIcon(c), ComponentIcons.getLargeDisabledIcon(c)); - - if (c == null) - return; - - componentClass = c; - - try { - constructor = c.getConstructor(); - } catch (NoSuchMethodException e) { - throw new IllegalArgumentException("Unable to get default " + - "constructor for class " + c, e); - } - } - - - /** - * Return whether the current component is addable when the component c is selected. - * c is null if there is no selection. The default is to use c.isCompatible(class). - */ - public boolean isAddable(RocketComponent c) { - if (c == null) - return false; - if (componentClass == null) - return false; - return c.isCompatible(componentClass); - } - - /** - * Return the position to add the component if component c is selected currently. - * The first element of the returned array is the RocketComponent to add the component - * to, and the second (if non-null) an Integer telling the position of the component. - * A return value of null means that the user cancelled addition of the component. - * If the Integer is null, the component is added at the end of the sibling - * list. By default returns the end of the currently selected component. - * - * @param c The component currently selected - * @return The position to add the new component to, or null if should not add. - */ - public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) { - return new Pair<RocketComponent, Integer>(c, null); - } - - /** - * Updates the enabled status of the button. - * TODO: LOW: What about updates to the rocket tree? - */ - public void valueChanged(TreeSelectionEvent e) { - updateEnabled(); - } - - /** - * Sets the enabled status of the button and all subcomponents. - */ - @Override - public void setEnabled(boolean enabled) { - super.setEnabled(enabled); - Component[] c = getComponents(); - for (int i = 0; i < c.length; i++) - c[i].setEnabled(enabled); - } - - - /** - * Update the enabled status of the button. - */ - private void updateEnabled() { - RocketComponent c = null; - TreePath p = selectionModel.getSelectionPath(); - if (p != null) - c = (RocketComponent) p.getLastPathComponent(); - setEnabled(isAddable(c)); - } - - - @Override - protected void fireActionPerformed(ActionEvent event) { - super.fireActionPerformed(event); - log.user("Adding component of type " + componentClass.getSimpleName()); - RocketComponent c = null; - Integer position = null; - - TreePath p = selectionModel.getSelectionPath(); - if (p != null) - c = (RocketComponent) p.getLastPathComponent(); - - Pair<RocketComponent, Integer> pos = getAdditionPosition(c); - if (pos == null) { - // Cancel addition - log.info("No position to add component"); - return; - } - c = pos.getU(); - position = pos.getV(); - - - if (c == null) { - // Should not occur - Application.getExceptionHandler().handleErrorCondition("ERROR: Could not place new component."); - updateEnabled(); - return; - } - - if (constructor == null) { - Application.getExceptionHandler().handleErrorCondition("ERROR: Construction of type not supported yet."); - return; - } - - RocketComponent component; - try { - component = (RocketComponent) constructor.newInstance(); - } catch (InstantiationException e) { - throw new BugException("Could not construct new instance of class " + constructor, e); - } catch (IllegalAccessException e) { - throw new BugException("Could not construct new instance of class " + constructor, e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - - // Next undo position is set by opening the configuration dialog - document.addUndoPosition("Add " + component.getComponentName()); - - log.info("Adding component " + component.getComponentName() + " to component " + c.getComponentName() + - " position=" + position); - - if (position == null) - c.addChild(component); - else - c.addChild(component, position); - - // Select new component and open config dialog - selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component)); - - JFrame parent = null; - for (Component comp = ComponentAddButtons.this; comp != null; comp = comp.getParent()) { - if (comp instanceof JFrame) { - parent = (JFrame) comp; - break; - } - } - - ComponentConfigDialog.showDialog(parent, document, component); - } - } - - /** - * A class suitable for BodyComponents. Addition is allowed ... - */ - private class BodyComponentButton extends ComponentButton { - - public BodyComponentButton(Class<? extends RocketComponent> c, String text) { - super(c, text); - } - - public BodyComponentButton(String text, Icon enabled, Icon disabled) { - super(text, enabled, disabled); - } - - public BodyComponentButton(String text) { - super(text); - } - - @Override - public boolean isAddable(RocketComponent c) { - if (super.isAddable(c)) - return true; - // Handled separately: - if (c instanceof BodyComponent) - return true; - if (c == null || c instanceof Rocket) - return true; - return false; - } - - @Override - public Pair<RocketComponent, Integer> getAdditionPosition(RocketComponent c) { - if (super.isAddable(c)) // Handled automatically - return super.getAdditionPosition(c); - - - if (c == null || c instanceof Rocket) { - // Add as last body component of the last stage - Rocket rocket = document.getRocket(); - return new Pair<RocketComponent, Integer>(rocket.getChild(rocket.getStageCount() - 1), - null); - } - - if (!(c instanceof BodyComponent)) - return null; - RocketComponent parent = c.getParent(); - if (parent == null) { - throw new BugException("Component " + c.getComponentName() + " is the root component, " + - "componentClass=" + componentClass); - } - - // Check whether to insert between or at the end. - // 0 = ask, 1 = in between, 2 = at the end - int pos = Application.getPreferences().getChoice(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0); - if (pos == 0) { - if (parent.getChildPosition(c) == parent.getChildCount() - 1) - pos = 2; // Selected component is the last component - else - pos = askPosition(); - } - - switch (pos) { - case 0: - // Cancel - return null; - case 1: - // Insert after current position - return new Pair<RocketComponent, Integer>(parent, parent.getChildPosition(c) + 1); - case 2: - // Insert at the end of the parent - return new Pair<RocketComponent, Integer>(parent, null); - default: - Application.getExceptionHandler().handleErrorCondition("ERROR: Bad position type: " + pos); - return null; - } - } - - private int askPosition() { - //// Insert here - //// Add to the end - //// Cancel - Object[] options = { trans.get("compaddbuttons.askPosition.Inserthere"), - trans.get("compaddbuttons.askPosition.Addtotheend"), - trans.get("compaddbuttons.askPosition.Cancel") }; - - JPanel panel = new JPanel(new MigLayout()); - //// Do not ask me again - JCheckBox check = new JCheckBox(trans.get("compaddbuttons.Donotaskmeagain")); - panel.add(check, "wrap"); - //// You can change the default operation in the preferences. - panel.add(new StyledLabel(trans.get("compaddbuttons.lbl.Youcanchange"), -2)); - - int sel = JOptionPane.showOptionDialog(null, // parent component - //// Insert the component after the current component or as the last component? - new Object[] { - trans.get("compaddbuttons.lbl.insertcomp"), - panel }, - //// Select component position - trans.get("compaddbuttons.Selectcomppos"), // title - JOptionPane.DEFAULT_OPTION, // default selections - JOptionPane.QUESTION_MESSAGE, // dialog type - null, // icon - options, // options - options[0]); // initial value - - switch (sel) { - case JOptionPane.CLOSED_OPTION: - case 2: - // Cancel - return 0; - case 0: - // Insert - sel = 1; - break; - case 1: - // Add - sel = 2; - break; - default: - Application.getExceptionHandler().handleErrorCondition("ERROR: JOptionPane returned " + sel); - return 0; - } - - if (check.isSelected()) { - // Save the preference - Application.getPreferences().putInt(Preferences.BODY_COMPONENT_INSERT_POSITION_KEY, sel); - } - return sel; - } - - } - - - - /** - * Class for fin sets, that attach only to BodyTubes. - */ - private class FinButton extends ComponentButton { - public FinButton(Class<? extends RocketComponent> c, String text) { - super(c, text); - } - - public FinButton(String text, Icon enabled, Icon disabled) { - super(text, enabled, disabled); - } - - public FinButton(String text) { - super(text); - } - - @Override - public boolean isAddable(RocketComponent c) { - if (c == null) - return false; - return (c.getClass().equals(BodyTube.class)); - } - } - - - - ///////// Scrolling functionality - - @Override - public Dimension getPreferredScrollableViewportSize() { - return getPreferredSize(); - } - - - @Override - public int getScrollableBlockIncrement(Rectangle visibleRect, - int orientation, int direction) { - if (orientation == SwingConstants.VERTICAL) - return visibleRect.height * 8 / 10; - return 10; - } - - - @Override - public boolean getScrollableTracksViewportHeight() { - return false; - } - - - @Override - public boolean getScrollableTracksViewportWidth() { - return true; - } - - - @Override - public int getScrollableUnitIncrement(Rectangle visibleRect, - int orientation, int direction) { - return 10; - } - -} diff --git a/src/net/sf/openrocket/gui/main/ComponentIcons.java b/src/net/sf/openrocket/gui/main/ComponentIcons.java deleted file mode 100644 index b0071caa..00000000 --- a/src/net/sf/openrocket/gui/main/ComponentIcons.java +++ /dev/null @@ -1,183 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URL; -import java.util.HashMap; - -import javax.imageio.ImageIO; -import javax.swing.Icon; -import javax.swing.ImageIcon; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.EngineBlock; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.ShockCord; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.startup.Application; - - -public class ComponentIcons { - private static final Translator trans = Application.getTranslator(); - - private static final String ICON_DIRECTORY = "pix/componenticons/"; - private static final String SMALL_SUFFIX = "-small.png"; - private static final String LARGE_SUFFIX = "-large.png"; - - private static final HashMap<Class<?>, ImageIcon> SMALL_ICONS = - new HashMap<Class<?>, ImageIcon>(); - private static final HashMap<Class<?>, ImageIcon> LARGE_ICONS = - new HashMap<Class<?>, ImageIcon>(); - private static final HashMap<Class<?>, ImageIcon> DISABLED_ICONS = - new HashMap<Class<?>, ImageIcon>(); - - static { - //// Nose cone - load("nosecone", trans.get("ComponentIcons.Nosecone"), NoseCone.class); - //// Body tube - load("bodytube", trans.get("ComponentIcons.Bodytube"), BodyTube.class); - //// Transition - load("transition", trans.get("ComponentIcons.Transition"), Transition.class); - //// Trapezoidal fin set - load("trapezoidfin", trans.get("ComponentIcons.Trapezoidalfinset"), TrapezoidFinSet.class); - //// Elliptical fin set - load("ellipticalfin", trans.get("ComponentIcons.Ellipticalfinset"), EllipticalFinSet.class); - //// Freeform fin set - load("freeformfin", trans.get("ComponentIcons.Freeformfinset"), FreeformFinSet.class); - //// Launch lug - load("launchlug", trans.get("ComponentIcons.Launchlug"), LaunchLug.class); - //// Inner tube - load("innertube", trans.get("ComponentIcons.Innertube"), InnerTube.class); - //// Tube coupler - load("tubecoupler", trans.get("ComponentIcons.Tubecoupler"), TubeCoupler.class); - //// Centering ring - load("centeringring", trans.get("ComponentIcons.Centeringring"), CenteringRing.class); - //// Bulk head - load("bulkhead", trans.get("ComponentIcons.Bulkhead"), Bulkhead.class); - //// Engine block - load("engineblock", trans.get("ComponentIcons.Engineblock"), EngineBlock.class); - //// Parachute - load("parachute", trans.get("ComponentIcons.Parachute"), Parachute.class); - //// Streamer - load("streamer", trans.get("ComponentIcons.Streamer"), Streamer.class); - //// Shock cord - load("shockcord", trans.get("ComponentIcons.Shockcord"), ShockCord.class); - //// Mass component - load("mass", trans.get("ComponentIcons.Masscomponent"), MassComponent.class); - } - - private static void load(String filename, String name, Class<?> componentClass) { - ImageIcon icon = loadSmall(ICON_DIRECTORY + filename + SMALL_SUFFIX, name); - SMALL_ICONS.put(componentClass, icon); - - ImageIcon[] icons = loadLarge(ICON_DIRECTORY + filename + LARGE_SUFFIX, name); - LARGE_ICONS.put(componentClass, icons[0]); - DISABLED_ICONS.put(componentClass, icons[1]); - } - - - /** - * Return the small icon for a component type. - * - * @param c the component class. - * @return the icon, or <code>null</code> if none available. - */ - public static Icon getSmallIcon(Class<?> c) { - return SMALL_ICONS.get(c); - } - - /** - * Return the large icon for a component type. - * - * @param c the component class. - * @return the icon, or <code>null</code> if none available. - */ - public static Icon getLargeIcon(Class<?> c) { - return LARGE_ICONS.get(c); - } - - /** - * Return the large disabled icon for a component type. - * - * @param c the component class. - * @return the icon, or <code>null</code> if none available. - */ - public static Icon getLargeDisabledIcon(Class<?> c) { - return DISABLED_ICONS.get(c); - } - - - - - private static ImageIcon loadSmall(String file, String desc) { - URL url = ClassLoader.getSystemResource(file); - if (url == null) { - Application.getExceptionHandler().handleErrorCondition("ERROR: Couldn't find file: " + file); - return null; - } - return new ImageIcon(url, desc); - } - - - private static ImageIcon[] loadLarge(String file, String desc) { - ImageIcon[] icons = new ImageIcon[2]; - - URL url = ClassLoader.getSystemResource(file); - if (url != null) { - BufferedImage bi, bi2; - try { - bi = ImageIO.read(url); - bi2 = ImageIO.read(url); // How the fsck can one duplicate a BufferedImage??? - } catch (IOException e) { - Application.getExceptionHandler().handleErrorCondition("ERROR: Couldn't read file: " + file, e); - return new ImageIcon[] { null, null }; - } - - icons[0] = new ImageIcon(bi, desc); - - // Create disabled icon - if (false) { // Fade using alpha - - int rgb[] = bi2.getRGB(0, 0, bi2.getWidth(), bi2.getHeight(), null, 0, bi2.getWidth()); - for (int i = 0; i < rgb.length; i++) { - final int alpha = (rgb[i] >> 24) & 0xFF; - rgb[i] = (rgb[i] & 0xFFFFFF) | (alpha / 3) << 24; - - //rgb[i] = (rgb[i]&0xFFFFFF) | ((rgb[i]>>1)&0x3F000000); - } - bi2.setRGB(0, 0, bi2.getWidth(), bi2.getHeight(), rgb, 0, bi2.getWidth()); - - } else { // Raster alpha - - for (int x = 0; x < bi.getWidth(); x++) { - for (int y = 0; y < bi.getHeight(); y++) { - if ((x + y) % 2 == 0) { - bi2.setRGB(x, y, 0); - } - } - } - - } - - //// (disabled) - icons[1] = new ImageIcon(bi2, desc + " " + trans.get("ComponentIcons.disabled")); - - return icons; - } else { - Application.getExceptionHandler().handleErrorCondition("ERROR: Couldn't find file: " + file); - return new ImageIcon[] { null, null }; - } - } -} diff --git a/src/net/sf/openrocket/gui/main/DocumentSelectionListener.java b/src/net/sf/openrocket/gui/main/DocumentSelectionListener.java deleted file mode 100644 index 055f1116..00000000 --- a/src/net/sf/openrocket/gui/main/DocumentSelectionListener.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.sf.openrocket.gui.main; - -public interface DocumentSelectionListener { - - public static final int COMPONENT_SELECTION_CHANGE = 1; - public static final int SIMULATION_SELECTION_CHANGE = 2; - - /** - * Called when the selection changes. - * - * @param changeType a bitmask of the type of change. - */ - public void valueChanged(int changeType); - -} diff --git a/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java b/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java deleted file mode 100644 index 409620cb..00000000 --- a/src/net/sf/openrocket/gui/main/DocumentSelectionModel.java +++ /dev/null @@ -1,211 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.swing.ListSelectionModel; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; -import net.sf.openrocket.rocketcomponent.RocketComponent; - -public class DocumentSelectionModel { - - private static final Simulation[] NO_SIMULATION = new Simulation[0]; - - private final ComponentTreeSelectionListener componentTreeSelectionListener = - new ComponentTreeSelectionListener(); - private final SimulationListSelectionListener simulationListSelectionListener = - new SimulationListSelectionListener(); - - - private final OpenRocketDocument document; - - private RocketComponent componentSelection = null; - private Simulation[] simulationSelection = NO_SIMULATION; - - private TreeSelectionModel componentTreeSelectionModel = null; - private ListSelectionModel simulationListSelectionModel = null; - - private final List<DocumentSelectionListener> listeners = - new ArrayList<DocumentSelectionListener>(); - - - - public DocumentSelectionModel(OpenRocketDocument document) { - this.document = document; - } - - - - - /** - * Return the currently selected simulations. Returns an empty array if none - * are selected. - * - * @return an array of the currently selected simulations, may be of zero length. - */ - public Simulation[] getSelectedSimulations() { - return Arrays.copyOf(simulationSelection, simulationSelection.length); - } - - public void setSelectedSimulations(Simulation[] sims) { - simulationSelection = sims; - clearComponentSelection(); - - simulationListSelectionModel.clearSelection(); - for (Simulation s: sims) { - int index = document.getSimulationIndex(s); - if (index >= 0) { - simulationListSelectionModel.addSelectionInterval(index, index); - } - } - } - - /** - * Return the currently selected rocket component. Returns <code>null</code> - * if no rocket component is selected. - * - * @return the currently selected rocket component, or <code>null</code>. - */ - public RocketComponent getSelectedComponent() { - return componentSelection; - } - - public void setSelectedComponent(RocketComponent component) { - componentSelection = component; - clearSimulationSelection(); - - TreePath path = ComponentTreeModel.makeTreePath(component); - componentTreeSelectionModel.setSelectionPath(path); - } - - - - - - public void attachComponentTreeSelectionModel(TreeSelectionModel model) { - if (componentTreeSelectionModel != null) - componentTreeSelectionModel.removeTreeSelectionListener( - componentTreeSelectionListener); - - componentTreeSelectionModel = model; - if (model != null) - model.addTreeSelectionListener(componentTreeSelectionListener); - clearComponentSelection(); - } - - - - public void attachSimulationListSelectionModel(ListSelectionModel model) { - if (simulationListSelectionModel != null) - simulationListSelectionModel.removeListSelectionListener( - simulationListSelectionListener); - - simulationListSelectionModel = model; - if (model != null) - model.addListSelectionListener(simulationListSelectionListener); - clearSimulationSelection(); - } - - - - public void clearSimulationSelection() { - if (simulationSelection.length == 0) - return; - - simulationSelection = NO_SIMULATION; - if (simulationListSelectionModel != null) - simulationListSelectionModel.clearSelection(); - - fireDocumentSelection(DocumentSelectionListener.SIMULATION_SELECTION_CHANGE); - } - - - public void clearComponentSelection() { - if (componentSelection == null) - return; - - componentSelection = null; - if (componentTreeSelectionModel != null) - componentTreeSelectionModel.clearSelection(); - - fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE); - } - - - - public void addDocumentSelectionListener(DocumentSelectionListener l) { - listeners.add(l); - } - - public void removeDocumentSelectionListener(DocumentSelectionListener l) { - listeners.remove(l); - } - - protected void fireDocumentSelection(int type) { - DocumentSelectionListener[] array = - listeners.toArray(new DocumentSelectionListener[0]); - - for (DocumentSelectionListener l: array) { - l.valueChanged(type); - } - } - - - - private class ComponentTreeSelectionListener implements TreeSelectionListener { - - @Override - public void valueChanged(TreeSelectionEvent e) { - TreePath path = componentTreeSelectionModel.getSelectionPath(); - if (path == null) { - componentSelection = null; - fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE); - return; - } - - componentSelection = (RocketComponent)path.getLastPathComponent(); - - clearSimulationSelection(); - fireDocumentSelection(DocumentSelectionListener.COMPONENT_SELECTION_CHANGE); - } - - } - - private class SimulationListSelectionListener implements ListSelectionListener { - - @Override - public void valueChanged(ListSelectionEvent e) { - int min = simulationListSelectionModel.getMinSelectionIndex(); - int max = simulationListSelectionModel.getMaxSelectionIndex(); - if (min < 0 || max < 0) { - simulationSelection = NO_SIMULATION; - fireDocumentSelection(DocumentSelectionListener.SIMULATION_SELECTION_CHANGE); - return; - } - - ArrayList<Simulation> list = new ArrayList<Simulation>(); - for (int i = min; i <= max; i++) { - if (simulationListSelectionModel.isSelectedIndex(i) && - (i < document.getSimulationCount())) { - list.add(document.getSimulation(i)); - } - } - simulationSelection = list.toArray(NO_SIMULATION); - - clearComponentSelection(); - fireDocumentSelection(DocumentSelectionListener.SIMULATION_SELECTION_CHANGE); - } - - } - -} diff --git a/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java b/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java deleted file mode 100644 index 5541efb0..00000000 --- a/src/net/sf/openrocket/gui/main/OpenRocketClipboard.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.rocketcomponent.RocketComponent; - -public final class OpenRocketClipboard { - - private static RocketComponent clipboardComponent = null; - private static Simulation[] clipboardSimulations = null; - - private static List<ClipboardListener> listeners = new ArrayList<ClipboardListener>(); - - private OpenRocketClipboard() { - // Disallow instantiation - } - - - /** - * Return the <code>RocketComponent</code> contained in the clipboard, or - * <code>null</code>. The component is returned verbatim, and must be copied - * before attaching to any rocket design! - * - * @return the rocket component contained in the clipboard, or <code>null</code> - * if the clipboard does not currently contain a rocket component. - */ - public static RocketComponent getClipboardComponent() { - return clipboardComponent; - } - - - public static void setClipboard(RocketComponent component) { - clipboardComponent = component; - clipboardSimulations = null; - fireClipboardChanged(); - } - - - public static Simulation[] getClipboardSimulations() { - if (clipboardSimulations == null || clipboardSimulations.length == 0) - return null; - return clipboardSimulations.clone(); - } - - public static void setClipboard(Simulation[] simulations) { - clipboardSimulations = simulations.clone(); - clipboardComponent = null; - fireClipboardChanged(); - } - - - - public static void addClipboardListener(ClipboardListener listener) { - listeners.add(listener); - } - - public static void removeClipboardListener(ClipboardListener listener) { - listeners.remove(listener); - } - - private static void fireClipboardChanged() { - ClipboardListener[] array = listeners.toArray(new ClipboardListener[0]); - for (ClipboardListener l: array) { - l.clipboardChanged(); - } - } - -} diff --git a/src/net/sf/openrocket/gui/main/RocketActions.java b/src/net/sf/openrocket/gui/main/RocketActions.java deleted file mode 100644 index a8da9c7e..00000000 --- a/src/net/sf/openrocket/gui/main/RocketActions.java +++ /dev/null @@ -1,705 +0,0 @@ -package net.sf.openrocket.gui.main; - - -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.util.ArrayList; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JCheckBox; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.KeyStroke; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Stage; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.Preferences; -import net.sf.openrocket.util.Pair; - - - -/** - * A class that holds Actions for common rocket and simulation operations such as - * cut/copy/paste/delete etc. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocketActions { - - public static final KeyStroke CUT_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_X, - ActionEvent.CTRL_MASK); - public static final KeyStroke COPY_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_C, - ActionEvent.CTRL_MASK); - public static final KeyStroke PASTE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_V, - ActionEvent.CTRL_MASK); - - private final OpenRocketDocument document; - private final Rocket rocket; - private final BasicFrame parentFrame; - private final DocumentSelectionModel selectionModel; - - - private final RocketAction deleteComponentAction; - private final RocketAction deleteSimulationAction; - private final RocketAction deleteAction; - private final RocketAction cutAction; - private final RocketAction copyAction; - private final RocketAction pasteAction; - private final RocketAction editAction; - private final RocketAction newStageAction; - private final RocketAction moveUpAction; - private final RocketAction moveDownAction; - private static final Translator trans = Application.getTranslator(); - - - public RocketActions(OpenRocketDocument document, DocumentSelectionModel selectionModel, - BasicFrame parentFrame) { - this.document = document; - this.rocket = document.getRocket(); - this.selectionModel = selectionModel; - this.parentFrame = parentFrame; - - // Add action also to updateActions() - this.deleteAction = new DeleteAction(); - this.deleteComponentAction = new DeleteComponentAction(); - this.deleteSimulationAction = new DeleteSimulationAction(); - this.cutAction = new CutAction(); - this.copyAction = new CopyAction(); - this.pasteAction = new PasteAction(); - this.editAction = new EditAction(); - this.newStageAction = new NewStageAction(); - this.moveUpAction = new MoveUpAction(); - this.moveDownAction = new MoveDownAction(); - - OpenRocketClipboard.addClipboardListener(this.pasteAction); - updateActions(); - - // Update all actions when tree selection or rocket changes - - selectionModel.addDocumentSelectionListener(new DocumentSelectionListener() { - @Override - public void valueChanged(int changeType) { - updateActions(); - } - }); - document.getRocket().addComponentChangeListener(new ComponentChangeListener() { - @Override - public void componentChanged(ComponentChangeEvent e) { - updateActions(); - } - }); - } - - /** - * Update the state of all of the actions. - */ - private void updateActions() { - deleteAction.clipboardChanged(); - cutAction.clipboardChanged(); - copyAction.clipboardChanged(); - pasteAction.clipboardChanged(); - editAction.clipboardChanged(); - newStageAction.clipboardChanged(); - moveUpAction.clipboardChanged(); - moveDownAction.clipboardChanged(); - } - - - - - public Action getDeleteComponentAction() { - return deleteAction; - } - - public Action getDeleteSimulationAction() { - return deleteAction; - } - - public Action getDeleteAction() { - return deleteAction; - } - - public Action getCutAction() { - return cutAction; - } - - public Action getCopyAction() { - return copyAction; - } - - public Action getPasteAction() { - return pasteAction; - } - - public Action getEditAction() { - return editAction; - } - - public Action getNewStageAction() { - return newStageAction; - } - - public Action getMoveUpAction() { - return moveUpAction; - } - - public Action getMoveDownAction() { - return moveDownAction; - } - - - - //////// Helper methods for the actions - - private boolean isDeletable(RocketComponent c) { - // Sanity check - if (c == null || c.getParent() == null) - return false; - - // Cannot remove Rocket - if (c instanceof Rocket) - return false; - - // Cannot remove last stage - if ((c instanceof Stage) && (c.getParent().getChildCount() == 1)) { - return false; - } - - return true; - } - - private void delete(RocketComponent c) { - if (!isDeletable(c)) { - throw new IllegalArgumentException("Report bug! Component " + c + - " not deletable."); - } - - RocketComponent parent = c.getParent(); - parent.removeChild(c); - } - - private boolean isCopyable(RocketComponent c) { - if (c==null) - return false; - if (c instanceof Rocket) - return false; - return true; - } - - - private boolean isSimulationSelected() { - Simulation[] selection = selectionModel.getSelectedSimulations(); - return (selection != null && selection.length > 0); - } - - - - private boolean verifyDeleteSimulation() { - boolean verify = Application.getPreferences().getBoolean(Preferences.CONFIRM_DELETE_SIMULATION, true); - if (verify) { - JPanel panel = new JPanel(new MigLayout()); - //// Do not ask me again - JCheckBox dontAsk = new JCheckBox(trans.get("RocketActions.checkbox.Donotaskmeagain")); - panel.add(dontAsk,"wrap"); - //// You can change the default operation in the preferences. - panel.add(new StyledLabel(trans.get("RocketActions.lbl.Youcanchangedefop"),-2)); - - int ret = JOptionPane.showConfirmDialog( - parentFrame, - new Object[] { - //// Delete the selected simulations? - trans.get("RocketActions.showConfirmDialog.lbl1"), - //// <html><i>This operation cannot be undone.</i> - trans.get("RocketActions.showConfirmDialog.lbl2"), - "", - panel }, - //// Delete simulations - trans.get("RocketActions.showConfirmDialog.title"), - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE); - if (ret != JOptionPane.OK_OPTION) - return false; - - if (dontAsk.isSelected()) { - Application.getPreferences().putBoolean(Preferences.CONFIRM_DELETE_SIMULATION, false); - } - } - - return true; - } - - - /** - * Return the component and position to which the current clipboard - * should be pasted. Returns null if the clipboard is empty or if the - * clipboard cannot be pasted to the current selection. - * - * @param clipboard the component on the clipboard. - * @return a Pair with both components defined, or null. - */ - private Pair<RocketComponent, Integer> getPastePosition(RocketComponent clipboard) { - RocketComponent selected = selectionModel.getSelectedComponent(); - if (selected == null) - return null; - - if (clipboard == null) - return null; - - if (selected.isCompatible(clipboard)) - return new Pair<RocketComponent, Integer>(selected, selected.getChildCount()); - - RocketComponent parent = selected.getParent(); - if (parent != null && parent.isCompatible(clipboard)) { - int index = parent.getChildPosition(selected) + 1; - return new Pair<RocketComponent, Integer>(parent, index); - } - - return null; - } - - - - - - /////// Action classes - - private abstract class RocketAction extends AbstractAction implements ClipboardListener { - public abstract void clipboardChanged(); - } - - - /** - * Action that deletes the selected component. - */ - private class DeleteComponentAction extends RocketAction { - public DeleteComponentAction() { - //// Delete - this.putValue(NAME, trans.get("RocketActions.DelCompAct.Delete")); - //// Delete the selected component. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DelCompAct.ttip.Delete")); - this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); -// this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); - this.putValue(SMALL_ICON, Icons.EDIT_DELETE); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent c = selectionModel.getSelectedComponent(); - - if (isDeletable(c)) { - ComponentConfigDialog.hideDialog(); - - document.addUndoPosition("Delete " + c.getComponentName()); - delete(c); - } - } - - @Override - public void clipboardChanged() { - this.setEnabled(isDeletable(selectionModel.getSelectedComponent())); - } - } - - - - /** - * Action that deletes the selected component. - */ - private class DeleteSimulationAction extends RocketAction { - public DeleteSimulationAction() { - //// Delete - this.putValue(NAME, trans.get("RocketActions.DelSimuAct.Delete")); - //// Delete the selected simulation. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DelSimuAct.ttip.Delete")); - this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); -// this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); - this.putValue(SMALL_ICON, Icons.EDIT_DELETE); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - Simulation[] sims = selectionModel.getSelectedSimulations(); - if (sims.length > 0) { - if (verifyDeleteSimulation()) { - for (Simulation s: sims) { - document.removeSimulation(s); - } - } - } - } - - @Override - public void clipboardChanged() { - this.setEnabled(isSimulationSelected()); - } - } - - - - /** - * Action that deletes the selected component. - */ - private class DeleteAction extends RocketAction { - public DeleteAction() { - //// Delete - this.putValue(NAME, trans.get("RocketActions.DelAct.Delete")); - //// Delete the selected component or simulation. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.DelAct.ttip.Delete")); - this.putValue(MNEMONIC_KEY, KeyEvent.VK_D); - this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0)); - this.putValue(SMALL_ICON, Icons.EDIT_DELETE); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - if (isSimulationSelected()) { - deleteSimulationAction.actionPerformed(e); - parentFrame.selectTab(BasicFrame.SIMULATION_TAB); - } else { - deleteComponentAction.actionPerformed(e); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); - } - } - - @Override - public void clipboardChanged() { - this.setEnabled(isDeletable(selectionModel.getSelectedComponent()) || - isSimulationSelected()); - } - } - - - - /** - * Action the cuts the selected component (copies to clipboard and deletes). - */ - private class CutAction extends RocketAction { - public CutAction() { - //// Cut - this.putValue(NAME, trans.get("RocketActions.CutAction.Cut")); - this.putValue(MNEMONIC_KEY, KeyEvent.VK_T); - this.putValue(ACCELERATOR_KEY, CUT_KEY_STROKE); - //// Cut this component or simulation to the clipboard and remove from this design - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.CutAction.ttip.Cut")); - this.putValue(SMALL_ICON, Icons.EDIT_CUT); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent c = selectionModel.getSelectedComponent(); - Simulation[] sims = selectionModel.getSelectedSimulations(); - - if (isDeletable(c) && isCopyable(c)) { - ComponentConfigDialog.hideDialog(); - - document.addUndoPosition("Cut " + c.getComponentName()); - OpenRocketClipboard.setClipboard(c.copy()); - delete(c); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); - } else if (isSimulationSelected()) { - - Simulation[] simsCopy = new Simulation[sims.length]; - for (int i=0; i < sims.length; i++) { - simsCopy[i] = sims[i].copy(); - } - OpenRocketClipboard.setClipboard(simsCopy); - - for (Simulation s: sims) { - document.removeSimulation(s); - } - parentFrame.selectTab(BasicFrame.SIMULATION_TAB); - } - } - - @Override - public void clipboardChanged() { - RocketComponent c = selectionModel.getSelectedComponent(); - this.setEnabled((isDeletable(c) && isCopyable(c)) || isSimulationSelected()); - } - } - - - - /** - * Action that copies the selected component to the clipboard. - */ - private class CopyAction extends RocketAction { - public CopyAction() { - //// Copy - this.putValue(NAME, trans.get("RocketActions.CopyAct.Copy")); - this.putValue(MNEMONIC_KEY, KeyEvent.VK_C); - this.putValue(ACCELERATOR_KEY, COPY_KEY_STROKE); - //// Copy this component (and subcomponents) to the clipboard. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.CopyAct.Copy")); - this.putValue(SMALL_ICON, Icons.EDIT_COPY); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent c = selectionModel.getSelectedComponent(); - Simulation[] sims = selectionModel.getSelectedSimulations(); - - if (isCopyable(c)) { - OpenRocketClipboard.setClipboard(c.copy()); - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); - } else if (sims.length >= 0) { - - Simulation[] simsCopy = new Simulation[sims.length]; - for (int i=0; i < sims.length; i++) { - simsCopy[i] = sims[i].copy(); - } - OpenRocketClipboard.setClipboard(simsCopy); - parentFrame.selectTab(BasicFrame.SIMULATION_TAB); - } - } - - @Override - public void clipboardChanged() { - this.setEnabled(isCopyable(selectionModel.getSelectedComponent()) || - isSimulationSelected()); - } - - } - - - - /** - * Action that pastes the current clipboard to the selected position. - * It first tries to paste the component to the end of the selected component - * as a child, and after that as a sibling after the selected component. - */ - private class PasteAction extends RocketAction { - public PasteAction() { - //// Paste - this.putValue(NAME, trans.get("RocketActions.PasteAct.Paste")); - this.putValue(MNEMONIC_KEY, KeyEvent.VK_P); - this.putValue(ACCELERATOR_KEY, PASTE_KEY_STROKE); - //// Paste the component or simulation on the clipboard to the design. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.PasteAct.ttip.Paste")); - this.putValue(SMALL_ICON, Icons.EDIT_PASTE); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent clipboard = OpenRocketClipboard.getClipboardComponent(); - Simulation[] sims = OpenRocketClipboard.getClipboardSimulations(); - - Pair<RocketComponent, Integer> position = getPastePosition(clipboard); - if (position != null) { - ComponentConfigDialog.hideDialog(); - - RocketComponent pasted = clipboard.copy(); - document.addUndoPosition("Paste " + pasted.getComponentName()); - position.getU().addChild(pasted, position.getV()); - selectionModel.setSelectedComponent(pasted); - - parentFrame.selectTab(BasicFrame.COMPONENT_TAB); - - } else if (sims != null) { - - ArrayList<Simulation> copySims = new ArrayList<Simulation>(); - - for (Simulation s: sims) { - Simulation copy = s.duplicateSimulation(rocket); - String name = copy.getName(); - if (name.matches(OpenRocketDocument.SIMULATION_NAME_PREFIX + "[0-9]+ *")) { - copy.setName(document.getNextSimulationName()); - } - document.addSimulation(copy); - copySims.add(copy); - } - selectionModel.setSelectedSimulations(copySims.toArray(new Simulation[0])); - - parentFrame.selectTab(BasicFrame.SIMULATION_TAB); - } - } - - @Override - public void clipboardChanged() { - this.setEnabled( - (getPastePosition(OpenRocketClipboard.getClipboardComponent()) != null) || - (OpenRocketClipboard.getClipboardSimulations() != null)); - } - } - - - - - - - /** - * Action to edit the currently selected component. - */ - private class EditAction extends RocketAction { - public EditAction() { - //// Edit - this.putValue(NAME, trans.get("RocketActions.EditAct.Edit")); - //// Edit the selected component. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.EditAct.ttip.Edit")); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent c = selectionModel.getSelectedComponent(); - if (c == null) - return; - - ComponentConfigDialog.showDialog(parentFrame, document, c); - } - - @Override - public void clipboardChanged() { - this.setEnabled(selectionModel.getSelectedComponent() != null); - } - } - - - - - - - - /** - * Action to add a new stage to the rocket. - */ - private class NewStageAction extends RocketAction { - public NewStageAction() { - //// New stage - this.putValue(NAME, trans.get("RocketActions.NewStageAct.Newstage")); - //// Add a new stage to the rocket design. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.NewStageAct.Newstage")); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - - ComponentConfigDialog.hideDialog(); - - RocketComponent stage = new Stage(); - //// Booster stage - stage.setName(trans.get("RocketActions.ActBoosterstage")); - //// Add stage - document.addUndoPosition("Add stage"); - rocket.addChild(stage); - rocket.getDefaultConfiguration().setAllStages(); - selectionModel.setSelectedComponent(stage); - ComponentConfigDialog.showDialog(parentFrame, document, stage); - - } - - @Override - public void clipboardChanged() { - this.setEnabled(true); - } - } - - - - - /** - * Action to move the selected component upwards in the parent's child list. - */ - private class MoveUpAction extends RocketAction { - public MoveUpAction() { - //// Move up - this.putValue(NAME, trans.get("RocketActions.MoveUpAct.Moveup")); - //// Move this component upwards. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.MoveUpAct.ttip.Moveup")); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent selected = selectionModel.getSelectedComponent(); - if (!canMove(selected)) - return; - - ComponentConfigDialog.hideDialog(); - - RocketComponent parent = selected.getParent(); - document.addUndoPosition("Move "+selected.getComponentName()); - parent.moveChild(selected, parent.getChildPosition(selected)-1); - selectionModel.setSelectedComponent(selected); - } - - @Override - public void clipboardChanged() { - this.setEnabled(canMove(selectionModel.getSelectedComponent())); - } - - private boolean canMove(RocketComponent c) { - if (c == null || c.getParent() == null) - return false; - RocketComponent parent = c.getParent(); - if (parent.getChildPosition(c) > 0) - return true; - return false; - } - } - - - - /** - * Action to move the selected component down in the parent's child list. - */ - private class MoveDownAction extends RocketAction { - public MoveDownAction() { - //// Move down - this.putValue(NAME, trans.get("RocketActions.MoveDownAct.Movedown")); - //// Move this component downwards. - this.putValue(SHORT_DESCRIPTION, trans.get("RocketActions.MoveDownAct.ttip.Movedown")); - clipboardChanged(); - } - - @Override - public void actionPerformed(ActionEvent e) { - RocketComponent selected = selectionModel.getSelectedComponent(); - if (!canMove(selected)) - return; - - ComponentConfigDialog.hideDialog(); - - RocketComponent parent = selected.getParent(); - document.addUndoPosition("Move "+selected.getComponentName()); - parent.moveChild(selected, parent.getChildPosition(selected)+1); - selectionModel.setSelectedComponent(selected); - } - - @Override - public void clipboardChanged() { - this.setEnabled(canMove(selectionModel.getSelectedComponent())); - } - - private boolean canMove(RocketComponent c) { - if (c == null || c.getParent() == null) - return false; - RocketComponent parent = c.getParent(); - if (parent.getChildPosition(c) < parent.getChildCount()-1) - return true; - return false; - } - } - - - -} diff --git a/src/net/sf/openrocket/gui/main/SimulationEditDialog.java b/src/net/sf/openrocket/gui/main/SimulationEditDialog.java deleted file mode 100644 index 2b5ca7e7..00000000 --- a/src/net/sf/openrocket/gui/main/SimulationEditDialog.java +++ /dev/null @@ -1,1057 +0,0 @@ -package net.sf.openrocket.gui.main; - - -import java.awt.Color; -import java.awt.Component; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.util.Arrays; -import java.util.List; - -import javax.swing.AbstractListModel; -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSpinner; -import javax.swing.JTabbedPane; -import javax.swing.JTextField; -import javax.swing.ListCellRenderer; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.SpinnerEditor; -import net.sf.openrocket.gui.adaptors.BooleanModel; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.EnumModel; -import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.SimulationExportPanel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.plot.Axis; -import net.sf.openrocket.gui.plot.PlotConfiguration; -import net.sf.openrocket.gui.plot.SimulationPlotPanel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.models.atmosphere.ExtendedISAModel; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.RK4SimulationStepper; -import net.sf.openrocket.simulation.SimulationOptions; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.example.CSVSaveListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.GeodeticComputationStrategy; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.ValueMarker; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.StandardXYItemRenderer; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - - -public class SimulationEditDialog extends JDialog { - - public static final int DEFAULT = -1; - public static final int EDIT = 1; - public static final int PLOT = 2; - - - private final Window parentWindow; - private final Simulation simulation; - private final SimulationOptions conditions; - private final Configuration configuration; - private static final Translator trans = Application.getTranslator(); - - - public SimulationEditDialog(Window parent, Simulation s) { - this(parent, s, 0); - } - - public SimulationEditDialog(Window parent, Simulation s, int tab) { - //// Edit simulation - super(parent, trans.get("simedtdlg.title.Editsim"), JDialog.ModalityType.DOCUMENT_MODAL); - - this.parentWindow = parent; - this.simulation = s; - this.conditions = simulation.getOptions(); - configuration = simulation.getConfiguration(); - - JPanel mainPanel = new JPanel(new MigLayout("fill", "[grow, fill]")); - - //// Simulation name: - mainPanel.add(new JLabel(trans.get("simedtdlg.lbl.Simname") + " "), "span, split 2, shrink"); - final JTextField field = new JTextField(simulation.getName()); - field.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void changedUpdate(DocumentEvent e) { - setText(); - } - - @Override - public void insertUpdate(DocumentEvent e) { - setText(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - setText(); - } - - private void setText() { - String name = field.getText(); - if (name == null || name.equals("")) - return; - System.out.println("Setting name:" + name); - simulation.setName(name); - - } - }); - mainPanel.add(field, "shrinky, growx, wrap"); - - JTabbedPane tabbedPane = new JTabbedPane(); - - //// Launch conditions - tabbedPane.addTab(trans.get("simedtdlg.tab.Launchcond"), flightConditionsTab()); - //// Simulation options - tabbedPane.addTab(trans.get("simedtdlg.tab.Simopt"), simulationOptionsTab()); - //// Plot data - tabbedPane.addTab(trans.get("simedtdlg.tab.Plotdata"), plotTab()); - //// Export data - tabbedPane.addTab(trans.get("simedtdlg.tab.Exportdata"), exportTab()); - - // Select the initial tab - if (tab == EDIT) { - tabbedPane.setSelectedIndex(0); - } else if (tab == PLOT) { - tabbedPane.setSelectedIndex(2); - } else { - FlightData data = s.getSimulatedData(); - if (data == null || data.getBranchCount() == 0) - tabbedPane.setSelectedIndex(0); - else - tabbedPane.setSelectedIndex(2); - } - - mainPanel.add(tabbedPane, "spanx, grow, wrap"); - - - // Buttons - mainPanel.add(new JPanel(), "spanx, split, growx"); - - JButton button; - //// Run simulation button - button = new JButton(trans.get("simedtdlg.but.runsimulation")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SimulationEditDialog.this.dispose(); - SimulationRunDialog.runSimulations(parentWindow, simulation); - } - }); - mainPanel.add(button, "gapright para"); - - //// Close button - JButton close = new JButton(trans.get("dlg.but.close")); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SimulationEditDialog.this.dispose(); - } - }); - mainPanel.add(close, ""); - - - this.add(mainPanel); - this.validate(); - this.pack(); - this.setLocationByPlatform(true); - - GUIUtil.setDisposableDialogOptions(this, button); - } - - - - - - private JPanel flightConditionsTab() { - JPanel panel = new JPanel(new MigLayout("fill")); - JPanel sub; - String tip; - UnitSelector unit; - BasicSlider slider; - DoubleModel m; - JSpinner spin; - - //// Motor selector - //// Motor configuration: - JLabel label = new JLabel(trans.get("simedtdlg.lbl.Motorcfg")); - //// Select the motor configuration to use. - label.setToolTipText(trans.get("simedtdlg.lbl.ttip.Motorcfg")); - panel.add(label, "shrinkx, spanx, split 2"); - - JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration)); - //// Select the motor configuration to use. - combo.setToolTipText(trans.get("simedtdlg.combo.ttip.motorconf")); - combo.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - conditions.setMotorConfigurationID(configuration.getMotorConfigurationID()); - } - }); - panel.add(combo, "growx, wrap para"); - - - //// Wind settings: Average wind speed, turbulence intensity, std. deviation - sub = new JPanel(new MigLayout("fill, gap rel unrel", - "[grow][65lp!][30lp!][75lp!]", "")); - //// Wind - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.lbl.Wind"))); - panel.add(sub, "growx, split 2, aligny 0, flowy, gapright para"); - - - // Wind average - //// Average windspeed: - label = new JLabel(trans.get("simedtdlg.lbl.Averwindspeed")); - //// The average windspeed relative to the ground. - tip = trans.get("simedtdlg.lbl.ttip.Averwindspeed"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "WindSpeedAverage", UnitGroup.UNITS_VELOCITY, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(0, 10.0)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - - // Wind std. deviation - //// Standard deviation: - label = new JLabel(trans.get("simedtdlg.lbl.Stddeviation")); - //// <html>The standard deviation of the windspeed.<br> - //// The windspeed is within twice the standard deviation from the average for 95% of the time. - tip = trans.get("simedtdlg.lbl.ttip.Stddeviation"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "WindSpeedDeviation", UnitGroup.UNITS_VELOCITY, 0); - DoubleModel m2 = new DoubleModel(conditions, "WindSpeedAverage", 0.25, - UnitGroup.UNITS_COEFFICIENT, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(new DoubleModel(0), m2)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - // Wind turbulence intensity - //// Turbulence intensity: - label = new JLabel(trans.get("simedtdlg.lbl.Turbulenceintensity")); - //// <html>The turbulence intensity is the standard deviation divided by the average windspeed.<br> - //// Typical values range from - //// to - tip = trans.get("simedtdlg.lbl.ttip.Turbulenceintensity1") + - trans.get("simedtdlg.lbl.ttip.Turbulenceintensity2") + " " + - UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.05) + - " " + trans.get("simedtdlg.lbl.ttip.Turbulenceintensity3") + " " + - UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.20) + "."; - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "WindTurbulenceIntensity", UnitGroup.UNITS_RELATIVE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - - final JLabel intensityLabel = new JLabel( - getIntensityDescription(conditions.getWindTurbulenceIntensity())); - intensityLabel.setToolTipText(tip); - sub.add(intensityLabel, "w 75lp, wrap"); - m.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - intensityLabel.setText( - getIntensityDescription(conditions.getWindTurbulenceIntensity())); - } - }); - - - - - - //// Temperature and pressure - sub = new JPanel(new MigLayout("fill, gap rel unrel", - "[grow][65lp!][30lp!][75lp!]", "")); - //// Atmospheric conditions - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Atmoscond"))); - panel.add(sub, "growx, aligny 0, gapright para"); - - - BooleanModel isa = new BooleanModel(conditions, "ISAAtmosphere"); - JCheckBox check = new JCheckBox(isa); - //// Use International Standard Atmosphere - check.setText(trans.get("simedtdlg.checkbox.InterStdAtmosphere")); - //// <html>Select to use the International Standard Atmosphere model. - //// <br>This model has a temperature of - //// and a pressure of - //// at sea level. - check.setToolTipText(trans.get("simedtdlg.checkbox.ttip.InterStdAtmosphere1") + " " + - UnitGroup.UNITS_TEMPERATURE.toStringUnit(ExtendedISAModel.STANDARD_TEMPERATURE) + - " " + trans.get("simedtdlg.checkbox.ttip.InterStdAtmosphere2") + " " + - UnitGroup.UNITS_PRESSURE.toStringUnit(ExtendedISAModel.STANDARD_PRESSURE) + - " " + trans.get("simedtdlg.checkbox.ttip.InterStdAtmosphere3")); - sub.add(check, "spanx, wrap unrel"); - - // Temperature: - label = new JLabel(trans.get("simedtdlg.lbl.Temperature")); - //// The temperature at the launch site. - tip = trans.get("simedtdlg.lbl.ttip.Temperature"); - label.setToolTipText(tip); - isa.addEnableComponent(label, false); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchTemperature", UnitGroup.UNITS_TEMPERATURE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - isa.addEnableComponent(spin, false); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - isa.addEnableComponent(unit, false); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(253.15, 308.15)); // -20 ... 35 - slider.setToolTipText(tip); - isa.addEnableComponent(slider, false); - sub.add(slider, "w 75lp, wrap"); - - - - // Pressure: - label = new JLabel(trans.get("simedtdlg.lbl.Pressure")); - //// The atmospheric pressure at the launch site. - tip = trans.get("simedtdlg.lbl.ttip.Pressure"); - label.setToolTipText(tip); - isa.addEnableComponent(label, false); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchPressure", UnitGroup.UNITS_PRESSURE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - isa.addEnableComponent(spin, false); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - isa.addEnableComponent(unit, false); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(0.950e5, 1.050e5)); - slider.setToolTipText(tip); - isa.addEnableComponent(slider, false); - sub.add(slider, "w 75lp, wrap"); - - - - - - //// Launch site conditions - sub = new JPanel(new MigLayout("fill, gap rel unrel", - "[grow][65lp!][30lp!][75lp!]", "")); - //// Launch site - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.lbl.Launchsite"))); - panel.add(sub, "growx, split 2, aligny 0, flowy"); - - - // Latitude: - label = new JLabel(trans.get("simedtdlg.lbl.Latitude")); - //// <html>The launch site latitude affects the gravitational pull of Earth.<br> - //// Positive values are on the Northern hemisphere, negative values on the Southern hemisphere. - tip = trans.get("simedtdlg.lbl.ttip.Latitude"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchLatitude", UnitGroup.UNITS_NONE, -90, 90); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - label = new JLabel(Chars.DEGREE + " N"); - label.setToolTipText(tip); - sub.add(label, "growx"); - slider = new BasicSlider(m.getSliderModel(-90, 90)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - // Longitude: - label = new JLabel(trans.get("simedtdlg.lbl.Longitude")); - tip = trans.get("simedtdlg.lbl.ttip.Longitude"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchLongitude", UnitGroup.UNITS_NONE, -180, 180); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - label = new JLabel(Chars.DEGREE + " E"); - label.setToolTipText(tip); - sub.add(label, "growx"); - slider = new BasicSlider(m.getSliderModel(-180, 180)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - // Altitude: - label = new JLabel(trans.get("simedtdlg.lbl.Altitude")); - //// <html>The launch altitude above mean sea level.<br> - //// This affects the position of the rocket in the atmospheric model. - tip = trans.get("simedtdlg.lbl.ttip.Altitude"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchAltitude", UnitGroup.UNITS_DISTANCE, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(0, 250, 1000)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - - - - //// Launch rod - sub = new JPanel(new MigLayout("fill, gap rel unrel", - "[grow][65lp!][30lp!][75lp!]", "")); - //// Launch rod - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Launchrod"))); - panel.add(sub, "growx, aligny 0, wrap"); - - - // Length: - label = new JLabel(trans.get("simedtdlg.lbl.Length")); - //// The length of the launch rod. - tip = trans.get("simedtdlg.lbl.ttip.Length"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchRodLength", UnitGroup.UNITS_LENGTH, 0); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(0, 1, 5)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - - // Angle: - label = new JLabel(trans.get("simedtdlg.lbl.Angle")); - //// The angle of the launch rod from vertical. - tip = trans.get("simedtdlg.lbl.ttip.Angle"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchRodAngle", UnitGroup.UNITS_ANGLE, - 0, SimulationOptions.MAX_LAUNCH_ROD_ANGLE); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(0, Math.PI / 9, - SimulationOptions.MAX_LAUNCH_ROD_ANGLE)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - - - // Direction: - label = new JLabel(trans.get("simedtdlg.lbl.Direction")); - //// <html>Direction of the launch rod relative to the wind.<br> - //// = towards the wind, - //// = downwind. - tip = trans.get("simedtdlg.lbl.ttip.Direction1") + - UnitGroup.UNITS_ANGLE.toStringUnit(0) + - " " + trans.get("simedtdlg.lbl.ttip.Direction2") + " " + - UnitGroup.UNITS_ANGLE.toStringUnit(Math.PI) + - " " + trans.get("simedtdlg.lbl.ttip.Direction3"); - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "LaunchRodDirection", UnitGroup.UNITS_ANGLE, - -Math.PI, Math.PI); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "growx"); - slider = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - - return panel; - } - - - private String getIntensityDescription(double i) { - if (i < 0.001) - //// None - return trans.get("simedtdlg.IntensityDesc.None"); - if (i < 0.05) - //// Very low - return trans.get("simedtdlg.IntensityDesc.Verylow"); - if (i < 0.10) - //// Low - return trans.get("simedtdlg.IntensityDesc.Low"); - if (i < 0.15) - //// Medium - return trans.get("simedtdlg.IntensityDesc.Medium"); - if (i < 0.20) - //// High - return trans.get("simedtdlg.IntensityDesc.High"); - if (i < 0.25) - //// Very high - return trans.get("simedtdlg.IntensityDesc.Veryhigh"); - //// Extreme - return trans.get("simedtdlg.IntensityDesc.Extreme"); - } - - - - private JPanel simulationOptionsTab() { - JPanel panel = new JPanel(new MigLayout("fill")); - JPanel sub, subsub; - String tip; - JLabel label; - DoubleModel m; - JSpinner spin; - UnitSelector unit; - BasicSlider slider; - - - //// Simulation options - sub = new JPanel(new MigLayout("fill, gap rel unrel", - "[grow][65lp!][30lp!][75lp!]", "")); - //// Simulator options - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Simopt"))); - panel.add(sub, "growx, growy, aligny 0"); - - - // Separate panel for computation methods, as they use a different layout - subsub = new JPanel(new MigLayout("insets 0, fill")); - - - //// Calculation method: - tip = trans.get("simedtdlg.lbl.ttip.Calcmethod"); - label = new JLabel(trans.get("simedtdlg.lbl.Calcmethod")); - label.setToolTipText(tip); - subsub.add(label, "gapright para"); - - //// Extended Barrowman - label = new JLabel(trans.get("simedtdlg.lbl.ExtBarrowman")); - label.setToolTipText(tip); - subsub.add(label, "growx, wrap para"); - - - // Simulation method - tip = trans.get("simedtdlg.lbl.ttip.Simmethod1") + - trans.get("simedtdlg.lbl.ttip.Simmethod2"); - label = new JLabel(trans.get("simedtdlg.lbl.Simmethod")); - label.setToolTipText(tip); - subsub.add(label, "gapright para"); - - label = new JLabel("6-DOF Runge-Kutta 4"); - label.setToolTipText(tip); - subsub.add(label, "growx, wrap para"); - - - //// Geodetic calculation method: - label = new JLabel(trans.get("simedtdlg.lbl.GeodeticMethod")); - label.setToolTipText(trans.get("simedtdlg.lbl.ttip.GeodeticMethodTip")); - subsub.add(label, "gapright para"); - - EnumModel<GeodeticComputationStrategy> gcsModel = new EnumModel<GeodeticComputationStrategy>(conditions, "GeodeticComputation"); - final JComboBox gcsCombo = new JComboBox(gcsModel); - ActionListener gcsTTipListener = new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - GeodeticComputationStrategy gcs = (GeodeticComputationStrategy) gcsCombo.getSelectedItem(); - gcsCombo.setToolTipText(gcs.getDescription()); - } - }; - gcsCombo.addActionListener(gcsTTipListener); - gcsTTipListener.actionPerformed(null); - subsub.add(gcsCombo, "growx, wrap para"); - - sub.add(subsub, "spanx, wrap para"); - - - //// Time step: - label = new JLabel(trans.get("simedtdlg.lbl.Timestep")); - tip = trans.get("simedtdlg.lbl.ttip.Timestep1") + - trans.get("simedtdlg.lbl.ttip.Timestep2") + " " + - UnitGroup.UNITS_TIME_STEP.toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + - "."; - label.setToolTipText(tip); - sub.add(label); - - m = new DoubleModel(conditions, "TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1); - - spin = new JSpinner(m.getSpinnerModel()); - spin.setEditor(new SpinnerEditor(spin)); - spin.setToolTipText(tip); - sub.add(spin, "w 65lp!"); - //sub.add(spin, "nogrid"); - - unit = new UnitSelector(m); - unit.setToolTipText(tip); - sub.add(unit, "w 25"); - //sub.add(unit, "nogrid"); - slider = new BasicSlider(m.getSliderModel(0, 0.2)); - slider.setToolTipText(tip); - sub.add(slider, "w 75lp, wrap"); - //sub.add(slider,"wrap"); - - - - - //// Reset to default button - JButton button = new JButton(trans.get("simedtdlg.but.resettodefault")); - //// Reset the time step to its default value ( - button.setToolTipText(trans.get("simedtdlg.but.ttip.resettodefault") + - UnitGroup.UNITS_SHORT_TIME.toStringUnit(RK4SimulationStepper.RECOMMENDED_TIME_STEP) + - ")."); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - conditions.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP); - conditions.setGeodeticComputation(GeodeticComputationStrategy.SPHERICAL); - } - }); - - sub.add(button, "align left"); - - - - - //// Simulation listeners - sub = new JPanel(new MigLayout("fill, gap 0 0")); - //// Simulator listeners - sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Simlist"))); - panel.add(sub, "growx, growy"); - - - DescriptionArea desc = new DescriptionArea(5); - //// <html><i>Simulation listeners</i> is an advanced feature that allows user-written code to listen to and interact with the simulation. - //// For details on writing simulation listeners, see the OpenRocket technical documentation. - desc.setText(trans.get("simedtdlg.txt.longA1") + - trans.get("simedtdlg.txt.longA2")); - sub.add(desc, "aligny 0, growx, wrap para"); - - //// Current listeners: - label = new JLabel(trans.get("simedtdlg.lbl.Curlist")); - sub.add(label, "spanx, wrap rel"); - - final ListenerListModel listenerModel = new ListenerListModel(); - final JList list = new JList(listenerModel); - list.setCellRenderer(new ListenerCellRenderer()); - JScrollPane scroll = new JScrollPane(list); - // scroll.setPreferredSize(new Dimension(1,1)); - sub.add(scroll, "height 1px, grow, wrap rel"); - - //// Add button - button = new JButton(trans.get("simedtdlg.but.add")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - String previous = Application.getPreferences().getString("previousListenerName", ""); - String input = (String) JOptionPane.showInputDialog(SimulationEditDialog.this, - new Object[] { - //// Type the full Java class name of the simulation listener, for example: - "Type the full Java class name of the simulation listener, for example:", - "<html><tt>" + CSVSaveListener.class.getName() + "</tt>" }, - //// Add simulation listener - trans.get("simedtdlg.lbl.Addsimlist"), - JOptionPane.QUESTION_MESSAGE, - null, null, - previous - ); - if (input == null || input.equals("")) - return; - - Application.getPreferences().putString("previousListenerName", input); - simulation.getSimulationListeners().add(input); - listenerModel.fireContentsChanged(); - } - }); - sub.add(button, "split 2, sizegroup buttons, alignx 50%, gapright para"); - - //// Remove button - button = new JButton(trans.get("simedtdlg.but.remove")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int[] selected = list.getSelectedIndices(); - Arrays.sort(selected); - for (int i = selected.length - 1; i >= 0; i--) { - simulation.getSimulationListeners().remove(selected[i]); - } - listenerModel.fireContentsChanged(); - } - }); - sub.add(button, "sizegroup buttons, alignx 50%"); - - - return panel; - } - - - private class ListenerListModel extends AbstractListModel { - @Override - public String getElementAt(int index) { - if (index < 0 || index >= getSize()) - return null; - return simulation.getSimulationListeners().get(index); - } - - @Override - public int getSize() { - return simulation.getSimulationListeners().size(); - } - - public void fireContentsChanged() { - super.fireContentsChanged(this, 0, getSize()); - } - } - - - - - /** - * A panel for plotting the previously calculated data. - */ - private JPanel plotTab() { - - // Check that data exists - if (simulation.getSimulatedData() == null || - simulation.getSimulatedData().getBranchCount() == 0) { - return noDataPanel(); - } - - return new SimulationPlotPanel(simulation); - } - - - - /** - * A panel for exporting the data. - */ - private JPanel exportTab() { - FlightData data = simulation.getSimulatedData(); - - // Check that data exists - if (data == null || data.getBranchCount() == 0 || - data.getBranch(0).getTypes().length == 0) { - return noDataPanel(); - } - - return new SimulationExportPanel(simulation); - } - - - - - - /** - * Return a panel stating that there is no data available, and that the user - * should run the simulation first. - */ - public static JPanel noDataPanel() { - JPanel panel = new JPanel(new MigLayout("fill")); - - // No data available - //// No flight data available. - panel.add(new JLabel(trans.get("simedtdlg.lbl.Noflightdata")), - "alignx 50%, aligny 100%, wrap para"); - //// Please run the simulation first. - panel.add(new JLabel(trans.get("simedtdlg.lbl.runsimfirst")), - "alignx 50%, aligny 0%, wrap"); - return panel; - } - - - private void performPlot(PlotConfiguration config) { - - // Fill the auto-selections - FlightDataBranch branch = simulation.getSimulatedData().getBranch(0); - PlotConfiguration filled = config.fillAutoAxes(branch); - List<Axis> axes = filled.getAllAxes(); - - - // Create the data series for both axes - XYSeriesCollection[] data = new XYSeriesCollection[2]; - data[0] = new XYSeriesCollection(); - data[1] = new XYSeriesCollection(); - - - // Get the domain axis type - final FlightDataType domainType = filled.getDomainAxisType(); - final Unit domainUnit = filled.getDomainAxisUnit(); - if (domainType == null) { - throw new IllegalArgumentException("Domain axis type not specified."); - } - List<Double> x = branch.get(domainType); - - - // Create the XYSeries objects from the flight data and store into the collections - int length = filled.getTypeCount(); - String[] axisLabel = new String[2]; - for (int i = 0; i < length; i++) { - // Get info - FlightDataType type = filled.getType(i); - Unit unit = filled.getUnit(i); - int axis = filled.getAxis(i); - String name = getLabel(type, unit); - - // Store data in provided units - List<Double> y = branch.get(type); - XYSeries series = new XYSeries(name, false, true); - for (int j = 0; j < x.size(); j++) { - series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j))); - } - data[axis].addSeries(series); - - // Update axis label - if (axisLabel[axis] == null) - axisLabel[axis] = type.getName(); - else - axisLabel[axis] += "; " + type.getName(); - } - - - // Create the chart using the factory to get all default settings - JFreeChart chart = ChartFactory.createXYLineChart( - //// Simulated flight - trans.get("simedtdlg.chart.Simflight"), - null, - null, - null, - PlotOrientation.VERTICAL, - true, - true, - false - ); - - - // Add the data and formatting to the plot - XYPlot plot = chart.getXYPlot(); - int axisno = 0; - for (int i = 0; i < 2; i++) { - // Check whether axis has any data - if (data[i].getSeriesCount() > 0) { - // Create and set axis - double min = axes.get(i).getMinValue(); - double max = axes.get(i).getMaxValue(); - NumberAxis axis = new PresetNumberAxis(min, max); - axis.setLabel(axisLabel[i]); - // axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue()); - plot.setRangeAxis(axisno, axis); - - // Add data and map to the axis - plot.setDataset(axisno, data[i]); - plot.setRenderer(axisno, new StandardXYItemRenderer()); - plot.mapDatasetToRangeAxis(axisno, axisno); - axisno++; - } - } - - plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit)); - plot.addDomainMarker(new ValueMarker(0)); - plot.addRangeMarker(new ValueMarker(0)); - - - // Create the dialog - //// Simulation results - final JDialog dialog = new JDialog(this, trans.get("simedtdlg.dlg.Simres")); - dialog.setModalityType(ModalityType.DOCUMENT_MODAL); - - JPanel panel = new JPanel(new MigLayout("fill")); - dialog.add(panel); - - ChartPanel chartPanel = new ChartPanel(chart, - false, // properties - true, // save - false, // print - true, // zoom - true); // tooltips - chartPanel.setMouseWheelEnabled(true); - chartPanel.setEnforceFileExtensions(true); - chartPanel.setInitialDelay(500); - - chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - - panel.add(chartPanel, "grow, wrap 20lp"); - - //// Close button - JButton button = new JButton(trans.get("dlg.but.close")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - dialog.setVisible(false); - } - }); - panel.add(button, "right"); - - dialog.setLocationByPlatform(true); - dialog.pack(); - - GUIUtil.setDisposableDialogOptions(dialog, button); - - dialog.setVisible(true); - } - - - private class PresetNumberAxis extends NumberAxis { - private final double min; - private final double max; - - public PresetNumberAxis(double min, double max) { - this.min = min; - this.max = max; - autoAdjustRange(); - } - - @Override - protected void autoAdjustRange() { - this.setRange(min, max); - } - } - - - private String getLabel(FlightDataType type, Unit unit) { - String name = type.getName(); - if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) && - !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0) - name += " (" + unit.getUnit() + ")"; - return name; - } - - - - private class ListenerCellRenderer extends JLabel implements ListCellRenderer { - - @Override - public Component getListCellRendererComponent(JList list, Object value, - int index, boolean isSelected, boolean cellHasFocus) { - String s = value.toString(); - setText(s); - - // Attempt instantiating, catch any exceptions - Exception ex = null; - try { - Class<?> c = Class.forName(s); - @SuppressWarnings("unused") - SimulationListener l = (SimulationListener) c.newInstance(); - } catch (Exception e) { - ex = e; - } - - if (ex == null) { - setIcon(Icons.SIMULATION_LISTENER_OK); - //// Listener instantiated successfully. - setToolTipText("Listener instantiated successfully."); - } else { - setIcon(Icons.SIMULATION_LISTENER_ERROR); - //// <html>Unable to instantiate listener due to exception:<br> - setToolTipText("<html>Unable to instantiate listener due to exception:<br>" + - ex.toString()); - } - - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - } - setOpaque(true); - return this; - } - } -} diff --git a/src/net/sf/openrocket/gui/main/SimulationPanel.java b/src/net/sf/openrocket/gui/main/SimulationPanel.java deleted file mode 100644 index 11b707ea..00000000 --- a/src/net/sf/openrocket/gui/main/SimulationPanel.java +++ /dev/null @@ -1,586 +0,0 @@ -package net.sf.openrocket.gui.main; - - -import java.awt.Color; -import java.awt.Component; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.Arrays; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.KeyStroke; -import javax.swing.ListSelectionModel; -import javax.swing.SwingUtilities; -import javax.swing.table.DefaultTableCellRenderer; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.document.events.DocumentChangeEvent; -import net.sf.openrocket.document.events.DocumentChangeListener; -import net.sf.openrocket.document.events.SimulationChangeEvent; -import net.sf.openrocket.gui.adaptors.Column; -import net.sf.openrocket.gui.adaptors.ColumnTableModel; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.Preferences; -import net.sf.openrocket.unit.UnitGroup; - -public class SimulationPanel extends JPanel { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - private static final Color WARNING_COLOR = Color.RED; - private static final String WARNING_TEXT = "\uFF01"; // Fullwidth exclamation mark - - private static final Color OK_COLOR = new Color(60, 150, 0); - private static final String OK_TEXT = "\u2714"; // Heavy check mark - - - - private final OpenRocketDocument document; - - private final ColumnTableModel simulationTableModel; - private final JTable simulationTable; - - - public SimulationPanel(OpenRocketDocument doc) { - super(new MigLayout("fill", "[grow][][][][][][grow]")); - - JButton button; - - - this.document = doc; - - - - //////// The simulation action buttons - - //// New simulation button - button = new JButton(trans.get("simpanel.but.newsimulation")); - //// Add a new simulation - button.setToolTipText(trans.get("simpanel.but.ttip.newsimulation")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - Simulation sim = new Simulation(document.getRocket()); - sim.setName(document.getNextSimulationName()); - - int n = document.getSimulationCount(); - document.addSimulation(sim); - simulationTableModel.fireTableDataChanged(); - simulationTable.clearSelection(); - simulationTable.addRowSelectionInterval(n, n); - - openDialog(sim, SimulationEditDialog.EDIT); - } - }); - this.add(button, "skip 1, gapright para"); - - //// Edit simulation button - button = new JButton(trans.get("simpanel.but.editsimulation")); - //// Edit the selected simulation - button.setToolTipText(trans.get("simpanel.but.ttip.editsim")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int selected = simulationTable.getSelectedRow(); - if (selected < 0) - return; // TODO: MEDIUM: "None selected" dialog - - selected = simulationTable.convertRowIndexToModel(selected); - simulationTable.clearSelection(); - simulationTable.addRowSelectionInterval(selected, selected); - - openDialog(document.getSimulations().get(selected), SimulationEditDialog.EDIT); - } - }); - this.add(button, "gapright para"); - - //// Run simulations - button = new JButton(trans.get("simpanel.but.runsimulations")); - //// Re-run the selected simulations - button.setToolTipText(trans.get("simpanel.but.ttip.runsimu")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int[] selection = simulationTable.getSelectedRows(); - if (selection.length == 0) - return; // TODO: LOW: "None selected" dialog - - Simulation[] sims = new Simulation[selection.length]; - for (int i = 0; i < selection.length; i++) { - selection[i] = simulationTable.convertRowIndexToModel(selection[i]); - sims[i] = document.getSimulation(selection[i]); - } - - long t = System.currentTimeMillis(); - new SimulationRunDialog(SwingUtilities.getWindowAncestor( - SimulationPanel.this), sims).setVisible(true); - log.info("Running simulations took " + (System.currentTimeMillis() - t) + " ms"); - fireMaintainSelection(); - } - }); - this.add(button, "gapright para"); - - //// Delete simulations button - button = new JButton(trans.get("simpanel.but.deletesimulations")); - //// Delete the selected simulations - button.setToolTipText(trans.get("simpanel.but.ttip.deletesim")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int[] selection = simulationTable.getSelectedRows(); - if (selection.length == 0) - return; // TODO: LOW: "None selected" dialog - - // Verify deletion - boolean verify = Application.getPreferences().getBoolean(Preferences.CONFIRM_DELETE_SIMULATION, true); - if (verify) { - - JPanel panel = new JPanel(new MigLayout()); - //// Do not ask me again - JCheckBox dontAsk = new JCheckBox(trans.get("simpanel.checkbox.donotask")); - panel.add(dontAsk, "wrap"); - //// You can change the default operation in the preferences. - panel.add(new StyledLabel(trans.get("simpanel.lbl.defpref"), -2)); - - int ret = JOptionPane.showConfirmDialog(SimulationPanel.this, - new Object[] { - //// Delete the selected simulations? - trans.get("simpanel.dlg.lbl.DeleteSim1"), - //// <html><i>This operation cannot be undone.</i> - trans.get("simpanel.dlg.lbl.DeleteSim2"), - "", - panel }, - //// Delete simulations - trans.get("simpanel.dlg.lbl.DeleteSim3"), - JOptionPane.OK_CANCEL_OPTION, - JOptionPane.WARNING_MESSAGE); - if (ret != JOptionPane.OK_OPTION) - return; - - if (dontAsk.isSelected()) { - Application.getPreferences().putBoolean(Preferences.CONFIRM_DELETE_SIMULATION, false); - } - } - - // Delete simulations - for (int i = 0; i < selection.length; i++) { - selection[i] = simulationTable.convertRowIndexToModel(selection[i]); - } - Arrays.sort(selection); - for (int i = selection.length - 1; i >= 0; i--) { - document.removeSimulation(selection[i]); - } - simulationTableModel.fireTableDataChanged(); - } - }); - this.add(button, "gapright para"); - - //// Plot / export button - button = new JButton(trans.get("simpanel.but.plotexport")); - // button = new JButton("Plot flight"); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int selected = simulationTable.getSelectedRow(); - if (selected < 0) - return; // TODO: MEDIUM: "None selected" dialog - - selected = simulationTable.convertRowIndexToModel(selected); - simulationTable.clearSelection(); - simulationTable.addRowSelectionInterval(selected, selected); - - openDialog(document.getSimulations().get(selected), SimulationEditDialog.PLOT); - } - }); - this.add(button, "wrap para"); - - - - - //////// The simulation table - - simulationTableModel = new ColumnTableModel( - - //// Status and warning column - new Column("") { - private JLabel label = null; - - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - // Initialize the label - if (label == null) { - label = new StyledLabel(2f); - label.setIconTextGap(1); - // label.setFont(label.getFont().deriveFont(Font.BOLD)); - } - - // Set simulation status icon - Simulation.Status status = document.getSimulation(row).getStatus(); - label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status)); - - - // Set warning marker - if (status == Simulation.Status.NOT_SIMULATED || - status == Simulation.Status.EXTERNAL) { - - label.setText(""); - - } else { - - WarningSet w = document.getSimulation(row).getSimulatedWarnings(); - if (w == null) { - label.setText(""); - } else if (w.isEmpty()) { - label.setForeground(OK_COLOR); - label.setText(OK_TEXT); - } else { - label.setForeground(WARNING_COLOR); - label.setText(WARNING_TEXT); - } - } - - return label; - } - - @Override - public int getExactWidth() { - return 36; - } - - @Override - public Class<?> getColumnClass() { - return JLabel.class; - } - }, - - //// Simulation name - //// Name - new Column(trans.get("simpanel.col.Name")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - return document.getSimulation(row).getName(); - } - - @Override - public int getDefaultWidth() { - return 125; - } - }, - - //// Simulation motors - //// Motors - new Column(trans.get("simpanel.col.Motors")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - return document.getSimulation(row).getConfiguration() - .getMotorConfigurationDescription(); - } - - @Override - public int getDefaultWidth() { - return 125; - } - }, - - //// Apogee - new Column(trans.get("simpanel.col.Apogee")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - FlightData data = document.getSimulation(row).getSimulatedData(); - if (data == null) - return null; - - return UnitGroup.UNITS_DISTANCE.getDefaultUnit().toStringUnit( - data.getMaxAltitude()); - } - }, - - //// Maximum velocity - new Column(trans.get("simpanel.col.Maxvelocity")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - FlightData data = document.getSimulation(row).getSimulatedData(); - if (data == null) - return null; - - return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit( - data.getMaxVelocity()); - } - }, - - //// Maximum acceleration - new Column(trans.get("simpanel.col.Maxacceleration")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - FlightData data = document.getSimulation(row).getSimulatedData(); - if (data == null) - return null; - - return UnitGroup.UNITS_ACCELERATION.getDefaultUnit().toStringUnit( - data.getMaxAcceleration()); - } - }, - - //// Time to apogee - new Column(trans.get("simpanel.col.Timetoapogee")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - FlightData data = document.getSimulation(row).getSimulatedData(); - if (data == null) - return null; - - return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit( - data.getTimeToApogee()); - } - }, - - //// Flight time - new Column(trans.get("simpanel.col.Flighttime")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - FlightData data = document.getSimulation(row).getSimulatedData(); - if (data == null) - return null; - - return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit( - data.getFlightTime()); - } - }, - - //// Ground hit velocity - new Column(trans.get("simpanel.col.Groundhitvelocity")) { - @Override - public Object getValueAt(int row) { - if (row < 0 || row >= document.getSimulationCount()) - return null; - - FlightData data = document.getSimulation(row).getSimulatedData(); - if (data == null) - return null; - - return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit( - data.getGroundHitVelocity()); - } - } - - ) { - @Override - public int getRowCount() { - return document.getSimulationCount(); - } - }; - - // Override processKeyBinding so that the JTable does not catch - // key bindings used in menu accelerators - simulationTable = new JTable(simulationTableModel) { - @Override - protected boolean processKeyBinding(KeyStroke ks, - KeyEvent e, - int condition, - boolean pressed) { - return false; - } - }; - simulationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer()); - simulationTableModel.setColumnWidths(simulationTable.getColumnModel()); - - - // Mouse listener to act on double-clicks - simulationTable.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) { - int selected = simulationTable.getSelectedRow(); - if (selected < 0) - return; - - selected = simulationTable.convertRowIndexToModel(selected); - simulationTable.clearSelection(); - simulationTable.addRowSelectionInterval(selected, selected); - - openDialog(document.getSimulations().get(selected), - SimulationEditDialog.DEFAULT); - } - } - }); - - document.addDocumentChangeListener(new DocumentChangeListener() { - @Override - public void documentChanged(DocumentChangeEvent event) { - if (!(event instanceof SimulationChangeEvent)) - return; - simulationTableModel.fireTableDataChanged(); - } - }); - - - - - // Fire table change event when the rocket changes - document.getRocket().addComponentChangeListener(new ComponentChangeListener() { - @Override - public void componentChanged(ComponentChangeEvent e) { - fireMaintainSelection(); - } - }); - - - JScrollPane scrollpane = new JScrollPane(simulationTable); - this.add(scrollpane, "spanx, grow, wrap rel"); - - - } - - - public ListSelectionModel getSimulationListSelectionModel() { - return simulationTable.getSelectionModel(); - } - - private void openDialog(final Simulation sim, int position) { - new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), sim, position) - .setVisible(true); - fireMaintainSelection(); - } - - private void fireMaintainSelection() { - int[] selection = simulationTable.getSelectedRows(); - simulationTableModel.fireTableDataChanged(); - for (int row : selection) { - if (row >= simulationTableModel.getRowCount()) - break; - simulationTable.addRowSelectionInterval(row, row); - } - } - - - private class JLabelRenderer extends DefaultTableCellRenderer { - - @Override - public Component getTableCellRendererComponent(JTable table, - Object value, boolean isSelected, boolean hasFocus, int row, - int column) { - - if (row < 0 || row >= document.getSimulationCount()) - return super.getTableCellRendererComponent(table, value, - isSelected, hasFocus, row, column); - - // A JLabel is self-contained and has set its own tool tip - if (value instanceof JLabel) { - JLabel label = (JLabel) value; - if (isSelected) - label.setBackground(table.getSelectionBackground()); - else - label.setBackground(table.getBackground()); - label.setOpaque(true); - - label.setToolTipText(getSimulationToolTip(document.getSimulation(row))); - return label; - } - - Component component = super.getTableCellRendererComponent(table, value, - isSelected, hasFocus, row, column); - - if (component instanceof JComponent) { - ((JComponent) component).setToolTipText(getSimulationToolTip( - document.getSimulation(row))); - } - return component; - } - - private String getSimulationToolTip(Simulation sim) { - String tip; - FlightData data = sim.getSimulatedData(); - - tip = "<html><b>" + sim.getName() + "</b><br>"; - switch (sim.getStatus()) { - case UPTODATE: - //// <i>Up to date</i><br> - tip += "<i>Up to date</i><br>"; - break; - - case LOADED: - //// <i>Data loaded from a file</i><br> - tip += "<i>Data loaded from a file</i><br>"; - break; - - case OUTDATED: - tip += "<i><font color=\"red\">Data is out of date</font></i><br>"; - tip += "Click <i><b>Run simulations</b></i> to simulate.<br>"; - break; - - case EXTERNAL: - tip += "<i>Imported data</i><br>"; - return tip; - - case NOT_SIMULATED: - tip += "<i>Not simulated yet</i><br>"; - tip += "Click <i><b>Run simulations</b></i> to simulate."; - return tip; - } - - if (data == null) { - tip += "No simulation data available."; - return tip; - } - WarningSet warnings = data.getWarningSet(); - - if (warnings.isEmpty()) { - tip += "<font color=\"gray\">No warnings.</font>"; - return tip; - } - - tip += "<font color=\"red\">Warnings:</font>"; - for (Warning w : warnings) { - tip += "<br>" + w.toString(); - } - - return tip; - } - - } -} diff --git a/src/net/sf/openrocket/gui/main/SimulationRunDialog.java b/src/net/sf/openrocket/gui/main/SimulationRunDialog.java deleted file mode 100644 index c1080305..00000000 --- a/src/net/sf/openrocket/gui/main/SimulationRunDialog.java +++ /dev/null @@ -1,462 +0,0 @@ -package net.sf.openrocket.gui.main; - - -import java.awt.Dialog; -import java.awt.Dimension; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JProgressBar; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.dialogs.DetailDialog; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationCancelledException; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.exception.SimulationLaunchException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.MathUtil; - - -public class SimulationRunDialog extends JDialog { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - /** Update the dialog status every this many ms */ - private static final long UPDATE_MS = 200; - - /** Flight progress at motor burnout */ - private static final double BURNOUT_PROGRESS = 0.4; - - /** Flight progress at apogee */ - private static final double APOGEE_PROGRESS = 0.7; - - - /* - * The executor service is not static since we want concurrent simulation - * dialogs to run in parallel, ie. they both have their own executor service. - */ - private final ExecutorService executor = Executors.newFixedThreadPool( - SwingPreferences.getMaxThreadCount()); - - - private final JLabel simLabel, timeLabel, altLabel, velLabel; - private final JProgressBar progressBar; - - - /* - * NOTE: Care must be used when accessing the simulation parameters, since they - * are being run in another thread. Mutexes are used to avoid concurrent usage, which - * will result in an exception being thrown! - */ - private final Simulation[] simulations; - private final String[] simulationNames; - private final SimulationWorker[] simulationWorkers; - private final SimulationStatus[] simulationStatuses; - private final double[] simulationMaxAltitude; - private final double[] simulationMaxVelocity; - private final boolean[] simulationDone; - - public SimulationRunDialog(Window window, Simulation... simulations) { - //// Running simulations... - super(window, trans.get("SimuRunDlg.title.RunSim"), Dialog.ModalityType.DOCUMENT_MODAL); - - if (simulations.length == 0) { - throw new IllegalArgumentException("Called with no simulations to run"); - } - - this.simulations = simulations; - - - // Randomize the simulation random seeds - for (Simulation sim : simulations) { - sim.getOptions().randomizeSeed(); - } - - // Initialize the simulations - int n = simulations.length; - simulationNames = new String[n]; - simulationWorkers = new SimulationWorker[n]; - simulationStatuses = new SimulationStatus[n]; - simulationMaxAltitude = new double[n]; - simulationMaxVelocity = new double[n]; - simulationDone = new boolean[n]; - - for (int i = 0; i < n; i++) { - simulationNames[i] = simulations[i].getName(); - simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i); - executor.execute(simulationWorkers[i]); - } - - // Build the dialog - JPanel panel = new JPanel(new MigLayout("fill", "[][grow]")); - - //// Running ... - simLabel = new JLabel(trans.get("SimuRunDlg.lbl.Running")); - panel.add(simLabel, "spanx, wrap para"); - //// Simulation time: - panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Simutime") + " "), "gapright para"); - timeLabel = new JLabel(""); - panel.add(timeLabel, "growx, wmin 200lp, wrap rel"); - - //// Altitude: - panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Altitude") + " ")); - altLabel = new JLabel(""); - panel.add(altLabel, "growx, wrap rel"); - - //// Velocity: - panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") + " ")); - velLabel = new JLabel(""); - panel.add(velLabel, "growx, wrap para"); - - progressBar = new JProgressBar(); - panel.add(progressBar, "spanx, growx, wrap para"); - - - // Add cancel button - JButton cancel = new JButton(trans.get("dlg.but.cancel")); - cancel.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - cancelSimulations(); - } - }); - panel.add(cancel, "spanx, tag cancel"); - - - // Cancel simulations when user closes the window - this.addWindowListener(new WindowAdapter() { - @Override - public void windowClosing(WindowEvent e) { - cancelSimulations(); - } - }); - - - this.add(panel); - this.setMinimumSize(new Dimension(300, 0)); - this.setLocationByPlatform(true); - this.validate(); - this.pack(); - - GUIUtil.setDisposableDialogOptions(this, null); - - updateProgress(); - } - - - /** - * Cancel the currently running simulations. This is equivalent to clicking - * the Cancel button on the dialog. - */ - public void cancelSimulations() { - executor.shutdownNow(); - for (SimulationWorker w : simulationWorkers) { - w.cancel(true); - } - } - - - /** - * Static helper method to run simulations. - * - * @param parent the parent Window of the dialog to use. - * @param simulations the simulations to run. - */ - public static void runSimulations(Window parent, Simulation... simulations) { - new SimulationRunDialog(parent, simulations).setVisible(true); - } - - - - - private void updateProgress() { - int index; - for (index = 0; index < simulations.length; index++) { - if (!simulationDone[index]) - break; - } - - if (index >= simulations.length) { - // Everything is done, close the dialog - log.debug("Everything done."); - this.dispose(); - return; - } - - // Update the progress bar status - int progress = 0; - for (SimulationWorker s : simulationWorkers) { - progress += s.getProgress(); - } - progress /= simulationWorkers.length; - progressBar.setValue(progress); - log.debug("Progressbar value " + progress); - - // Update the simulation fields - simLabel.setText("Running " + simulationNames[index]); - if (simulationStatuses[index] == null) { - log.debug("No simulation status data available, setting empty labels"); - timeLabel.setText(""); - altLabel.setText(""); - velLabel.setText(""); - return; - } - - Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit(); - timeLabel.setText(u.toStringUnit(simulationStatuses[index].getSimulationTime())); - - u = UnitGroup.UNITS_DISTANCE.getDefaultUnit(); - altLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketPosition().z) + " (max. " + - u.toStringUnit(simulationMaxAltitude[index]) + ")"); - - u = UnitGroup.UNITS_VELOCITY.getDefaultUnit(); - velLabel.setText(u.toStringUnit(simulationStatuses[index].getRocketVelocity().z) + " (max. " + - u.toStringUnit(simulationMaxVelocity[index]) + ")"); - } - - - - /** - * A SwingWorker that performs a flight simulation. It periodically updates the - * simulation statuses of the parent class and calls updateProgress(). - * The progress of the simulation is stored in the progress property of the - * SwingWorker. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - private class InteractiveSimulationWorker extends SimulationWorker { - - private final int index; - private final double burnoutTimeEstimate; - private volatile double burnoutVelocity; - private volatile double apogeeAltitude; - - /* - * -2 = time from 0 ... burnoutTimeEstimate - * -1 = velocity from v(burnoutTimeEstimate) ... 0 - * 0 ... n = stages from alt(max) ... 0 - */ - private volatile int simulationStage = -2; - - private int progress = 0; - - - public InteractiveSimulationWorker(Simulation sim, int index) { - super(sim); - this.index = index; - - // Calculate estimate of motor burn time - double launchBurn = 0; - double otherBurn = 0; - Configuration config = simulation.getConfiguration(); - String id = simulation.getOptions().getMotorConfigurationID(); - Iterator<MotorMount> iterator = config.motorIterator(); - while (iterator.hasNext()) { - MotorMount m = iterator.next(); - if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH) - launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getBurnTimeEstimate()); - else - otherBurn = otherBurn + m.getMotor(id).getBurnTimeEstimate(); - } - burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1); - - } - - - /** - * Return the extra listeners to use, a progress listener and cancel listener. - */ - @Override - protected SimulationListener[] getExtraListeners() { - return new SimulationListener[] { new SimulationProgressListener() }; - } - - - /** - * Processes simulation statuses published by the simulation listener. - * The statuses of the parent class and the progress property are updated. - */ - @Override - protected void process(List<SimulationStatus> chunks) { - - // Update max. altitude and velocity - for (SimulationStatus s : chunks) { - simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index], - s.getRocketPosition().z); - simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index], - s.getRocketVelocity().length()); - } - - // Calculate the progress - SimulationStatus status = chunks.get(chunks.size() - 1); - simulationStatuses[index] = status; - - // 1. time = 0 ... burnoutTimeEstimate - if (simulationStage == -2 && status.getSimulationTime() < burnoutTimeEstimate) { - log.debug("Method 1: t=" + status.getSimulationTime() + " est=" + burnoutTimeEstimate); - setSimulationProgress(MathUtil.map(status.getSimulationTime(), 0, burnoutTimeEstimate, - 0.0, BURNOUT_PROGRESS)); - updateProgress(); - return; - } - - if (simulationStage == -2) { - simulationStage++; - burnoutVelocity = MathUtil.max(status.getRocketVelocity().z, 0.1); - log.debug("CHANGING to Method 2, vel=" + burnoutVelocity); - } - - // 2. z-velocity from burnout velocity to zero - if (simulationStage == -1 && status.getRocketVelocity().z >= 0) { - log.debug("Method 2: vel=" + status.getRocketVelocity().z + " burnout=" + burnoutVelocity); - setSimulationProgress(MathUtil.map(status.getRocketVelocity().z, burnoutVelocity, 0, - BURNOUT_PROGRESS, APOGEE_PROGRESS)); - updateProgress(); - return; - } - - if (simulationStage == -1 && status.getRocketVelocity().z < 0) { - simulationStage++; - apogeeAltitude = MathUtil.max(status.getRocketPosition().z, 1); - log.debug("CHANGING to Method 3, apogee=" + apogeeAltitude); - } - - // 3. z-position from apogee to zero - // TODO: MEDIUM: several stages - log.debug("Method 3: alt=" + status.getRocketPosition().z + " apogee=" + apogeeAltitude); - setSimulationProgress(MathUtil.map(status.getRocketPosition().z, - apogeeAltitude, 0, APOGEE_PROGRESS, 1.0)); - updateProgress(); - } - - /** - * Marks this simulation as done and calls the progress update. - */ - @Override - protected void simulationDone() { - simulationDone[index] = true; - log.debug("Simulation done"); - setSimulationProgress(1.0); - updateProgress(); - } - - - /** - * Marks the simulation as done and shows a dialog presenting - * the error, unless the simulation was cancelled. - */ - @Override - protected void simulationInterrupted(Throwable t) { - - if (t instanceof SimulationCancelledException) { - simulationDone(); - return; // Ignore cancellations - } - - // Analyze the exception type - if (t instanceof SimulationLaunchException) { - - DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, - new Object[] { - //// Unable to simulate: - trans.get("SimuRunDlg.msg.Unabletosim"), - t.getMessage() - }, - null, simulation.getName(), JOptionPane.ERROR_MESSAGE); - - } else if (t instanceof SimulationException) { - - DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, - new Object[] { - //// A error occurred during the simulation: - trans.get("SimuRunDlg.msg.errorOccurred"), - t.getMessage() - }, - null, simulation.getName(), JOptionPane.ERROR_MESSAGE); - - } else { - - Application.getExceptionHandler().handleErrorCondition("An exception occurred during the simulation", t); - - } - simulationDone(); - } - - - private void setSimulationProgress(double p) { - int exact = Math.max(progress, (int) (100 * p + 0.5)); - progress = MathUtil.clamp(exact, 0, 100); - log.debug("Setting progress to " + progress + " (real " + exact + ")"); - super.setProgress(progress); - } - - - /** - * A simulation listener that regularly updates the progress property of the - * SimulationWorker and publishes the simulation status for the run dialog to process. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - private class SimulationProgressListener extends AbstractSimulationListener { - private long time = 0; - - @Override - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { - switch (event.getType()) { - case APOGEE: - simulationStage = 0; - apogeeAltitude = status.getRocketPosition().z; - log.debug("APOGEE, setting progress"); - setSimulationProgress(APOGEE_PROGRESS); - publish(status); - break; - - case LAUNCH: - publish(status); - break; - - case SIMULATION_END: - log.debug("END, setting progress"); - setSimulationProgress(1.0); - break; - } - return true; - } - - @Override - public void postStep(SimulationStatus status) { - if (System.currentTimeMillis() >= time + UPDATE_MS) { - time = System.currentTimeMillis(); - publish(status); - } - } - } - } -} diff --git a/src/net/sf/openrocket/gui/main/SimulationWorker.java b/src/net/sf/openrocket/gui/main/SimulationWorker.java deleted file mode 100644 index f348fbcd..00000000 --- a/src/net/sf/openrocket/gui/main/SimulationWorker.java +++ /dev/null @@ -1,121 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.util.Arrays; - -import javax.swing.SwingWorker; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationCancelledException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; -import net.sf.openrocket.simulation.listeners.SimulationListener; - - - -/** - * A SwingWorker that runs a simulation in a background thread. The simulation - * always includes a listener that checks whether this SwingWorked has been cancelled, - * and throws a {@link SimulationCancelledException} if it has. This allows the - * {@link #cancel(boolean)} method to be used to cancel the simulation. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class SimulationWorker extends SwingWorker<FlightData, SimulationStatus> { - - protected final Simulation simulation; - private Throwable throwable = null; - - public SimulationWorker(Simulation sim) { - this.simulation = sim; - } - - - /** - * Runs the simulation. - */ - @Override - protected FlightData doInBackground() { - if (isCancelled()) { - throwable = new SimulationCancelledException("The simulation was interrupted."); - return null; - } - - SimulationListener[] listeners = getExtraListeners(); - - if (listeners != null) { - listeners = Arrays.copyOf(listeners, listeners.length + 1); - } else { - listeners = new SimulationListener[1]; - } - - listeners[listeners.length - 1] = new CancelListener(); - - try { - simulation.simulate(listeners); - } catch (Throwable e) { - throwable = e; - return null; - } - return simulation.getSimulatedData(); - } - - - /** - * Return additional listeners to use during the simulation. The default - * implementation returns an empty array. - * - * @return additional listeners to use, or <code>null</code>. - */ - protected SimulationListener[] getExtraListeners() { - return new SimulationListener[0]; - } - - - /** - * Called after a simulation is successfully simulated. This method is not - * called if the simulation ends in an exception. - */ - protected abstract void simulationDone(); - - /** - * Called if the simulation is interrupted due to an exception. - * - * @param t the Throwable that caused the interruption - */ - protected abstract void simulationInterrupted(Throwable t); - - - - /** - * Marks this simulation as done and calls the progress update. - */ - @Override - protected final void done() { - if (throwable == null) - simulationDone(); - else - simulationInterrupted(throwable); - } - - - - /** - * A simulation listener that throws a {@link SimulationCancelledException} if - * this SwingWorker has been cancelled. The conditions is checked every time a step - * is taken. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - private class CancelListener extends AbstractSimulationListener { - - @Override - public void postStep(SimulationStatus status) throws SimulationCancelledException { - - if (isCancelled()) { - throw new SimulationCancelledException("The simulation was interrupted."); - } - - } - } -} diff --git a/src/net/sf/openrocket/gui/main/Splash.java b/src/net/sf/openrocket/gui/main/Splash.java deleted file mode 100644 index 85f3d529..00000000 --- a/src/net/sf/openrocket/gui/main/Splash.java +++ /dev/null @@ -1,93 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.awt.Color; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.RenderingHints; -import java.awt.SplashScreen; -import java.awt.font.GlyphVector; -import java.awt.geom.Rectangle2D; - -import net.sf.openrocket.util.BuildProperties; - -/** - * Helper methods for manipulating the Java runtime splash screen. - * <p> - * Notes: - * SplashScreen.update() takes randomly between 4 and 500 ms to complete, - * even after it has been called ~100 times (and therefore pre-compiled). - * Therefore it cannot be relied upon to perform for example color fades. - * Care should be taken to call update() only once or twice per second. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Splash { - - // The right edge of the text base line for the version string - private static final int VERSION_POSITION_X = 617; - private static final int VERSION_POSITION_Y = 135; - private static final Font VERSION_FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 9); - private static final Color VERSION_COLOR = Color.WHITE; - - - /** - * Initialize the splash screen with additional information. This method should - * be called as soon as reasonably possible during program startup. - * - * @return <code>true</code> if the splash screen could be successfully initialized - */ - public static boolean init() { - // Get the splash screen - SplashScreen s = getSplash(); - if (s == null) - return false; - - // Create graphics context and set antialiasing on - Graphics2D g2 = s.createGraphics(); - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - // Draw the version number - drawVersionNumber(g2); - - // Update the splash screen - s.update(); - return true; - } - - - - private static void drawVersionNumber(Graphics2D g2) { - String text = "Version " + BuildProperties.getVersion(); - GlyphVector gv = VERSION_FONT.createGlyphVector(g2.getFontRenderContext(), text); - - Rectangle2D rect = gv.getVisualBounds(); - double width = rect.getWidth(); - - g2.setColor(VERSION_COLOR); - g2.drawGlyphVector(gv, (float) (VERSION_POSITION_X - width), (float) VERSION_POSITION_Y); - - } - - - /** - * Return the current splash screen or <code>null</code> if not available. - * This method catches the possible exceptions and returns null if they occur. - * - * @return the current splash screen, or <code>null</code>. - */ - private static SplashScreen getSplash() { - try { - return SplashScreen.getSplashScreen(); - } catch (RuntimeException e) { - return null; - } - } - - - -} diff --git a/src/net/sf/openrocket/gui/main/SwingExceptionHandler.java b/src/net/sf/openrocket/gui/main/SwingExceptionHandler.java deleted file mode 100644 index dbaf437c..00000000 --- a/src/net/sf/openrocket/gui/main/SwingExceptionHandler.java +++ /dev/null @@ -1,425 +0,0 @@ -package net.sf.openrocket.gui.main; - -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; - -import net.sf.openrocket.gui.dialogs.BugReportDialog; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.ExceptionHandler; - - -public class SwingExceptionHandler implements Thread.UncaughtExceptionHandler, ExceptionHandler { - - private static final LogHelper log = Application.getLogger(); - - private static final int MEMORY_RESERVE = 512 * 1024; - - /** - * A memory reserve of 0.5 MB of memory, that can be freed when showing the dialog. - * <p> - * This field is package-private so that the JRE cannot optimize its use away. - */ - volatile byte[] memoryReserve = null; - - private volatile boolean handling = false; - - - - - @Override - public void uncaughtException(final Thread thread, final Throwable throwable) { - - // Free memory reserve if out of memory - if (isOutOfMemoryError(throwable)) { - memoryReserve = null; - handling = false; - log.error("Out of memory error detected", throwable); - } - - if (isNonFatalJREBug(throwable)) { - log.warn("Ignoring non-fatal JRE bug", throwable); - return; - } - - log.error("Handling uncaught exception on thread=" + thread, throwable); - throwable.printStackTrace(); - - if (handling) { - log.warn("Exception is currently being handled, ignoring"); - return; - } - - try { - handling = true; - - // Show on the EDT - if (SwingUtilities.isEventDispatchThread()) { - log.info("Exception handler running on EDT, showing dialog"); - showDialog(thread, throwable); - } else { - log.info("Exception handler not on EDT, invoking dialog on EDT"); - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - showDialog(thread, throwable); - } - }); - } - - } catch (Throwable ex) { - - // Make sure the handler does not throw any exceptions - try { - log.error("Caught exception while handling exception", ex); - System.err.println("Exception in exception handler, dumping exception:"); - ex.printStackTrace(); - } catch (Exception ignore) { - } - - } finally { - // Mark handling as completed - handling = false; - } - - } - - - /** - * Handle an error condition programmatically without throwing an exception. - * This can be used in cases where recovery of the error is desirable. - * <p> - * This method is guaranteed never to throw an exception, and can thus be safely - * used in finally blocks. - * - * @param message the error message. - */ - @Override - public void handleErrorCondition(String message) { - log.error(1, message, new TraceException()); - handleErrorCondition(new InternalException(message)); - } - - - /** - * Handle an error condition programmatically without throwing an exception. - * This can be used in cases where recovery of the error is desirable. - * <p> - * This method is guaranteed never to throw an exception, and can thus be safely - * used in finally blocks. - * - * @param message the error message. - * @param exception the exception that occurred. - */ - @Override - public void handleErrorCondition(String message, Throwable exception) { - log.error(1, message, exception); - handleErrorCondition(new InternalException(message, exception)); - } - - - /** - * Handle an error condition programmatically without throwing an exception. - * This can be used in cases where recovery of the error is desirable. - * <p> - * This method is guaranteed never to throw an exception, and can thus be safely - * used in finally blocks. - * - * @param exception the exception that occurred. - */ - @Override - public void handleErrorCondition(final Throwable exception) { - try { - if (!(exception instanceof InternalException)) { - log.error(1, "Error occurred", exception); - } - final Thread thread = Thread.currentThread(); - - if (SwingUtilities.isEventDispatchThread()) { - log.info("Running in EDT, showing dialog"); - this.showDialog(thread, exception); - } else { - log.info("Not in EDT, invoking dialog later"); - final SwingExceptionHandler instance = this; - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - instance.showDialog(thread, exception); - } - }); - } - } catch (Exception e) { - log.error("Exception occurred in error handler", e); - } - } - - - /** - * The actual handling routine. - * - * @param t the thread that caused the exception, or <code>null</code>. - * @param e the exception. - */ - private void showDialog(Thread t, Throwable e) { - - // Out of memory - if (isOutOfMemoryError(e)) { - log.info("Showing out-of-memory dialog"); - JOptionPane.showMessageDialog(null, - new Object[] { - "OpenRocket is out of available memory!", - "You should immediately close unnecessary design windows,", - "save any unsaved designs and restart OpenRocket!" - }, "Out of memory", JOptionPane.ERROR_MESSAGE); - return; - } - - // Create the message - String msg = e.getClass().getSimpleName() + ": " + e.getMessage(); - if (msg.length() > 90) { - msg = msg.substring(0, 80) + "..."; - } - - // Unknown Error - if (!(e instanceof Exception) && !(e instanceof LinkageError)) { - log.info("Showing Error dialog"); - JOptionPane.showMessageDialog(null, - new Object[] { - "An unknown Java error occurred:", - msg, - "<html>You should immediately close unnecessary design windows,<br>" + - "save any unsaved designs and restart OpenRocket!" - }, "Unknown Java error", JOptionPane.ERROR_MESSAGE); - return; - } - - - // Normal exception, show question dialog - log.info("Showing Exception dialog"); - int selection = JOptionPane.showOptionDialog(null, new Object[] { - "OpenRocket encountered an uncaught exception. This typically signifies " + - "a bug in the software.", - "<html><em>        " + msg + "</em>", - " ", - "Please take a moment to report this bug to the developers.", - "This can be done automatically if you have an Internet connection." - }, "Uncaught exception", JOptionPane.DEFAULT_OPTION, - JOptionPane.ERROR_MESSAGE, null, - new Object[] { "View bug report", "Close" }, "View bug report"); - - if (selection != 0) { - // User cancelled - log.user("User chose not to fill bug report"); - return; - } - - // Show bug report dialog - log.user("User requested sending bug report"); - BugReportDialog.showExceptionDialog(null, t, e); - } - - - - /** - * Registers the uncaught exception handler. This should be used to ensure that - * all necessary registrations are performed. - */ - public void registerExceptionHandler() { - - Thread.setDefaultUncaughtExceptionHandler(this); - - // Handler for modal dialogs of Sun's Java implementation - // See bug ID 4499199. - System.setProperty("sun.awt.exception.handler", AwtHandler.class.getName()); - - reserveMemory(); - - } - - - /** - * Reserve the buffer memory that is freed in case an OutOfMemoryError occurs. - */ - private void reserveMemory() { - memoryReserve = new byte[MEMORY_RESERVE]; - for (int i = 0; i < MEMORY_RESERVE; i++) { - memoryReserve[i] = (byte) i; - } - } - - - - /** - * Return whether this throwable was caused by an OutOfMemoryError - * condition. An exception is deemed to be caused by OutOfMemoryError - * if the throwable or any of its causes is of the type OutOfMemoryError. - * <p> - * This method is required because Apple's JRE implementation sometimes - * masks OutOfMemoryErrors within RuntimeExceptions. Idiots. - * - * @param t the throwable to examine. - * @return whether this is an out-of-memory condition. - */ - private boolean isOutOfMemoryError(Throwable t) { - while (t != null) { - if (t instanceof OutOfMemoryError) - return true; - t = t.getCause(); - } - return false; - } - - - - /** - * Handler used in modal dialogs by Sun Java implementation. - */ - public static class AwtHandler { - public void handle(Throwable t) { - Application.getExceptionHandler().uncaughtException(Thread.currentThread(), t); - } - } - - - /** - * Detect various non-fatal Sun JRE bugs. - * - * @param t the throwable - * @return whether this exception should be ignored - */ - private boolean isNonFatalJREBug(Throwable t) { - - // NOTE: Calling method logs the entire throwable, so log only message here - - - /* - * Detect and ignore bug 6826104 in Sun JRE. - */ - if (t instanceof NullPointerException) { - StackTraceElement[] trace = t.getStackTrace(); - - if (trace.length > 3 && - trace[0].getClassName().equals("sun.awt.X11.XWindowPeer") && - trace[0].getMethodName().equals("restoreTransientFor") && - - trace[1].getClassName().equals("sun.awt.X11.XWindowPeer") && - trace[1].getMethodName().equals("removeFromTransientFors") && - - trace[2].getClassName().equals("sun.awt.X11.XWindowPeer") && - trace[2].getMethodName().equals("setModalBlocked")) { - log.warn("Ignoring Sun JRE bug (6826104): http://bugs.sun.com/view_bug.do?bug_id=6826104" + t); - return true; - } - - } - - - /* - * Detect and ignore bug 6828938 in Sun JRE 1.6.0_14 - 1.6.0_16. - */ - if (t instanceof ArrayIndexOutOfBoundsException) { - final String buggyClass = "sun.font.FontDesignMetrics"; - StackTraceElement[] elements = t.getStackTrace(); - if (elements.length >= 3 && - (buggyClass.equals(elements[0].getClassName()) || - buggyClass.equals(elements[1].getClassName()) || - buggyClass.equals(elements[2].getClassName()))) { - log.warn("Ignoring Sun JRE bug 6828938: " + - "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6828938): " + t); - return true; - } - } - - /* - * Detect and ignore bug 6561072 in Sun JRE 1.6.0_? - */ - if (t instanceof NullPointerException) { - StackTraceElement[] trace = t.getStackTrace(); - - if (trace.length > 3 && - trace[0].getClassName().equals("javax.swing.JComponent") && - trace[0].getMethodName().equals("repaint") && - - trace[1].getClassName().equals("sun.swing.FilePane$2") && - trace[1].getMethodName().equals("repaintListSelection") && - - trace[2].getClassName().equals("sun.swing.FilePane$2") && - trace[2].getMethodName().equals("repaintSelection")) { - log.warn("Ignoring Sun JRE bug 6561072 " + - "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6561072): " + t); - return true; - } - } - - - /* - * Detect and ignore bug 6933331 in Sun JRE 1.6.0_18 and others - */ - if (t instanceof IllegalStateException) { - StackTraceElement[] trace = t.getStackTrace(); - - if (trace.length > 1 && - trace[0].getClassName().equals("sun.awt.windows.WComponentPeer") && - trace[0].getMethodName().equals("getBackBuffer")) { - log.warn("Ignoring Sun JRE bug 6933331 " + - "(see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6933331): " + t); - return true; - } - } - - /* - * Detect and ignore bug in Sun JRE 1.6.0_19 - */ - if (t instanceof NullPointerException) { - StackTraceElement[] trace = t.getStackTrace(); - - if (trace.length > 3 && - trace[0].getClassName().equals("sun.awt.shell.Win32ShellFolder2") && - trace[0].getMethodName().equals("pidlsEqual") && - - trace[1].getClassName().equals("sun.awt.shell.Win32ShellFolder2") && - trace[1].getMethodName().equals("equals") && - - trace[2].getClassName().equals("sun.awt.shell.Win32ShellFolderManager2") && - trace[2].getMethodName().equals("isFileSystemRoot")) { - log.warn("Ignoring Sun JRE bug " + - "(see http://forums.sun.com/thread.jspa?threadID=5435324): " + t); - return true; - } - } - - /* - * Detect Sun JRE bug in D3D - */ - if (t instanceof ClassCastException) { - if (t.getMessage().equals("sun.awt.Win32GraphicsConfig cannot be cast to sun.java2d.d3d.D3DGraphicsConfig")) { - log.warn("Ignoring Sun JRE bug " + - "(see http://forums.sun.com/thread.jspa?threadID=5440525): " + t); - return true; - } - } - - return false; - } - - - @SuppressWarnings("unused") - private static class InternalException extends Exception { - public InternalException() { - super(); - } - - public InternalException(String message, Throwable cause) { - super(message, cause); - } - - public InternalException(String message) { - super(message); - } - - public InternalException(Throwable cause) { - super(cause); - } - } -} diff --git a/src/net/sf/openrocket/gui/main/UndoRedoAction.java b/src/net/sf/openrocket/gui/main/UndoRedoAction.java deleted file mode 100644 index 08c31c7e..00000000 --- a/src/net/sf/openrocket/gui/main/UndoRedoAction.java +++ /dev/null @@ -1,104 +0,0 @@ -package net.sf.openrocket.gui.main; - -import java.awt.event.ActionEvent; - -import javax.swing.AbstractAction; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.UndoRedoListener; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; - -/** - * Inner class to implement undo/redo actions. - */ -public class UndoRedoAction extends AbstractAction implements UndoRedoListener { - - // Use Factory mechanism because we want to register the new instance as an - // UndoRedoListener. - public static UndoRedoAction newUndoAction( OpenRocketDocument document ) { - UndoRedoAction undo = new UndoRedoAction( UNDO, document ); - document.addUndoRedoListener(undo); - return undo; - } - - public static UndoRedoAction newRedoAction( OpenRocketDocument document ) { - UndoRedoAction redo = new UndoRedoAction( REDO, document ); - document.addUndoRedoListener(redo); - return redo; - } - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private static final int UNDO = 1; - private static final int REDO = 2; - - private final int type; - - private final OpenRocketDocument document; - - // Sole constructor - private UndoRedoAction(int type, OpenRocketDocument document) { - this.document = document; - if (type != UNDO && type != REDO) { - throw new IllegalArgumentException("Unknown type = " + type); - } - this.type = type; - setAllValues(); - } - - - // Actual action to make - @Override - public void actionPerformed(ActionEvent e) { - switch (type) { - case UNDO: - log.user("Performing undo, event=" + e); - document.undo(); - break; - - case REDO: - log.user("Performing redo, event=" + e); - document.redo(); - break; - } - } - - - // Set all the values correctly (name and enabled/disabled status) - public void setAllValues() { - String name, desc; - boolean actionEnabled; - - switch (type) { - case UNDO: - //// Undo - name = trans.get("OpenRocketDocument.Undo"); - desc = document.getUndoDescription(); - actionEnabled = document.isUndoAvailable(); - this.putValue(SMALL_ICON, Icons.EDIT_UNDO); - break; - - case REDO: - ////Redo - name = trans.get("OpenRocketDocument.Redo"); - desc = document.getRedoDescription(); - actionEnabled = document.isRedoAvailable(); - this.putValue(SMALL_ICON, Icons.EDIT_REDO); - break; - - default: - throw new BugException("illegal type=" + type); - } - - if (desc != null) - name = name + " (" + desc + ")"; - - putValue(NAME, name); - setEnabled(actionEnabled); - } -} diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java deleted file mode 100644 index f78bfd00..00000000 --- a/src/net/sf/openrocket/gui/main/componenttree/ComponentTree.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.sf.openrocket.gui.main.componenttree; - -import javax.swing.DropMode; -import javax.swing.ToolTipManager; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.components.BasicTree; - - -public class ComponentTree extends BasicTree { - - public ComponentTree(OpenRocketDocument document) { - super(); - this.setModel(new ComponentTreeModel(document.getRocket(), this)); - - this.setCellRenderer(new ComponentTreeRenderer()); - - this.setDragEnabled(true); - this.setDropMode(DropMode.INSERT); - this.setTransferHandler(new ComponentTreeTransferHandler(document)); - - // Expand whole tree by default - expandTree(); - - // Enable tooltips for this component - ToolTipManager.sharedInstance().registerComponent(this); - - } - - - -} diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java deleted file mode 100644 index fe141f61..00000000 --- a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeModel.java +++ /dev/null @@ -1,226 +0,0 @@ -package net.sf.openrocket.gui.main.componenttree; - -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import javax.swing.JTree; -import javax.swing.event.TreeModelEvent; -import javax.swing.event.TreeModelListener; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreePath; - -import net.sf.openrocket.rocketcomponent.ComponentChangeEvent; -import net.sf.openrocket.rocketcomponent.ComponentChangeListener; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.BugException; - - -/** - * A TreeModel that implements viewing of the rocket tree structure. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class ComponentTreeModel implements TreeModel, ComponentChangeListener { - ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>(); - - private final RocketComponent root; - private final JTree tree; - - public ComponentTreeModel(RocketComponent root, JTree tree) { - this.root = root; - this.tree = tree; - root.addComponentChangeListener(this); - } - - - @Override - public Object getChild(Object parent, int index) { - RocketComponent component = (RocketComponent) parent; - - try { - return component.getChild(index); - } catch (IndexOutOfBoundsException e) { - return null; - } - } - - - @Override - public int getChildCount(Object parent) { - RocketComponent c = (RocketComponent) parent; - - return c.getChildCount(); - } - - - @Override - public int getIndexOfChild(Object parent, Object child) { - if (parent == null || child == null) - return -1; - - RocketComponent p = (RocketComponent) parent; - RocketComponent c = (RocketComponent) child; - - return p.getChildPosition(c); - } - - @Override - public Object getRoot() { - return root; - } - - @Override - public boolean isLeaf(Object node) { - return !((RocketComponent) node).allowsChildren(); - } - - @Override - public void addTreeModelListener(TreeModelListener l) { - listeners.add(l); - } - - @Override - public void removeTreeModelListener(TreeModelListener l) { - listeners.remove(l); - } - - private void fireTreeNodeChanged(RocketComponent node) { - TreeModelEvent e = new TreeModelEvent(this, makeTreePath(node), null, null); - Object[] l = listeners.toArray(); - for (int i = 0; i < l.length; i++) - ((TreeModelListener) l[i]).treeNodesChanged(e); - } - - - private void fireTreeStructureChanged(RocketComponent source) { - Object[] path = { root }; - - - // Get currently expanded path IDs - Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path)); - ArrayList<String> expanded = new ArrayList<String>(); - if (enumer != null) { - while (enumer.hasMoreElements()) { - TreePath p = enumer.nextElement(); - expanded.add(((RocketComponent) p.getLastPathComponent()).getID()); - } - } - - // Send structure change event - TreeModelEvent e = new TreeModelEvent(this, path); - Object[] l = listeners.toArray(); - for (int i = 0; i < l.length; i++) - ((TreeModelListener) l[i]).treeStructureChanged(e); - - // Re-expand the paths - for (String id : expanded) { - RocketComponent c = root.findComponent(id); - if (c == null) - continue; - tree.expandPath(makeTreePath(c)); - } - if (source != null) { - TreePath p = makeTreePath(source); - tree.makeVisible(p); - tree.expandPath(p); - } - } - - @Override - public void valueForPathChanged(TreePath path, Object newValue) { - System.err.println("ERROR: valueForPathChanged called?!"); - } - - - @Override - public void componentChanged(ComponentChangeEvent e) { - if (e.isTreeChange() || e.isUndoChange()) { - // Tree must be fully updated also in case of an undo change - fireTreeStructureChanged(e.getSource()); - if (e.isTreeChange() && e.isUndoChange()) { - // If the undo has changed the tree structure, some elements may be hidden - // unnecessarily - // TODO: LOW: Could this be performed better? - expandAll(); - } - } else if (e.isOtherChange()) { - fireTreeNodeChanged(e.getSource()); - } - } - - public void expandAll() { - Iterator<RocketComponent> iterator = root.iterator(false); - while (iterator.hasNext()) { - tree.makeVisible(makeTreePath(iterator.next())); - } - } - - - /** - * Return the rocket component that a TreePath object is referring to. - * - * @param path the TreePath - * @return the RocketComponent the path is referring to. - * @throws NullPointerException if path is null - * @throws BugException if the path does not refer to a RocketComponent - */ - public static RocketComponent componentFromPath(TreePath path) { - Object last = path.getLastPathComponent(); - if (!(last instanceof RocketComponent)) { - throw new BugException("Tree path does not refer to a RocketComponent: " + path.toString()); - } - return (RocketComponent) last; - } - - - /** - * Return a TreePath corresponding to the specified rocket component. - * - * @param component the rocket component - * @return a TreePath corresponding to this RocketComponent - */ - public static TreePath makeTreePath(RocketComponent component) { - if (component == null) { - throw new NullPointerException(); - } - - RocketComponent c = component; - - List<RocketComponent> list = new LinkedList<RocketComponent>(); - - while (c != null) { - list.add(0, c); - c = c.getParent(); - } - - return new TreePath(list.toArray()); - } - - - /** - * Return a string describing the path, using component normal names. - * - * @param treePath the tree path - * @return a string representation - */ - public static String pathToString(TreePath treePath) { - StringBuffer sb = new StringBuffer(); - sb.append("["); - for (Object o : treePath.getPath()) { - if (sb.length() > 1) { - sb.append("; "); - } - if (o instanceof RocketComponent) { - sb.append(((RocketComponent) o).getComponentName()); - } else { - sb.append(o.toString()); - } - } - sb.append("]"); - return sb.toString(); - } -} diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java deleted file mode 100644 index a7e8386e..00000000 --- a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeRenderer.java +++ /dev/null @@ -1,45 +0,0 @@ -package net.sf.openrocket.gui.main.componenttree; - - - -import java.awt.Component; - -import javax.swing.JTree; -import javax.swing.tree.DefaultTreeCellRenderer; - -import net.sf.openrocket.gui.main.ComponentIcons; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.TextUtil; - -public class ComponentTreeRenderer extends DefaultTreeCellRenderer { - - @Override - public Component getTreeCellRendererComponent( - JTree tree, - Object value, - boolean sel, - boolean expanded, - boolean leaf, - int row, - boolean hasFocus) { - - super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); - - // Set icon - setIcon(ComponentIcons.getSmallIcon(value.getClass())); - - // Set tooltip - RocketComponent c = (RocketComponent) value; - String comment = c.getComment().trim(); - if (comment.length() > 0) { - comment = TextUtil.htmlEncode(comment); - comment = "<html>" + comment.replace("\n", "<br>"); - this.setToolTipText(comment); - } else { - this.setToolTipText(null); - } - - return this; - } - -} diff --git a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java b/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java deleted file mode 100644 index 4855112b..00000000 --- a/src/net/sf/openrocket/gui/main/componenttree/ComponentTreeTransferHandler.java +++ /dev/null @@ -1,362 +0,0 @@ -package net.sf.openrocket.gui.main.componenttree; - -import java.awt.Point; -import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.io.IOException; -import java.util.Arrays; - -import javax.swing.JComponent; -import javax.swing.JTree; -import javax.swing.SwingUtilities; -import javax.swing.TransferHandler; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreePath; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; - -/** - * A TransferHandler that handles dragging components from and to a ComponentTree. - * Supports both moving and copying (only copying when dragging to a different rocket). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ComponentTreeTransferHandler extends TransferHandler { - - private static final LogHelper log = Application.getLogger(); - - private final OpenRocketDocument document; - - /** - * Sole constructor. - * - * @param document the document this handler will drop to, used for undo actions. - */ - public ComponentTreeTransferHandler(OpenRocketDocument document) { - this.document = document; - } - - @Override - public int getSourceActions(JComponent comp) { - return COPY_OR_MOVE; - } - - @Override - public Transferable createTransferable(JComponent component) { - if (!(component instanceof JTree)) { - throw new BugException("TransferHandler called with component " + component); - } - - JTree tree = (JTree) component; - TreePath path = tree.getSelectionPath(); - if (path == null) { - return null; - } - - RocketComponent c = ComponentTreeModel.componentFromPath(path); - if (c instanceof Rocket) { - log.info("Attempting to create transferable from Rocket"); - return null; - } - - log.info("Creating transferable from component " + c.getComponentName()); - return new RocketComponentTransferable(c); - } - - - - - @Override - public void exportDone(JComponent comp, Transferable trans, int action) { - // Removal from the old place is implemented already in import, so do nothing - } - - - - @Override - public boolean canImport(TransferHandler.TransferSupport support) { - SourceTarget data = getSourceAndTarget(support); - - if (data == null) { - return false; - } - - boolean allowed = data.destParent.isCompatible(data.child); - log.verbose("Checking validity of drag-drop " + data.toString() + " allowed:" + allowed); - - // If drag-dropping to another rocket always copy - if (support.getDropAction() == MOVE && data.srcParent.getRoot() != data.destParent.getRoot()) { - support.setDropAction(COPY); - } - - return allowed; - } - - - - @Override - public boolean importData(TransferHandler.TransferSupport support) { - - // We currently only support drop, not paste - if (!support.isDrop()) { - log.warn("Import action is not a drop action"); - return false; - } - - // Sun JRE silently ignores any RuntimeExceptions in importData, yeech! - try { - - SourceTarget data = getSourceAndTarget(support); - - // Check what action to perform - int action = support.getDropAction(); - if (data.srcParent.getRoot() != data.destParent.getRoot()) { - // If drag-dropping to another rocket always copy - log.info("Performing DnD between different rockets, forcing copy action"); - action = TransferHandler.COPY; - } - - - // Check whether move action would be a no-op - if ((action == MOVE) && (data.srcParent == data.destParent) && - (data.destIndex == data.srcIndex || data.destIndex == data.srcIndex + 1)) { - log.user("Dropped component at the same place as previously: " + data); - return false; - } - - - switch (action) { - case MOVE: - log.user("Performing DnD move operation: " + data); - - // If parents are the same, check whether removing the child changes the insert position - int index = data.destIndex; - if (data.srcParent == data.destParent && data.srcIndex < data.destIndex) { - index--; - } - - // Mark undo and freeze rocket. src and dest are in same rocket, need to freeze only one - try { - document.startUndo("Move component"); - try { - data.srcParent.getRocket().freeze(); - data.srcParent.removeChild(data.srcIndex); - data.destParent.addChild(data.child, index); - } finally { - data.srcParent.getRocket().thaw(); - } - } finally { - document.stopUndo(); - } - return true; - - case COPY: - log.user("Performing DnD copy operation: " + data); - RocketComponent copy = data.child.copy(); - try { - document.startUndo("Copy component"); - data.destParent.addChild(copy, data.destIndex); - } finally { - document.stopUndo(); - } - return true; - - default: - log.warn("Unknown transfer action " + action); - return false; - } - - } catch (final RuntimeException e) { - // Open error dialog later if an exception has occurred - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - Application.getExceptionHandler().handleErrorCondition(e); - } - }); - return false; - } - } - - - - /** - * Fetch the source and target for the DnD action. This method does not perform - * checks on whether this action is allowed based on component positioning rules. - * - * @param support the transfer support - * @return the source and targer, or <code>null</code> if invalid. - */ - private SourceTarget getSourceAndTarget(TransferHandler.TransferSupport support) { - // We currently only support drop, not paste - if (!support.isDrop()) { - log.warn("Import action is not a drop action"); - return null; - } - - // we only import RocketComponentTransferable - if (!support.isDataFlavorSupported(RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR)) { - log.debug("Attempting to import data with data flavors " + - Arrays.toString(support.getTransferable().getTransferDataFlavors())); - return null; - } - - // Fetch the drop location and convert it to work around bug 6560955 - JTree.DropLocation dl = (JTree.DropLocation) support.getDropLocation(); - if (dl.getPath() == null) { - log.debug("No drop path location available"); - return null; - } - MyDropLocation location = convertDropLocation((JTree) support.getComponent(), dl); - - - // Fetch the transferred component (child component) - Transferable transferable = support.getTransferable(); - RocketComponent child; - - try { - child = (RocketComponent) transferable.getTransferData( - RocketComponentTransferable.ROCKET_COMPONENT_DATA_FLAVOR); - } catch (IOException e) { - throw new BugException(e); - } catch (UnsupportedFlavorException e) { - throw new BugException(e); - } - - - // Get the source component & index - RocketComponent srcParent = child.getParent(); - if (srcParent == null) { - log.debug("Attempting to drag root component"); - return null; - } - int srcIndex = srcParent.getChildPosition(child); - - - // Get destination component & index - RocketComponent destParent = ComponentTreeModel.componentFromPath(location.path); - int destIndex = location.index; - if (destIndex < 0) { - destIndex = 0; - } - - return new SourceTarget(srcParent, srcIndex, destParent, destIndex, child); - } - - private class SourceTarget { - private final RocketComponent srcParent; - private final int srcIndex; - private final RocketComponent destParent; - private final int destIndex; - private final RocketComponent child; - - public SourceTarget(RocketComponent srcParent, int srcIndex, RocketComponent destParent, int destIndex, - RocketComponent child) { - this.srcParent = srcParent; - this.srcIndex = srcIndex; - this.destParent = destParent; - this.destIndex = destIndex; - this.child = child; - } - - @Override - public String toString() { - return "[" + - "srcParent=" + srcParent.getComponentName() + - ", srcIndex=" + srcIndex + - ", destParent=" + destParent.getComponentName() + - ", destIndex=" + destIndex + - ", child=" + child.getComponentName() + - "]"; - } - - } - - - - /** - * Convert the JTree drop location in order to work around bug 6560955 - * (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6560955). - * <p> - * This method analyzes whether the user is dropping on top of the last component - * of a subtree or the next item in the tree. The case to fix must fulfill the following - * requirements: - * <ul> - * <li> The node before the current insertion node is not a leaf node - * <li> The drop point is on top of the last node of that node - * </ul> - * <p> - * This does not fix the visual clue provided to the user, but fixes the actual drop location. - * - * @param tree the JTree in question - * @param location the original drop location - * @return the updated drop location - */ - private MyDropLocation convertDropLocation(JTree tree, JTree.DropLocation location) { - - final TreePath originalPath = location.getPath(); - final int originalIndex = location.getChildIndex(); - - if (originalPath == null || originalIndex <= 0) { - return new MyDropLocation(location); - } - - // Check whether previous node is a leaf node - TreeModel model = tree.getModel(); - Object previousNode = model.getChild(originalPath.getLastPathComponent(), originalIndex - 1); - if (model.isLeaf(previousNode)) { - return new MyDropLocation(location); - } - - // Find node on top of which the drop occurred - Point point = location.getDropPoint(); - TreePath dropPath = tree.getPathForLocation(point.x, point.y); - if (dropPath == null) { - return new MyDropLocation(location); - } - - // Check whether previousNode is in the ancestry of the actual drop location - boolean inAncestry = false; - for (Object o : dropPath.getPath()) { - if (o == previousNode) { - inAncestry = true; - break; - } - } - if (!inAncestry) { - return new MyDropLocation(location); - } - - // The bug has occurred - insert after the actual drop location - TreePath correctInsertPath = dropPath.getParentPath(); - int correctInsertIndex = model.getIndexOfChild(correctInsertPath.getLastPathComponent(), - dropPath.getLastPathComponent()) + 1; - - log.verbose("Working around Sun JRE bug 6560955: " + - "converted path=" + ComponentTreeModel.pathToString(originalPath) + " index=" + originalIndex + - " into path=" + ComponentTreeModel.pathToString(correctInsertPath) + - " index=" + correctInsertIndex); - - return new MyDropLocation(correctInsertPath, correctInsertIndex); - } - - private class MyDropLocation { - private final TreePath path; - private final int index; - - public MyDropLocation(JTree.DropLocation location) { - this(location.getPath(), location.getChildIndex()); - } - - public MyDropLocation(TreePath path, int index) { - this.path = path; - this.index = index; - } - - } -} diff --git a/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java b/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java deleted file mode 100644 index e9379dae..00000000 --- a/src/net/sf/openrocket/gui/main/componenttree/RocketComponentTransferable.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.sf.openrocket.gui.main.componenttree; - -import java.awt.datatransfer.DataFlavor; -import java.awt.datatransfer.Transferable; -import java.awt.datatransfer.UnsupportedFlavorException; -import java.io.IOException; - -import net.sf.openrocket.rocketcomponent.RocketComponent; - -/** - * A transferable that provides a reference to a (JVM-local) RocketComponent object. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocketComponentTransferable implements Transferable { - - public static final DataFlavor ROCKET_COMPONENT_DATA_FLAVOR = new DataFlavor( - DataFlavor.javaJVMLocalObjectMimeType + "; class=" + RocketComponent.class.getCanonicalName(), - "OpenRocket component"); - - - private final RocketComponent component; - - public RocketComponentTransferable(RocketComponent component) { - this.component = component; - } - - - @Override - public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { - if (!isDataFlavorSupported(flavor)) { - throw new UnsupportedFlavorException(flavor); - } - return component; - } - - @Override - public DataFlavor[] getTransferDataFlavors() { - return new DataFlavor[] { ROCKET_COMPONENT_DATA_FLAVOR }; - } - - @Override - public boolean isDataFlavorSupported(DataFlavor flavor) { - return flavor.equals(ROCKET_COMPONENT_DATA_FLAVOR); - } - -} diff --git a/src/net/sf/openrocket/gui/plot/Axis.java b/src/net/sf/openrocket/gui/plot/Axis.java deleted file mode 100644 index 3f97ad36..00000000 --- a/src/net/sf/openrocket/gui/plot/Axis.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.sf.openrocket.gui.plot; - -import net.sf.openrocket.util.BugException; - -public class Axis implements Cloneable { - - private double minValue = Double.NaN; - private double maxValue = Double.NaN; - - - - public void addBound(double value) { - - if (value < minValue || Double.isNaN(minValue)) { - minValue = value; - } - if (value > maxValue || Double.isNaN(maxValue)) { - maxValue = value; - } - - } - - - public double getMinValue() { - return minValue; - } - - public double getMaxValue() { - return maxValue; - } - - public double getRangeLength() { - return maxValue - minValue; - } - - public void reset() { - minValue = Double.NaN; - maxValue = Double.NaN; - } - - - - @Override - public Axis clone() { - try { - - return (Axis) super.clone(); - - } catch (CloneNotSupportedException e) { - throw new BugException("BUG! Could not clone()."); - } - } - -} diff --git a/src/net/sf/openrocket/gui/plot/PlotConfiguration.java b/src/net/sf/openrocket/gui/plot/PlotConfiguration.java deleted file mode 100644 index bd58782f..00000000 --- a/src/net/sf/openrocket/gui/plot/PlotConfiguration.java +++ /dev/null @@ -1,754 +0,0 @@ -package net.sf.openrocket.gui.plot; - -import java.util.EnumSet; -import java.util.List; -import java.util.Set; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Pair; - - -public class PlotConfiguration implements Cloneable { - - private static final Translator trans = Application.getTranslator(); - - public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS; - static { - ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>(); - PlotConfiguration config; - - //// Vertical motion vs. time - config = new PlotConfiguration(trans.get("PlotConfiguration.Verticalmotion")); - config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); - config.addPlotDataType(FlightDataType.TYPE_VELOCITY_Z); - config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_Z); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - //// Total motion vs. time - config = new PlotConfiguration(trans.get("PlotConfiguration.Totalmotion")); - config.addPlotDataType(FlightDataType.TYPE_ALTITUDE, 0); - config.addPlotDataType(FlightDataType.TYPE_VELOCITY_TOTAL); - config.addPlotDataType(FlightDataType.TYPE_ACCELERATION_TOTAL); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - //// Flight side profile - config = new PlotConfiguration(trans.get("PlotConfiguration.Flightside"), FlightDataType.TYPE_POSITION_X); - config.addPlotDataType(FlightDataType.TYPE_ALTITUDE); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - //// Stability vs. time - config = new PlotConfiguration("Stability vs. time"); - config.addPlotDataType(FlightDataType.TYPE_STABILITY, 0); - config.addPlotDataType(FlightDataType.TYPE_CP_LOCATION, 1); - config.addPlotDataType(FlightDataType.TYPE_CG_LOCATION, 1); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - //// Drag coefficients vs. Mach number - config = new PlotConfiguration("Drag coefficients vs. Mach number", - FlightDataType.TYPE_MACH_NUMBER); - config.addPlotDataType(FlightDataType.TYPE_DRAG_COEFF, 0); - config.addPlotDataType(FlightDataType.TYPE_FRICTION_DRAG_COEFF, 0); - config.addPlotDataType(FlightDataType.TYPE_BASE_DRAG_COEFF, 0); - config.addPlotDataType(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, 0); - configs.add(config); - - //// Roll characteristics - config = new PlotConfiguration("Roll characteristics"); - config.addPlotDataType(FlightDataType.TYPE_ROLL_RATE, 0); - config.addPlotDataType(FlightDataType.TYPE_ROLL_MOMENT_COEFF, 1); - config.addPlotDataType(FlightDataType.TYPE_ROLL_FORCING_COEFF, 1); - config.addPlotDataType(FlightDataType.TYPE_ROLL_DAMPING_COEFF, 1); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.LAUNCHROD, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - //// Angle of attack and orientation vs. time - config = new PlotConfiguration("Angle of attack and orientation vs. time"); - config.addPlotDataType(FlightDataType.TYPE_AOA, 0); - config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_PHI); - config.addPlotDataType(FlightDataType.TYPE_ORIENTATION_THETA); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - //// Simulation time step and computation time - config = new PlotConfiguration("Simulation time step and computation time"); - config.addPlotDataType(FlightDataType.TYPE_TIME_STEP); - config.addPlotDataType(FlightDataType.TYPE_COMPUTATION_TIME); - config.setEvent(FlightEvent.Type.IGNITION, true); - config.setEvent(FlightEvent.Type.BURNOUT, true); - config.setEvent(FlightEvent.Type.APOGEE, true); - config.setEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, true); - config.setEvent(FlightEvent.Type.STAGE_SEPARATION, true); - config.setEvent(FlightEvent.Type.GROUND_HIT, true); - configs.add(config); - - DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]); - } - - - - /** Bonus given for the first type being on the first axis */ - private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0; - - /** - * Bonus given if the first axis includes zero (to prefer first axis having zero over - * the others) - */ - private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0; - - /** Bonus given for a common zero point on left and right axes. */ - private static final double BONUS_COMMON_ZERO = 40.0; - - /** Bonus given for only using a single axis. */ - private static final double BONUS_ONLY_ONE_AXIS = 50.0; - - - private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range - - - - /** The data types to be plotted. */ - private ArrayList<FlightDataType> plotDataTypes = new ArrayList<FlightDataType>(); - - private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>(); - - /** The corresponding Axis on which they will be plotted, or null to auto-select. */ - private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>(); - - private EnumSet<FlightEvent.Type> events = EnumSet.noneOf(FlightEvent.Type.class); - - /** The domain (x) axis. */ - private FlightDataType domainAxisType = null; - private Unit domainAxisUnit = null; - - - /** All available axes. */ - private final int axesCount; - private ArrayList<Axis> allAxes = new ArrayList<Axis>(); - - - - private String name = null; - - - - public PlotConfiguration() { - this(null, FlightDataType.TYPE_TIME); - } - - public PlotConfiguration(String name) { - this(name, FlightDataType.TYPE_TIME); - } - - public PlotConfiguration(String name, FlightDataType domainType) { - this.name = name; - // Two axes - allAxes.add(new Axis()); - allAxes.add(new Axis()); - axesCount = 2; - - setDomainAxisType(domainType); - } - - - - - - public FlightDataType getDomainAxisType() { - return domainAxisType; - } - - public void setDomainAxisType(FlightDataType type) { - boolean setUnit; - - if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup()) - setUnit = false; - else - setUnit = true; - - domainAxisType = type; - if (setUnit) - domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit(); - } - - public Unit getDomainAxisUnit() { - return domainAxisUnit; - } - - public void setDomainAxisUnit(Unit u) { - if (!domainAxisType.getUnitGroup().contains(u)) { - throw new IllegalArgumentException("Setting unit " + u + " to type " + domainAxisType); - } - domainAxisUnit = u; - } - - - - public void addPlotDataType(FlightDataType type) { - plotDataTypes.add(type); - plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); - plotDataAxes.add(-1); - } - - public void addPlotDataType(FlightDataType type, int axis) { - if (axis >= axesCount) { - throw new IllegalArgumentException("Axis index too large"); - } - plotDataTypes.add(type); - plotDataUnits.add(type.getUnitGroup().getDefaultUnit()); - plotDataAxes.add(axis); - } - - - - - public void setPlotDataType(int index, FlightDataType type) { - FlightDataType origType = plotDataTypes.get(index); - plotDataTypes.set(index, type); - - if (origType.getUnitGroup() != type.getUnitGroup()) { - plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit()); - } - } - - public void setPlotDataUnit(int index, Unit unit) { - if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) { - throw new IllegalArgumentException("Attempting to set unit " + unit + " to group " - + plotDataTypes.get(index).getUnitGroup()); - } - plotDataUnits.set(index, unit); - } - - public void setPlotDataAxis(int index, int axis) { - if (axis >= axesCount) { - throw new IllegalArgumentException("Axis index too large"); - } - plotDataAxes.set(index, axis); - } - - - public void setPlotDataType(int index, FlightDataType type, Unit unit, int axis) { - if (axis >= axesCount) { - throw new IllegalArgumentException("Axis index too large"); - } - plotDataTypes.set(index, type); - plotDataUnits.set(index, unit); - plotDataAxes.set(index, axis); - } - - public void removePlotDataType(int index) { - plotDataTypes.remove(index); - plotDataUnits.remove(index); - plotDataAxes.remove(index); - } - - - - public FlightDataType getType(int index) { - return plotDataTypes.get(index); - } - - public Unit getUnit(int index) { - return plotDataUnits.get(index); - } - - public int getAxis(int index) { - return plotDataAxes.get(index); - } - - public int getTypeCount() { - return plotDataTypes.size(); - } - - - /// Events - - public Set<FlightEvent.Type> getActiveEvents() { - return events.clone(); - } - - public void setEvent(FlightEvent.Type type, boolean active) { - if (active) { - events.add(type); - } else { - events.remove(type); - } - } - - public boolean isEventActive(FlightEvent.Type type) { - return events.contains(type); - } - - - - - - public List<Axis> getAllAxes() { - List<Axis> list = new ArrayList<Axis>(); - list.addAll(allAxes); - return list; - } - - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - /** - * Returns the name of this PlotConfiguration. - */ - @Override - public String toString() { - return name; - } - - - - /** - * Find the best combination of the auto-selectable axes. - * - * @return a new PlotConfiguration with the best fitting auto-selected axes and - * axes ranges selected. - */ - public PlotConfiguration fillAutoAxes(FlightDataBranch data) { - PlotConfiguration config = recursiveFillAutoAxes(data).getU(); - System.out.println("BEST FOUND, fitting"); - config.fitAxes(data); - return config; - } - - - - - /** - * Recursively search for the best combination of the auto-selectable axes. - * This is a brute-force search method. - * - * @return a new PlotConfiguration with the best fitting auto-selected axes and - * axes ranges selected, and the goodness value - */ - private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) { - - // Create copy to fill in - PlotConfiguration copy = this.clone(); - - int autoindex; - for (autoindex = 0; autoindex < plotDataAxes.size(); autoindex++) { - if (plotDataAxes.get(autoindex) < 0) - break; - } - - - if (autoindex >= plotDataAxes.size()) { - // All axes have been assigned, just return since we are already the best - return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data)); - } - - - // Set the auto-selected index one at a time and choose the best one - PlotConfiguration best = null; - double bestValue = Double.NEGATIVE_INFINITY; - for (int i = 0; i < axesCount; i++) { - copy.plotDataAxes.set(autoindex, i); - Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data); - if (result.getV() > bestValue) { - best = result.getU(); - bestValue = result.getV(); - } - } - - return new Pair<PlotConfiguration, Double>(best, bestValue); - } - - - - - - /** - * Fit the axes to hold the provided data. All of the plotDataAxis elements must - * be non-negative. - * <p> - * NOTE: This method assumes that only two axes are used. - */ - protected void fitAxes(FlightDataBranch data) { - - // Reset axes - for (Axis a : allAxes) { - a.reset(); - } - - // Add full range to the axes - int length = plotDataTypes.size(); - for (int i = 0; i < length; i++) { - FlightDataType type = plotDataTypes.get(i); - Unit unit = plotDataUnits.get(i); - int index = plotDataAxes.get(i); - if (index < 0) { - throw new IllegalStateException("fitAxes called with auto-selected axis"); - } - Axis axis = allAxes.get(index); - - double min = unit.toUnit(data.getMinimum(type)); - double max = unit.toUnit(data.getMaximum(type)); - - axis.addBound(min); - axis.addBound(max); - } - - // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close - for (Axis a : allAxes) { - if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) { - a.addBound(a.getMinValue() - 1); - a.addBound(a.getMaxValue() + 1); - } - - double addition = a.getRangeLength() * 0.03; - a.addBound(a.getMinValue() - addition); - a.addBound(a.getMaxValue() + addition); - - double dist; - dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue())); - if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) { - a.addBound(0); - } - } - - - // Check whether to use a common zero - Axis left = allAxes.get(0); - Axis right = allAxes.get(1); - - if (left.getMinValue() > 0 || left.getMaxValue() < 0 || - right.getMinValue() > 0 || right.getMaxValue() < 0 || - Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue())) - return; - - - - //// Compute common zero - // TODO: MEDIUM: This algorithm may require tweaking - - double min1 = left.getMinValue(); - double max1 = left.getMaxValue(); - double min2 = right.getMinValue(); - double max2 = right.getMaxValue(); - - // Calculate and round scaling factor - double scale = Math.max(left.getRangeLength(), right.getRangeLength()) / - Math.min(left.getRangeLength(), right.getRangeLength()); - - System.out.println("Scale: " + scale); - - scale = roundScale(scale); - if (right.getRangeLength() > left.getRangeLength()) { - scale = 1 / scale; - } - System.out.println("Rounded scale: " + scale); - - // Scale right axis, enlarge axes if necessary and scale back - min2 *= scale; - max2 *= scale; - min1 = Math.min(min1, min2); - min2 = min1; - max1 = Math.max(max1, max2); - max2 = max1; - min2 /= scale; - max2 /= scale; - - - - // Scale to unit length - // double scale1 = left.getRangeLength(); - // double scale2 = right.getRangeLength(); - // - // double min1 = left.getMinValue() / scale1; - // double max1 = left.getMaxValue() / scale1; - // double min2 = right.getMinValue() / scale2; - // double max2 = right.getMaxValue() / scale2; - // - // // Combine unit ranges - // min1 = MathUtil.min(min1, min2); - // min2 = min1; - // max1 = MathUtil.max(max1, max2); - // max2 = max1; - // - // // Scale up - // min1 *= scale1; - // max1 *= scale1; - // min2 *= scale2; - // max2 *= scale2; - // - // // Compute common scale - // double range1 = max1-min1; - // double range2 = max2-min2; - // - // double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2); - // double roundScale = roundScale(scale); - // - // if (range2 < range1) { - // if (roundScale < scale) { - // min2 = min1 / roundScale; - // max2 = max1 / roundScale; - // } else { - // min1 = min2 * roundScale; - // max1 = max2 * roundScale; - // } - // } else { - // if (roundScale > scale) { - // min2 = min1 * roundScale; - // max2 = max1 * roundScale; - // } else { - // min1 = min2 / roundScale; - // max1 = max2 / roundScale; - // } - // } - - // Apply scale - left.addBound(min1); - left.addBound(max1); - right.addBound(min2); - right.addBound(max2); - - } - - - - private double roundScale(double scale) { - double mul = 1; - while (scale >= 10) { - scale /= 10; - mul *= 10; - } - while (scale < 1) { - scale *= 10; - mul /= 10; - } - - // 1 2 4 5 10 - - if (scale > 7.5) { - scale = 10; - } else if (scale > 4.5) { - scale = 5; - } else if (scale > 3) { - scale = 4; - } else if (scale > 1.5) { - scale = 2; - } else { - scale = 1; - } - return scale * mul; - } - - - - private double roundScaleUp(double scale) { - double mul = 1; - while (scale >= 10) { - scale /= 10; - mul *= 10; - } - while (scale < 1) { - scale *= 10; - mul /= 10; - } - - if (scale > 5) { - scale = 10; - } else if (scale > 4) { - scale = 5; - } else if (scale > 2) { - scale = 4; - } else if (scale > 1) { - scale = 2; - } else { - scale = 1; - } - return scale * mul; - } - - - private double roundScaleDown(double scale) { - double mul = 1; - while (scale >= 10) { - scale /= 10; - mul *= 10; - } - while (scale < 1) { - scale *= 10; - mul /= 10; - } - - if (scale > 5) { - scale = 5; - } else if (scale > 4) { - scale = 4; - } else if (scale > 2) { - scale = 2; - } else { - scale = 1; - } - return scale * mul; - } - - - - /** - * Fits the axis ranges to the data and returns the "goodness value" of this - * selection of axes. All plotDataAxis elements must be non-null. - * <p> - * NOTE: This method assumes that all data can fit into the axes ranges and - * that only two axes are used. - * - * @return a "goodness value", the larger the better. - */ - protected double getGoodnessValue(FlightDataBranch data) { - double goodness = 0; - int length = plotDataTypes.size(); - - // Fit the axes ranges to the data - fitAxes(data); - - /* - * Calculate goodness of ranges. 100 points is given if the values fill the - * entire range, 0 if they fill none of it. - */ - for (int i = 0; i < length; i++) { - FlightDataType type = plotDataTypes.get(i); - Unit unit = plotDataUnits.get(i); - int index = plotDataAxes.get(i); - if (index < 0) { - throw new IllegalStateException("getGoodnessValue called with auto-selected axis"); - } - Axis axis = allAxes.get(index); - - double min = unit.toUnit(data.getMinimum(type)); - double max = unit.toUnit(data.getMaximum(type)); - if (Double.isNaN(min) || Double.isNaN(max)) - continue; - if (MathUtil.equals(min, max)) - continue; - - double d = (max - min) / axis.getRangeLength(); - d = MathUtil.safeSqrt(d); // Prioritize small ranges - goodness += d * 100.0; - } - - - /* - * Add extra points for specific things. - */ - - // A little for the first type being on the first axis - if (plotDataAxes.get(0) == 0) - goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS; - - // A little bonus if the first axis contains zero - Axis left = allAxes.get(0); - if (left.getMinValue() <= 0 && left.getMaxValue() >= 0) - goodness += BONUS_FIRST_AXIS_HAS_ZERO; - - // A boost if a common zero was used in the ranging - Axis right = allAxes.get(1); - if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 && - right.getMinValue() <= 0 && right.getMaxValue() >= 0) - goodness += BONUS_COMMON_ZERO; - - // A boost if only one axis is used - if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue())) - goodness += BONUS_ONLY_ONE_AXIS; - - return goodness; - } - - - - /** - * Reset the units of this configuration to the default units. Returns this - * PlotConfiguration. - * - * @return this PlotConfiguration. - */ - public PlotConfiguration resetUnits() { - for (int i = 0; i < plotDataTypes.size(); i++) { - plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit()); - } - return this; - } - - - - - @Override - public PlotConfiguration clone() { - try { - - PlotConfiguration copy = (PlotConfiguration) super.clone(); - - // Shallow-clone all immutable lists - copy.plotDataTypes = this.plotDataTypes.clone(); - copy.plotDataAxes = this.plotDataAxes.clone(); - copy.plotDataUnits = this.plotDataUnits.clone(); - copy.events = this.events.clone(); - - // Deep-clone all Axis since they are mutable - copy.allAxes = new ArrayList<Axis>(); - for (Axis a : this.allAxes) { - copy.allAxes.add(a.clone()); - } - - return copy; - - - } catch (CloneNotSupportedException e) { - throw new BugException("BUG! Could not clone()."); - } - } - -} diff --git a/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java b/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java deleted file mode 100644 index d26ea7c2..00000000 --- a/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java +++ /dev/null @@ -1,583 +0,0 @@ -package net.sf.openrocket.gui.plot; - -import java.awt.AlphaComposite; -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Composite; -import java.awt.Font; -import java.awt.Graphics2D; -import java.awt.Image; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.geom.Line2D; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import javax.imageio.ImageIO; -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.components.StyledLabel; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.startup.Preferences; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.annotations.XYImageAnnotation; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.axis.ValueAxis; -import org.jfree.chart.plot.Marker; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.plot.ValueMarker; -import org.jfree.chart.plot.XYPlot; -import org.jfree.chart.renderer.xy.StandardXYItemRenderer; -import org.jfree.chart.title.TextTitle; -import org.jfree.data.Range; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; -import org.jfree.text.TextUtilities; -import org.jfree.ui.LengthAdjustmentType; -import org.jfree.ui.RectangleAnchor; -import org.jfree.ui.TextAnchor; - -/** - * Dialog that shows a plot of a simulation results based on user options. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationPlotDialog extends JDialog { - - private static final float PLOT_STROKE_WIDTH = 1.5f; - private static final Translator trans = Application.getTranslator(); - - private static final Color DEFAULT_EVENT_COLOR = new Color(0, 0, 0); - private static final Map<FlightEvent.Type, Color> EVENT_COLORS = - new HashMap<FlightEvent.Type, Color>(); - static { - EVENT_COLORS.put(FlightEvent.Type.LAUNCH, new Color(255, 0, 0)); - EVENT_COLORS.put(FlightEvent.Type.LIFTOFF, new Color(0, 80, 196)); - EVENT_COLORS.put(FlightEvent.Type.LAUNCHROD, new Color(0, 100, 80)); - EVENT_COLORS.put(FlightEvent.Type.IGNITION, new Color(230, 130, 15)); - EVENT_COLORS.put(FlightEvent.Type.BURNOUT, new Color(80, 55, 40)); - EVENT_COLORS.put(FlightEvent.Type.EJECTION_CHARGE, new Color(80, 55, 40)); - EVENT_COLORS.put(FlightEvent.Type.STAGE_SEPARATION, new Color(80, 55, 40)); - EVENT_COLORS.put(FlightEvent.Type.APOGEE, new Color(15, 120, 15)); - EVENT_COLORS.put(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, new Color(0, 0, 128)); - EVENT_COLORS.put(FlightEvent.Type.GROUND_HIT, new Color(0, 0, 0)); - EVENT_COLORS.put(FlightEvent.Type.SIMULATION_END, new Color(128, 0, 0)); - } - - private static final Map<FlightEvent.Type, Image> EVENT_IMAGES = - new HashMap<FlightEvent.Type, Image>(); - static { - loadImage(FlightEvent.Type.LAUNCH, "pix/eventicons/event-launch.png"); - loadImage(FlightEvent.Type.LIFTOFF, "pix/eventicons/event-liftoff.png"); - loadImage(FlightEvent.Type.LAUNCHROD, "pix/eventicons/event-launchrod.png"); - loadImage(FlightEvent.Type.IGNITION, "pix/eventicons/event-ignition.png"); - loadImage(FlightEvent.Type.BURNOUT, "pix/eventicons/event-burnout.png"); - loadImage(FlightEvent.Type.EJECTION_CHARGE, "pix/eventicons/event-ejection-charge.png"); - loadImage(FlightEvent.Type.STAGE_SEPARATION, - "pix/eventicons/event-stage-separation.png"); - loadImage(FlightEvent.Type.APOGEE, "pix/eventicons/event-apogee.png"); - loadImage(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, - "pix/eventicons/event-recovery-device-deployment.png"); - loadImage(FlightEvent.Type.GROUND_HIT, "pix/eventicons/event-ground-hit.png"); - loadImage(FlightEvent.Type.SIMULATION_END, "pix/eventicons/event-simulation-end.png"); - } - - private static void loadImage(FlightEvent.Type type, String file) { - InputStream is; - - is = ClassLoader.getSystemResourceAsStream(file); - if (is == null) { - System.out.println("ERROR: File " + file + " not found!"); - return; - } - - try { - Image image = ImageIO.read(is); - EVENT_IMAGES.put(type, image); - } catch (IOException ignore) { - ignore.printStackTrace(); - } - } - - - - - private final List<ModifiedXYItemRenderer> renderers = - new ArrayList<ModifiedXYItemRenderer>(); - - private SimulationPlotDialog(Window parent, Simulation simulation, PlotConfiguration config) { - //// Flight data plot - super(parent, trans.get("PlotDialog.title.Flightdataplot")); - this.setModalityType(ModalityType.DOCUMENT_MODAL); - - final boolean initialShowPoints = Application.getPreferences().getBoolean(Preferences.PLOT_SHOW_POINTS, false); - - - // Fill the auto-selections - FlightDataBranch branch = simulation.getSimulatedData().getBranch(0); - PlotConfiguration filled = config.fillAutoAxes(branch); - List<Axis> axes = filled.getAllAxes(); - - - // Create the data series for both axes - XYSeriesCollection[] data = new XYSeriesCollection[2]; - data[0] = new XYSeriesCollection(); - data[1] = new XYSeriesCollection(); - - - // Get the domain axis type - final FlightDataType domainType = filled.getDomainAxisType(); - final Unit domainUnit = filled.getDomainAxisUnit(); - if (domainType == null) { - throw new IllegalArgumentException("Domain axis type not specified."); - } - List<Double> x = branch.get(domainType); - - - // Get plot length (ignore trailing NaN's) - int typeCount = filled.getTypeCount(); - int dataLength = 0; - for (int i = 0; i < typeCount; i++) { - FlightDataType type = filled.getType(i); - List<Double> y = branch.get(type); - - for (int j = dataLength; j < y.size(); j++) { - if (!Double.isNaN(y.get(j)) && !Double.isInfinite(y.get(j))) - dataLength = j; - } - } - dataLength = Math.min(dataLength, x.size()); - - - // Create the XYSeries objects from the flight data and store into the collections - String[] axisLabel = new String[2]; - for (int i = 0; i < typeCount; i++) { - // Get info - FlightDataType type = filled.getType(i); - Unit unit = filled.getUnit(i); - int axis = filled.getAxis(i); - String name = getLabel(type, unit); - - // Store data in provided units - List<Double> y = branch.get(type); - XYSeries series = new XYSeries(name, false, true); - for (int j = 0; j < dataLength; j++) { - series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j))); - } - data[axis].addSeries(series); - - // Update axis label - if (axisLabel[axis] == null) - axisLabel[axis] = type.getName(); - else - axisLabel[axis] += "; " + type.getName(); - } - - - // Create the chart using the factory to get all default settings - JFreeChart chart = ChartFactory.createXYLineChart( - //// Simulated flight - trans.get("PlotDialog.Chart.Simulatedflight"), - null, - null, - null, - PlotOrientation.VERTICAL, - true, - true, - false - ); - - chart.addSubtitle(new TextTitle(config.getName())); - - // Add the data and formatting to the plot - XYPlot plot = chart.getXYPlot(); - int axisno = 0; - for (int i = 0; i < 2; i++) { - // Check whether axis has any data - if (data[i].getSeriesCount() > 0) { - // Create and set axis - double min = axes.get(i).getMinValue(); - double max = axes.get(i).getMaxValue(); - NumberAxis axis = new PresetNumberAxis(min, max); - axis.setLabel(axisLabel[i]); - // axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue()); - plot.setRangeAxis(axisno, axis); - - // Add data and map to the axis - plot.setDataset(axisno, data[i]); - ModifiedXYItemRenderer r = new ModifiedXYItemRenderer(); - r.setBaseShapesVisible(initialShowPoints); - r.setBaseShapesFilled(true); - for (int j = 0; j < data[i].getSeriesCount(); j++) { - r.setSeriesStroke(j, new BasicStroke(PLOT_STROKE_WIDTH)); - } - renderers.add(r); - plot.setRenderer(axisno, r); - plot.mapDatasetToRangeAxis(axisno, axisno); - axisno++; - } - } - - plot.getDomainAxis().setLabel(getLabel(domainType, domainUnit)); - plot.addDomainMarker(new ValueMarker(0)); - plot.addRangeMarker(new ValueMarker(0)); - - - - // Create list of events to show (combine event too close to each other) - ArrayList<Double> timeList = new ArrayList<Double>(); - ArrayList<String> eventList = new ArrayList<String>(); - ArrayList<Color> colorList = new ArrayList<Color>(); - ArrayList<Image> imageList = new ArrayList<Image>(); - - HashSet<FlightEvent.Type> typeSet = new HashSet<FlightEvent.Type>(); - - double prevTime = -100; - String text = null; - Color color = null; - Image image = null; - - List<FlightEvent> events = branch.getEvents(); - for (int i = 0; i < events.size(); i++) { - FlightEvent event = events.get(i); - double t = event.getTime(); - FlightEvent.Type type = event.getType(); - - if (type != FlightEvent.Type.ALTITUDE && config.isEventActive(type)) { - if (Math.abs(t - prevTime) <= 0.01) { - - if (!typeSet.contains(type)) { - text = text + ", " + type.toString(); - color = getEventColor(type); - image = EVENT_IMAGES.get(type); - typeSet.add(type); - } - - } else { - - if (text != null) { - timeList.add(prevTime); - eventList.add(text); - colorList.add(color); - imageList.add(image); - } - prevTime = t; - text = type.toString(); - color = getEventColor(type); - image = EVENT_IMAGES.get(type); - typeSet.clear(); - typeSet.add(type); - - } - } - } - if (text != null) { - timeList.add(prevTime); - eventList.add(text); - colorList.add(color); - imageList.add(image); - } - - - // Create the event markers - - if (config.getDomainAxisType() == FlightDataType.TYPE_TIME) { - - // Domain time is plotted as vertical markers - for (int i = 0; i < eventList.size(); i++) { - double t = timeList.get(i); - String event = eventList.get(i); - color = colorList.get(i); - - ValueMarker m = new ValueMarker(t); - m.setLabel(event); - m.setPaint(color); - m.setLabelPaint(color); - m.setAlpha(0.7f); - plot.addDomainMarker(m); - } - - } else { - - // Other domains are plotted as image annotations - List<Double> time = branch.get(FlightDataType.TYPE_TIME); - List<Double> domain = branch.get(config.getDomainAxisType()); - - for (int i = 0; i < eventList.size(); i++) { - final double t = timeList.get(i); - String event = eventList.get(i); - image = imageList.get(i); - - if (image == null) - continue; - - // Calculate index and interpolation position a - final double a; - int tindex = Collections.binarySearch(time, t); - if (tindex < 0) { - tindex = -tindex - 1; - } - if (tindex >= time.size()) { - // index greater than largest value in time list - tindex = time.size() - 1; - a = 0; - } else if (tindex <= 0) { - // index smaller than smallest value in time list - tindex = 0; - a = 0; - } else { - tindex--; - double t1 = time.get(tindex); - double t2 = time.get(tindex + 1); - - if ((t1 > t) || (t2 < t)) { - throw new BugException("t1=" + t1 + " t2=" + t2 + " t=" + t); - } - - if (MathUtil.equals(t1, t2)) { - a = 0; - } else { - a = 1 - (t - t1) / (t2 - t1); - } - } - - double xcoord; - if (a == 0) { - xcoord = domain.get(tindex); - } else { - xcoord = a * domain.get(tindex) + (1 - a) * domain.get(tindex + 1); - } - - for (int index = 0; index < config.getTypeCount(); index++) { - FlightDataType type = config.getType(index); - List<Double> range = branch.get(type); - - // Image annotations are not supported on the right-side axis - // TODO: LOW: Can this be achieved by JFreeChart? - if (filled.getAxis(index) != SimulationPlotPanel.LEFT) { - continue; - } - - double ycoord; - if (a == 0) { - ycoord = range.get(tindex); - } else { - ycoord = a * range.get(tindex) + (1 - a) * range.get(tindex + 1); - } - - // Convert units - xcoord = config.getDomainAxisUnit().toUnit(xcoord); - ycoord = config.getUnit(index).toUnit(ycoord); - - XYImageAnnotation annotation = - new XYImageAnnotation(xcoord, ycoord, image, RectangleAnchor.CENTER); - annotation.setToolTipText(event); - plot.addAnnotation(annotation); - } - } - } - - - // Create the dialog - - JPanel panel = new JPanel(new MigLayout("fill")); - this.add(panel); - - ChartPanel chartPanel = new ChartPanel(chart, - false, // properties - true, // save - false, // print - true, // zoom - true); // tooltips - chartPanel.setMouseWheelEnabled(true); - chartPanel.setEnforceFileExtensions(true); - chartPanel.setInitialDelay(500); - - chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - - panel.add(chartPanel, "grow, wrap 20lp"); - - //// Show data points - final JCheckBox check = new JCheckBox(trans.get("PlotDialog.CheckBox.Showdatapoints")); - check.setSelected(initialShowPoints); - check.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - boolean show = check.isSelected(); - Application.getPreferences().putBoolean(Preferences.PLOT_SHOW_POINTS, show); - for (ModifiedXYItemRenderer r : renderers) { - r.setBaseShapesVisible(show); - } - } - }); - panel.add(check, "split, left"); - - - JLabel label = new StyledLabel(trans.get("PlotDialog.lbl.Chart"), -2); - panel.add(label, "gapleft para"); - - - panel.add(new JPanel(), "growx"); - - //// Close button - JButton button = new JButton(trans.get("dlg.but.close")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - SimulationPlotDialog.this.dispose(); - } - }); - panel.add(button, "right"); - - this.setLocationByPlatform(true); - this.pack(); - - GUIUtil.setDisposableDialogOptions(this, button); - GUIUtil.rememberWindowSize(this); - } - - private String getLabel(FlightDataType type, Unit unit) { - String name = type.getName(); - if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) && - !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0) - name += " (" + unit.getUnit() + ")"; - return name; - } - - - - private class PresetNumberAxis extends NumberAxis { - private final double min; - private final double max; - - public PresetNumberAxis(double min, double max) { - this.min = min; - this.max = max; - autoAdjustRange(); - } - - @Override - protected void autoAdjustRange() { - this.setRange(min, max); - } - } - - - /** - * Static method that shows a plot with the specified parameters. - * - * @param parent the parent window, which will be blocked. - * @param simulation the simulation to plot. - * @param config the configuration of the plot. - */ - public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) { - new SimulationPlotDialog(parent, simulation, config).setVisible(true); - } - - - - private static Color getEventColor(FlightEvent.Type type) { - Color c = EVENT_COLORS.get(type); - if (c != null) - return c; - return DEFAULT_EVENT_COLOR; - } - - - - - - /** - * A modification to the standard renderer that renders the domain marker - * labels vertically instead of horizontally. - */ - private static class ModifiedXYItemRenderer extends StandardXYItemRenderer { - - @Override - public void drawDomainMarker(Graphics2D g2, XYPlot plot, ValueAxis domainAxis, - Marker marker, Rectangle2D dataArea) { - - if (!(marker instanceof ValueMarker)) { - // Use parent for all others - super.drawDomainMarker(g2, plot, domainAxis, marker, dataArea); - return; - } - - /* - * Draw the normal marker, but with rotated text. - * Copied from the overridden method. - */ - ValueMarker vm = (ValueMarker) marker; - double value = vm.getValue(); - Range range = domainAxis.getRange(); - if (!range.contains(value)) { - return; - } - - double v = domainAxis.valueToJava2D(value, dataArea, plot.getDomainAxisEdge()); - - PlotOrientation orientation = plot.getOrientation(); - Line2D line = null; - if (orientation == PlotOrientation.HORIZONTAL) { - line = new Line2D.Double(dataArea.getMinX(), v, dataArea.getMaxX(), v); - } else { - line = new Line2D.Double(v, dataArea.getMinY(), v, dataArea.getMaxY()); - } - - final Composite originalComposite = g2.getComposite(); - g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, marker - .getAlpha())); - g2.setPaint(marker.getPaint()); - g2.setStroke(marker.getStroke()); - g2.draw(line); - - String label = marker.getLabel(); - RectangleAnchor anchor = marker.getLabelAnchor(); - if (label != null) { - Font labelFont = marker.getLabelFont(); - g2.setFont(labelFont); - g2.setPaint(marker.getLabelPaint()); - Point2D coordinates = calculateDomainMarkerTextAnchorPoint(g2, - orientation, dataArea, line.getBounds2D(), marker - .getLabelOffset(), LengthAdjustmentType.EXPAND, anchor); - - // Changed: - TextAnchor textAnchor = TextAnchor.TOP_RIGHT; - TextUtilities.drawRotatedString(label, g2, (float) coordinates.getX() + 2, - (float) coordinates.getY(), textAnchor, - -Math.PI / 2, textAnchor); - } - g2.setComposite(originalComposite); - } - - } - -} diff --git a/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java b/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java deleted file mode 100644 index 06544f52..00000000 --- a/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java +++ /dev/null @@ -1,532 +0,0 @@ -package net.sf.openrocket.gui.plot; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.util.Arrays; -import java.util.EnumSet; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JTable; -import javax.swing.SwingUtilities; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.components.DescriptionArea; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.util.Utils; - -/** - * Panel that displays the simulation plot options to the user. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationPlotPanel extends JPanel { - private static final Translator trans = Application.getTranslator(); - - // TODO: LOW: Should these be somewhere else? - public static final int AUTO = -1; - public static final int LEFT = 0; - public static final int RIGHT = 1; - - //// Auto - public static final String AUTO_NAME = trans.get("simplotpanel.AUTO_NAME"); - //// Left - public static final String LEFT_NAME = trans.get("simplotpanel.LEFT_NAME"); - //// Right - public static final String RIGHT_NAME = trans.get("simplotpanel.RIGHT_NAME"); - - //// Custom - private static final String CUSTOM = trans.get("simplotpanel.CUSTOM"); - - /** The "Custom" configuration - not to be used for anything other than the title. */ - private static final PlotConfiguration CUSTOM_CONFIGURATION; - static { - CUSTOM_CONFIGURATION = new PlotConfiguration(CUSTOM); - } - - /** The array of presets for the combo box. */ - private static final PlotConfiguration[] PRESET_ARRAY; - static { - PRESET_ARRAY = Arrays.copyOf(PlotConfiguration.DEFAULT_CONFIGURATIONS, - PlotConfiguration.DEFAULT_CONFIGURATIONS.length + 1); - PRESET_ARRAY[PRESET_ARRAY.length - 1] = CUSTOM_CONFIGURATION; - } - - - - /** The current default configuration, set each time a plot is made. */ - private static PlotConfiguration defaultConfiguration = - PlotConfiguration.DEFAULT_CONFIGURATIONS[0].resetUnits(); - - - private final Simulation simulation; - private final FlightDataType[] types; - private PlotConfiguration configuration; - - - private JComboBox configurationSelector; - - private JComboBox domainTypeSelector; - private UnitSelector domainUnitSelector; - - private JPanel typeSelectorPanel; - private FlightEventTableModel eventTableModel; - - - private int modifying = 0; - - - public SimulationPlotPanel(final Simulation simulation) { - super(new MigLayout("fill")); - - this.simulation = simulation; - if (simulation.getSimulatedData() == null || - simulation.getSimulatedData().getBranchCount() == 0) { - throw new IllegalArgumentException("Simulation contains no data."); - } - FlightDataBranch branch = simulation.getSimulatedData().getBranch(0); - types = branch.getTypes(); - - setConfiguration(defaultConfiguration); - - //// Configuration selector - - // Setup the combo box - configurationSelector = new JComboBox(PRESET_ARRAY); - for (PlotConfiguration config : PRESET_ARRAY) { - if (config.getName().equals(configuration.getName())) { - configurationSelector.setSelectedItem(config); - } - } - - configurationSelector.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - if (modifying > 0) - return; - PlotConfiguration conf = (PlotConfiguration) configurationSelector.getSelectedItem(); - if (conf == CUSTOM_CONFIGURATION) - return; - modifying++; - setConfiguration(conf.clone().resetUnits()); - updatePlots(); - modifying--; - } - }); - //// Preset plot configurations: - this.add(new JLabel(trans.get("simplotpanel.lbl.Presetplotconf")), "spanx, split"); - this.add(configurationSelector, "growx, wrap 20lp"); - - - - //// X axis - - //// X axis type: - this.add(new JLabel(trans.get("simplotpanel.lbl.Xaxistype")), "spanx, split"); - domainTypeSelector = new JComboBox(types); - domainTypeSelector.setSelectedItem(configuration.getDomainAxisType()); - domainTypeSelector.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - if (modifying > 0) - return; - FlightDataType type = (FlightDataType) domainTypeSelector.getSelectedItem(); - configuration.setDomainAxisType(type); - domainUnitSelector.setUnitGroup(type.getUnitGroup()); - domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit()); - setToCustom(); - } - }); - this.add(domainTypeSelector, "gapright para"); - - //// Unit: - this.add(new JLabel(trans.get("simplotpanel.lbl.Unit"))); - domainUnitSelector = new UnitSelector(configuration.getDomainAxisType().getUnitGroup()); - domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit()); - domainUnitSelector.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - if (modifying > 0) - return; - configuration.setDomainAxisUnit(domainUnitSelector.getSelectedUnit()); - } - }); - this.add(domainUnitSelector, "width 40lp, gapright para"); - - //// The data will be plotted in time order even if the X axis type is not time. - DescriptionArea desc = new DescriptionArea(trans.get("simplotpanel.Desc"), 2, -2f); - desc.setViewportBorder(BorderFactory.createEmptyBorder()); - this.add(desc, "width 1px, growx 1, wrap unrel"); - - - - //// Y axis selector panel - //// Y axis types: - this.add(new JLabel(trans.get("simplotpanel.lbl.Yaxistypes"))); - //// Flight events: - this.add(new JLabel(trans.get("simplotpanel.lbl.Flightevents")), "wrap rel"); - - typeSelectorPanel = new JPanel(new MigLayout("gapy rel")); - JScrollPane scroll = new JScrollPane(typeSelectorPanel); - this.add(scroll, "spany 2, height 10px, wmin 400lp, grow 100, gapright para"); - - - //// Flight events - eventTableModel = new FlightEventTableModel(); - JTable table = new JTable(eventTableModel); - table.setTableHeader(null); - table.setShowVerticalLines(false); - table.setRowSelectionAllowed(false); - table.setColumnSelectionAllowed(false); - - TableColumnModel columnModel = table.getColumnModel(); - TableColumn col0 = columnModel.getColumn(0); - int w = table.getRowHeight() + 2; - col0.setMinWidth(w); - col0.setPreferredWidth(w); - col0.setMaxWidth(w); - table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); - this.add(new JScrollPane(table), "height 10px, width 200lp, grow 1, wrap rel"); - - - //// All + None buttons - JButton button = new JButton(trans.get("simplotpanel.but.All")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - for (FlightEvent.Type t : FlightEvent.Type.values()) - configuration.setEvent(t, true); - eventTableModel.fireTableDataChanged(); - } - }); - this.add(button, "split 2, gapleft para, gapright para, growx, sizegroup buttons"); - - //// None - button = new JButton(trans.get("simplotpanel.but.None")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - for (FlightEvent.Type t : FlightEvent.Type.values()) - configuration.setEvent(t, false); - eventTableModel.fireTableDataChanged(); - } - }); - this.add(button, "gapleft para, gapright para, growx, sizegroup buttons, wrap para"); - - - - //// New Y axis plot type - button = new JButton(trans.get("simplotpanel.but.NewYaxisplottype")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (configuration.getTypeCount() >= 15) { - JOptionPane.showMessageDialog(SimulationPlotPanel.this, - //// A maximum of 15 plots is allowed. - //// Cannot add plot - trans.get("simplotpanel.OptionPane.lbl1"), - trans.get("simplotpanel.OptionPane.lbl2"), - JOptionPane.ERROR_MESSAGE); - return; - } - - // Select new type smartly - FlightDataType type = null; - for (FlightDataType t : - simulation.getSimulatedData().getBranch(0).getTypes()) { - - boolean used = false; - if (configuration.getDomainAxisType().equals(t)) { - used = true; - } else { - for (int i = 0; i < configuration.getTypeCount(); i++) { - if (configuration.getType(i).equals(t)) { - used = true; - break; - } - } - } - - if (!used) { - type = t; - break; - } - } - if (type == null) { - type = simulation.getSimulatedData().getBranch(0).getTypes()[0]; - } - - // Add new type - configuration.addPlotDataType(type); - setToCustom(); - updatePlots(); - } - }); - this.add(button, "spanx, split"); - - - this.add(new JPanel(), "growx"); - - //// Plot flight - button = new JButton(trans.get("simplotpanel.but.Plotflight")); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - if (configuration.getTypeCount() == 0) { - JOptionPane.showMessageDialog(SimulationPlotPanel.this, - trans.get("error.noPlotSelected"), - trans.get("error.noPlotSelected.title"), - JOptionPane.ERROR_MESSAGE); - return; - } - defaultConfiguration = configuration.clone(); - SimulationPlotDialog.showPlot(SwingUtilities.getWindowAncestor(SimulationPlotPanel.this), - simulation, configuration); - } - }); - this.add(button, "right"); - - - updatePlots(); - } - - - private void setConfiguration(PlotConfiguration conf) { - - boolean modified = false; - - configuration = conf.clone(); - if (!Utils.contains(types, configuration.getDomainAxisType())) { - configuration.setDomainAxisType(types[0]); - modified = true; - } - - for (int i = 0; i < configuration.getTypeCount(); i++) { - if (!Utils.contains(types, configuration.getType(i))) { - configuration.removePlotDataType(i); - i--; - modified = true; - } - } - - if (modified) { - configuration.setName(CUSTOM); - } - - } - - - private void setToCustom() { - modifying++; - configuration.setName(CUSTOM); - configurationSelector.setSelectedItem(CUSTOM_CONFIGURATION); - modifying--; - } - - - private void updatePlots() { - domainTypeSelector.setSelectedItem(configuration.getDomainAxisType()); - domainUnitSelector.setUnitGroup(configuration.getDomainAxisType().getUnitGroup()); - domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit()); - - typeSelectorPanel.removeAll(); - for (int i = 0; i < configuration.getTypeCount(); i++) { - FlightDataType type = configuration.getType(i); - Unit unit = configuration.getUnit(i); - int axis = configuration.getAxis(i); - - typeSelectorPanel.add(new PlotTypeSelector(i, type, unit, axis), "wrap"); - } - - typeSelectorPanel.repaint(); - - eventTableModel.fireTableDataChanged(); - } - - - - - /** - * A JPanel which configures a single plot of a PlotConfiguration. - */ - private class PlotTypeSelector extends JPanel { - private final String[] POSITIONS = { AUTO_NAME, LEFT_NAME, RIGHT_NAME }; - - private final int index; - private JComboBox typeSelector; - private UnitSelector unitSelector; - private JComboBox axisSelector; - - - public PlotTypeSelector(int plotIndex, FlightDataType type, Unit unit, int position) { - super(new MigLayout("ins 0")); - - this.index = plotIndex; - - typeSelector = new JComboBox(types); - typeSelector.setSelectedItem(type); - typeSelector.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - if (modifying > 0) - return; - FlightDataType type = (FlightDataType) typeSelector.getSelectedItem(); - configuration.setPlotDataType(index, type); - unitSelector.setUnitGroup(type.getUnitGroup()); - unitSelector.setSelectedUnit(configuration.getUnit(index)); - setToCustom(); - } - }); - this.add(typeSelector, "gapright para"); - - //// Unit: - this.add(new JLabel(trans.get("simplotpanel.lbl.Unit"))); - unitSelector = new UnitSelector(type.getUnitGroup()); - if (unit != null) - unitSelector.setSelectedUnit(unit); - unitSelector.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - if (modifying > 0) - return; - Unit unit = unitSelector.getSelectedUnit(); - configuration.setPlotDataUnit(index, unit); - } - }); - this.add(unitSelector, "width 40lp, gapright para"); - - //// Axis: - this.add(new JLabel(trans.get("simplotpanel.lbl.Axis"))); - axisSelector = new JComboBox(POSITIONS); - if (position == LEFT) - axisSelector.setSelectedIndex(1); - else if (position == RIGHT) - axisSelector.setSelectedIndex(2); - else - axisSelector.setSelectedIndex(0); - axisSelector.addItemListener(new ItemListener() { - @Override - public void itemStateChanged(ItemEvent e) { - if (modifying > 0) - return; - int axis = axisSelector.getSelectedIndex() - 1; - configuration.setPlotDataAxis(index, axis); - } - }); - this.add(axisSelector); - - - JButton button = new JButton(Icons.DELETE); - //// Remove this plot - button.setToolTipText(trans.get("simplotpanel.but.ttip.Removethisplot")); - button.setBorderPainted(false); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - configuration.removePlotDataType(index); - setToCustom(); - updatePlots(); - } - }); - this.add(button, "gapright 0"); - } - } - - - - private class FlightEventTableModel extends AbstractTableModel { - private final FlightEvent.Type[] eventTypes; - - public FlightEventTableModel() { - EnumSet<FlightEvent.Type> set = EnumSet.noneOf(FlightEvent.Type.class); - for (int i = 0; i < simulation.getSimulatedData().getBranchCount(); i++) { - for (FlightEvent e : simulation.getSimulatedData().getBranch(i).getEvents()) { - set.add(e.getType()); - } - } - set.remove(FlightEvent.Type.ALTITUDE); - int count = set.size(); - - eventTypes = new FlightEvent.Type[count]; - int pos = 0; - for (FlightEvent.Type t : FlightEvent.Type.values()) { - if (set.contains(t)) { - eventTypes[pos] = t; - pos++; - } - } - } - - @Override - public int getColumnCount() { - return 2; - } - - @Override - public int getRowCount() { - return eventTypes.length; - } - - @Override - public Class<?> getColumnClass(int column) { - switch (column) { - case 0: - return Boolean.class; - - case 1: - return String.class; - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - @Override - public Object getValueAt(int row, int column) { - switch (column) { - case 0: - return new Boolean(configuration.isEventActive(eventTypes[row])); - - case 1: - return eventTypes[row].toString(); - - default: - throw new IndexOutOfBoundsException("column=" + column); - } - } - - @Override - public boolean isCellEditable(int row, int column) { - return column == 0; - } - - @Override - public void setValueAt(Object value, int row, int column) { - if (column != 0 || !(value instanceof Boolean)) { - throw new IllegalArgumentException("column=" + column + ", value=" + value); - } - - configuration.setEvent(eventTypes[row], (Boolean) value); - this.fireTableCellUpdated(row, column); - } - } -} diff --git a/src/net/sf/openrocket/gui/print/AbstractPrintableTransition.java b/src/net/sf/openrocket/gui/print/AbstractPrintableTransition.java deleted file mode 100644 index 8e6042de..00000000 --- a/src/net/sf/openrocket/gui/print/AbstractPrintableTransition.java +++ /dev/null @@ -1,86 +0,0 @@ -package net.sf.openrocket.gui.print; - -import net.sf.openrocket.rocketcomponent.Transition; - -import javax.swing.*; -import java.awt.*; -import java.awt.image.BufferedImage; - -public abstract class AbstractPrintableTransition extends JPanel { - /** - * The stroke of the transition arc. - */ - private final static BasicStroke thinStroke = new BasicStroke(1.0f); - - /** - * The X margin. - */ - protected int marginX = (int) PrintUnit.INCHES.toPoints(0.25f); - - /** - * The Y margin. - */ - protected int marginY = (int) PrintUnit.INCHES.toPoints(0.25f); - - /** - * Constructor. Initialize this printable with the component to be printed. - * - * @param isDoubleBuffered a boolean, true for double-buffering - * @param transition the component to be printed - */ - public AbstractPrintableTransition(boolean isDoubleBuffered, Transition transition) { - super(isDoubleBuffered); - setBackground(Color.white); - init(transition); - } - - /** - * Compute the basic values of each arc of the transition/shroud. This is adapted from - * <a href="http://www.rocketshoppe.com/info/Transitions.pdf">The Properties of - * Model Rocket Body Tube Transitions, by J.R. Brohm</a> - * - * @param component the transition component - */ - protected abstract void init(Transition component); - - /** - * Draw the component onto the graphics context. - * - * @param g2 the graphics context - */ - protected abstract void draw(Graphics2D g2); - - /** - * Returns a generated image of the transition. May then be used wherever AWT images can be used, or converted to - * another image/picture format and used accordingly. - * - * @return an awt image of the fin set - */ - public Image createImage() { - int width = getWidth() + marginX; - int height = getHeight() + marginY; - // Create a buffered image in which to draw - BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // Create a graphics contents on the buffered image - Graphics2D g2d = bufferedImage.createGraphics(); - // Draw graphics - g2d.setBackground(Color.white); - g2d.clearRect(0, 0, width, height); - paintComponent(g2d); - // Graphics context no longer needed so dispose it - g2d.dispose(); - return bufferedImage; - } - - @Override - public void paintComponent(Graphics g) { - Graphics2D g2 = (Graphics2D) g; - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - g2.setColor(Color.BLACK); - g2.setStroke(thinStroke); - - draw(g2); - } -} diff --git a/src/net/sf/openrocket/gui/print/ConceptPrintDialog.java b/src/net/sf/openrocket/gui/print/ConceptPrintDialog.java deleted file mode 100644 index 9a07efa0..00000000 --- a/src/net/sf/openrocket/gui/print/ConceptPrintDialog.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.sf.openrocket.gui.print; - -import java.awt.Window; -import java.lang.reflect.InvocationTargetException; - -import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JDialog; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.SwingUtilities; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.util.GUIUtil; - -public class ConceptPrintDialog extends JDialog { - - public ConceptPrintDialog() { - super((Window) null, "Print"); - - JPanel panel = new JPanel(new MigLayout("fill")); - - JList list = new JList(new Object[] { - "Model name", - "Parts detail", - "Fin templates", - "Design report" - }); - panel.add(new JScrollPane(list), "spanx, growx, wrap"); - - JCheckBox checkbox = new JCheckBox("Show by stage"); - panel.add(checkbox, ""); - - JButton button = new JButton("Settings"); - panel.add(button, "right, wrap para"); - - JLabel label = new JLabel("<html>Printer: LaserJet 6L<br>Paper size: A4 Portrait"); - panel.add(label); - - button = new JButton("Change"); - panel.add(button, "right, wrap 20lp"); - - panel.add(new JButton("Save as PDF"), "split, spanx, right"); - panel.add(new JButton("Preview"), "right"); - panel.add(new JButton("Print"), "right"); - panel.add(new JButton("Close"), "right"); - - - this.add(panel); - - } - - - - public static void main(String[] args) throws InterruptedException, InvocationTargetException { - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - GUIUtil.setBestLAF(); - JDialog dialog = new ConceptPrintDialog(); - GUIUtil.setDisposableDialogOptions(dialog, null); - dialog.setSize(450, 350); - dialog.setVisible(true); - } - }); - } - -} diff --git a/src/net/sf/openrocket/gui/print/DesignReport.java b/src/net/sf/openrocket/gui/print/DesignReport.java deleted file mode 100644 index 886e225a..00000000 --- a/src/net/sf/openrocket/gui/print/DesignReport.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * DesignReport.java - */ -package net.sf.openrocket.gui.print; - -import java.awt.Graphics2D; -import java.io.IOException; -import java.text.DecimalFormat; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.figureelements.FigureElement; -import net.sf.openrocket.gui.figureelements.RocketInfo; -import net.sf.openrocket.gui.scalefigure.RocketPanel; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Stage; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.Coordinate; - -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Element; -import com.itextpdf.text.Paragraph; -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.pdf.BaseFont; -import com.itextpdf.text.pdf.DefaultFontMapper; -import com.itextpdf.text.pdf.PdfContentByte; -import com.itextpdf.text.pdf.PdfPCell; -import com.itextpdf.text.pdf.PdfPTable; -import com.itextpdf.text.pdf.PdfWriter; - -/** - * <pre> - * # Title # Section describing the rocket in general without motors - * # Section describing the rocket in general without motors - * <p/> - * design name - * empty mass & CG - * CP position - * CP position at 5 degree AOA (or similar) - * number of stages - * parachute/streamer sizes - * max. diameter (caliber) - * velocity at exit of rail/rod - * minimum safe velocity reached in x inches/cm - * <p/> - * # Section for each motor configuration - * <p/> - * a summary of the motors, e.g. 3xC6-0; B4-6 - * a list of the motors including the manufacturer, designation (maybe also info like burn time, grams of propellant, - * total impulse) - * total grams of propellant - * total impulse - * takeoff weight - * CG and CP position, stability margin - * predicted flight altitude, max. velocity and max. acceleration - * predicted velocity at chute deployment - * predicted descent rate - * Thrust to Weight Ratio of each stage - * <p/> - * </pre> - */ -public class DesignReport { - - /** - * The logger. - */ - private static final LogHelper log = Application.getLogger(); - - /** - * The OR Document. - */ - private OpenRocketDocument rocketDocument; - - /** - * A panel used for rendering of the design diagram. - */ - final RocketPanel panel; - - /** - * The iText document. - */ - protected Document document; - - /** The displayed strings. */ - private static final String STAGES = "Stages: "; - private static final String MASS_WITH_MOTORS = "Mass (with motors): "; - private static final String MASS_WITH_MOTOR = "Mass (with motor): "; - private static final String MASS_EMPTY = "Mass (Empty): "; - private static final String STABILITY = "Stability: "; - private static final String CG = "CG: "; - private static final String CP = "CP: "; - private static final String MOTOR = "Motor"; - private static final String AVG_THRUST = "Avg Thrust"; - private static final String BURN_TIME = "Burn Time"; - private static final String MAX_THRUST = "Max Thrust"; - private static final String TOTAL_IMPULSE = "Total Impulse"; - private static final String THRUST_TO_WT = "Thrust to Wt"; - private static final String PROPELLANT_WT = "Propellant Wt"; - private static final String SIZE = "Size"; - private static final String ALTITUDE = "Altitude"; - private static final String FLIGHT_TIME = "Flight Time"; - private static final String TIME_TO_APOGEE = "Time to Apogee"; - private static final String VELOCITY_OFF_PAD = "Velocity off Pad"; - private static final String MAX_VELOCITY = "Max Velocity"; - private static final String LANDING_VELOCITY = "Landing Velocity"; - private static final String ROCKET_DESIGN = "Rocket Design"; - private static final double GRAVITY_CONSTANT = 9.80665d; - - /** - * Constructor. - * - * @param theRocDoc the OR document - * @param theIDoc the iText document - */ - public DesignReport(OpenRocketDocument theRocDoc, Document theIDoc) { - document = theIDoc; - rocketDocument = theRocDoc; - panel = new RocketPanel(rocketDocument); - } - - /** - * Main entry point. Prints the rocket drawing and design data. - * - * @param writer a direct byte writer - */ - public void writeToDocument(PdfWriter writer) { - if (writer == null) { - return; - } - com.itextpdf.text.Rectangle pageSize = document.getPageSize(); - int pageImageableWidth = (int) pageSize.getWidth() - (int) pageSize.getBorderWidth() * 2; - int pageImageableHeight = (int) pageSize.getHeight() / 2 - (int) pageSize.getBorderWidthTop(); - - PrintUtilities.addText(document, PrintUtilities.BIG_BOLD, ROCKET_DESIGN); - - Rocket rocket = rocketDocument.getRocket(); - final Configuration configuration = rocket.getDefaultConfiguration().clone(); - configuration.setAllStages(); - PdfContentByte canvas = writer.getDirectContent(); - - final PrintFigure figure = new PrintFigure(configuration); - - FigureElement cp = panel.getExtraCP(); - FigureElement cg = panel.getExtraCG(); - RocketInfo text = panel.getExtraText(); - - double scale = paintRocketDiagram(pageImageableWidth, pageImageableHeight, canvas, figure, cp, cg); - - canvas.beginText(); - try { - canvas.setFontAndSize(BaseFont.createFont(PrintUtilities.NORMAL.getFamilyname(), BaseFont.CP1252, - BaseFont.EMBEDDED), PrintUtilities.NORMAL_FONT_SIZE); - } catch (DocumentException e) { - log.error("Could not set font.", e); - } catch (IOException e) { - log.error("Could not create font.", e); - } - int figHeightPts = (int) (PrintUnit.METERS.toPoints(figure.getFigureHeight()) * 0.4 * (scale / PrintUnit.METERS - .toPoints(1))); - final int diagramHeight = pageImageableHeight * 2 - 70 - (figHeightPts); - canvas.moveText(document.leftMargin() + pageSize.getBorderWidthLeft(), diagramHeight); - canvas.moveTextWithLeading(0, -16); - - float initialY = canvas.getYTLM(); - - canvas.showText(rocketDocument.getRocket().getName()); - - canvas.newlineShowText(STAGES); - canvas.showText("" + rocket.getStageCount()); - - - if (configuration.hasMotors()) { - if (configuration.getStageCount() > 1) { - canvas.newlineShowText(MASS_WITH_MOTORS); - } else { - canvas.newlineShowText(MASS_WITH_MOTOR); - } - } else { - canvas.newlineShowText(MASS_EMPTY); - } - canvas.showText(text.getMass(UnitGroup.UNITS_MASS.getDefaultUnit())); - - canvas.newlineShowText(STABILITY); - canvas.showText(text.getStability()); - - canvas.newlineShowText(CG); - canvas.showText(text.getCg()); - - canvas.newlineShowText(CP); - canvas.showText(text.getCp()); - canvas.endText(); - - try { - //Move the internal pointer of the document below that of what was just written using the direct byte buffer. - Paragraph paragraph = new Paragraph(); - float finalY = canvas.getYTLM(); - int heightOfDiagramAndText = (int) (pageSize.getHeight() - (finalY - initialY + diagramHeight)); - - paragraph.setSpacingAfter(heightOfDiagramAndText); - document.add(paragraph); - - String[] motorIds = rocket.getMotorConfigurationIDs(); - - for (int j = 0; j < motorIds.length; j++) { - String motorId = motorIds[j]; - if (motorId != null) { - PdfPTable parent = new PdfPTable(2); - parent.setWidthPercentage(100); - parent.setHorizontalAlignment(Element.ALIGN_LEFT); - parent.setSpacingBefore(0); - parent.setWidths(new int[] { 1, 3 }); - int leading = 0; - //The first motor config is always null. Skip it and the top-most motor, then set the leading. - if (j > 1) { - leading = 25; - } - addFlightData(rocket, motorId, parent, leading); - addMotorData(rocket, motorId, parent); - document.add(parent); - } - } - } catch (DocumentException e) { - log.error("Could not modify document.", e); - } - } - - - /** - * Paint a diagram of the rocket into the PDF document. - * - * @param thePageImageableWidth the number of points in the width of the page available for drawing - * @param thePageImageableHeight the number of points in the height of the page available for drawing - * @param theCanvas the direct byte writer - * @param theFigure the print figure - * @param theCp the center of pressure figure element - * @param theCg the center of gravity figure element - * - * @return the scale of the diagram - */ - private double paintRocketDiagram(final int thePageImageableWidth, final int thePageImageableHeight, - final PdfContentByte theCanvas, final PrintFigure theFigure, - final FigureElement theCp, final FigureElement theCg) { - theFigure.clearAbsoluteExtra(); - theFigure.clearRelativeExtra(); - theFigure.addRelativeExtra(theCp); - theFigure.addRelativeExtra(theCg); - theFigure.updateFigure(); - - double scale = - (thePageImageableWidth * 2.2) / theFigure.getFigureWidth(); - - theFigure.setScale(scale); - /* - * page dimensions are in points-per-inch, which, in Java2D, are the same as pixels-per-inch; thus we don't need any conversion - */ - theFigure.setSize(thePageImageableWidth, thePageImageableHeight); - theFigure.updateFigure(); - - - final DefaultFontMapper mapper = new DefaultFontMapper(); - Graphics2D g2d = theCanvas.createGraphics(thePageImageableWidth, thePageImageableHeight * 2, mapper); - g2d.translate(20, 120); - - g2d.scale(0.4d, 0.4d); - theFigure.paint(g2d); - g2d.dispose(); - return scale; - } - - /** - * Add the motor data for a motor configuration to the table. - * - * @param rocket the rocket - * @param motorId the motor ID to output - * @param parent the parent to which the motor data will be added - */ - private void addMotorData(Rocket rocket, String motorId, final PdfPTable parent) { - - PdfPTable motorTable = new PdfPTable(8); - motorTable.setWidthPercentage(68); - motorTable.setHorizontalAlignment(Element.ALIGN_LEFT); - - final PdfPCell motorCell = ITextHelper.createCell(MOTOR, PdfPCell.BOTTOM); - final int mPad = 10; - motorCell.setPaddingLeft(mPad); - motorTable.addCell(motorCell); - motorTable.addCell(ITextHelper.createCell(AVG_THRUST, PdfPCell.BOTTOM)); - motorTable.addCell(ITextHelper.createCell(BURN_TIME, PdfPCell.BOTTOM)); - motorTable.addCell(ITextHelper.createCell(MAX_THRUST, PdfPCell.BOTTOM)); - motorTable.addCell(ITextHelper.createCell(TOTAL_IMPULSE, PdfPCell.BOTTOM)); - motorTable.addCell(ITextHelper.createCell(THRUST_TO_WT, PdfPCell.BOTTOM)); - motorTable.addCell(ITextHelper.createCell(PROPELLANT_WT, PdfPCell.BOTTOM)); - motorTable.addCell(ITextHelper.createCell(SIZE, PdfPCell.BOTTOM)); - - DecimalFormat ttwFormat = new DecimalFormat("0.00"); - - MassCalculator massCalc = new BasicMassCalculator(); - - Configuration config = new Configuration(rocket); - config.setMotorConfigurationID(motorId); - - int totalMotorCount = 0; - double totalPropMass = 0; - double totalImpulse = 0; - double totalTTW = 0; - - int stage = 0; - double stageMass = 0; - - boolean topBorder = false; - for (RocketComponent c : rocket) { - - if (c instanceof Stage) { - config.setToStage(stage); - stage++; - stageMass = massCalc.getCG(config, MassCalcType.LAUNCH_MASS).weight; - // Calculate total thrust-to-weight from only lowest stage motors - totalTTW = 0; - topBorder = true; - } - - if (c instanceof MotorMount && ((MotorMount) c).isMotorMount()) { - MotorMount mount = (MotorMount) c; - - if (mount.isMotorMount() && mount.getMotor(motorId) != null) { - Motor motor = mount.getMotor(motorId); - int motorCount = c.toAbsolute(Coordinate.NUL).length; - - - int border = Rectangle.NO_BORDER; - if (topBorder) { - border = Rectangle.TOP; - topBorder = false; - } - - String name = motor.getDesignation(); - if (motorCount > 1) { - name += " (" + Chars.TIMES + motorCount + ")"; - } - - final PdfPCell motorVCell = ITextHelper.createCell(name, border); - motorVCell.setPaddingLeft(mPad); - motorTable.addCell(motorVCell); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getAverageThrustEstimate()), border)); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(motor.getBurnTimeEstimate()), border)); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(motor.getMaxThrustEstimate()), border)); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(motor.getTotalImpulseEstimate()), border)); - - double ttw = motor.getAverageThrustEstimate() / (stageMass * GRAVITY_CONSTANT); - motorTable.addCell(ITextHelper.createCell( - ttwFormat.format(ttw) + ":1", border)); - - double propMass = (motor.getLaunchCG().weight - motor.getEmptyCG().weight); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(propMass), border)); - - final Unit motorUnit = UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit(); - motorTable.addCell(ITextHelper.createCell(motorUnit.toString(motor.getDiameter()) + - "/" + - motorUnit.toString(motor.getLength()) + " " + - motorUnit.toString(), border)); - - // Sum up total count - totalMotorCount += motorCount; - totalPropMass += propMass * motorCount; - totalImpulse += motor.getTotalImpulseEstimate() * motorCount; - totalTTW += ttw * motorCount; - } - } - } - - if (totalMotorCount > 1) { - int border = Rectangle.TOP; - final PdfPCell motorVCell = ITextHelper.createCell("Total:", border); - motorVCell.setPaddingLeft(mPad); - motorTable.addCell(motorVCell); - motorTable.addCell(ITextHelper.createCell("", border)); - motorTable.addCell(ITextHelper.createCell("", border)); - motorTable.addCell(ITextHelper.createCell("", border)); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(totalImpulse), border)); - motorTable.addCell(ITextHelper.createCell( - ttwFormat.format(totalTTW) + ":1", border)); - motorTable.addCell(ITextHelper.createCell( - UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(totalPropMass), border)); - motorTable.addCell(ITextHelper.createCell("", border)); - - } - - PdfPCell c = new PdfPCell(motorTable); - c.setBorder(PdfPCell.LEFT); - c.setBorderWidthTop(0f); - parent.addCell(c); - } - - - /** - * Add the motor data for a motor configuration to the table. - * - * @param theRocket the rocket - * @param motorId a motor configuration id - * @param parent the parent to which the motor data will be added - * @param leading the number of points for the leading - */ - private void addFlightData(final Rocket theRocket, final String motorId, final PdfPTable parent, int leading) { - - // Perform flight simulation - Rocket duplicate = theRocket.copyWithOriginalID(); - FlightData flight = null; - try { - Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate); - simulation.getOptions().setMotorConfigurationID(motorId); - simulation.simulate(); - flight = simulation.getSimulatedData(); - } catch (SimulationException e1) { - // Ignore - } - - // Output the flight data - if (flight != null) { - try { - final Unit distanceUnit = UnitGroup.UNITS_DISTANCE.getDefaultUnit(); - final Unit velocityUnit = UnitGroup.UNITS_VELOCITY.getDefaultUnit(); - final Unit flightTimeUnit = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit(); - - PdfPTable labelTable = new PdfPTable(2); - labelTable.setWidths(new int[] { 3, 2 }); - final Paragraph chunk = ITextHelper.createParagraph(stripBrackets( - theRocket.getMotorConfigurationNameOrDescription(motorId)), PrintUtilities.BOLD); - chunk.setLeading(leading); - chunk.setSpacingAfter(3f); - - document.add(chunk); - - final PdfPCell cell = ITextHelper.createCell(ALTITUDE, 2, 2); - cell.setUseBorderPadding(false); - cell.setBorderWidthTop(0f); - labelTable.addCell(cell); - labelTable.addCell(ITextHelper.createCell(distanceUnit.toStringUnit(flight.getMaxAltitude()), 2, 2)); - - labelTable.addCell(ITextHelper.createCell(FLIGHT_TIME, 2, 2)); - labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getFlightTime()), 2, 2)); - - labelTable.addCell(ITextHelper.createCell(TIME_TO_APOGEE, 2, 2)); - labelTable.addCell(ITextHelper.createCell(flightTimeUnit.toStringUnit(flight.getTimeToApogee()), 2, 2)); - - labelTable.addCell(ITextHelper.createCell(VELOCITY_OFF_PAD, 2, 2)); - labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getLaunchRodVelocity()), 2, 2)); - - labelTable.addCell(ITextHelper.createCell(MAX_VELOCITY, 2, 2)); - labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getMaxVelocity()), 2, 2)); - - labelTable.addCell(ITextHelper.createCell(LANDING_VELOCITY, 2, 2)); - labelTable.addCell(ITextHelper.createCell(velocityUnit.toStringUnit(flight.getGroundHitVelocity()), 2, 2)); - - //Add the table to the parent; have to wrap it in a cell - PdfPCell c = new PdfPCell(labelTable); - c.setBorder(PdfPCell.RIGHT); - c.setBorderWidthTop(0); - c.setTop(0); - parent.addCell(c); - } catch (DocumentException e) { - log.error("Could not add flight data to document.", e); - } - } - } - - /** - * Strip [] brackets from a string. - * - * @param target the original string - * - * @return target with [] removed - */ - private String stripBrackets(String target) { - return stripLeftBracket(stripRightBracket(target)); - } - - /** - * Strip [ from a string. - * - * @param target the original string - * - * @return target with [ removed - */ - private String stripLeftBracket(String target) { - return target.replace("[", ""); - } - - /** - * Strip ] from a string. - * - * @param target the original string - * - * @return target with ] removed - */ - private String stripRightBracket(String target) { - return target.replace("]", ""); - } - -} diff --git a/src/net/sf/openrocket/gui/print/FinMarkingGuide.java b/src/net/sf/openrocket/gui/print/FinMarkingGuide.java deleted file mode 100644 index f0be174d..00000000 --- a/src/net/sf/openrocket/gui/print/FinMarkingGuide.java +++ /dev/null @@ -1,456 +0,0 @@ -package net.sf.openrocket.gui.print; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - -import javax.swing.*; -import java.awt.*; -import java.awt.geom.GeneralPath; -import java.awt.geom.Path2D; -import java.awt.image.BufferedImage; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * This is the core Swing representation of a fin marking guide. It can handle multiple fin sets on the same or - * different body tubes. One marking guide will be created for any body tube that has a finset. If a tube has - * multiple finsets, then they are combined onto one marking guide. It also includes launch lug marking line(s) if lugs - * are present. If (and only if) a launch lug exists, then the word 'Front' is affixed to the leading edge of the - * guide to give orientation. - * <p/> - */ -public class FinMarkingGuide extends JPanel { - - /** - * The stroke of normal lines. - */ - private final static BasicStroke thinStroke = new BasicStroke(1.0f); - - /** - * The size of the arrow in points. - */ - private static final int ARROW_SIZE = 10; - - /** - * The default guide width in inches. - */ - public final static double DEFAULT_GUIDE_WIDTH = 3d; - - /** - * 2 PI radians (represents a circle). - */ - public final static double TWO_PI = 2 * Math.PI; - - /** - * The I18N translator. - */ - private static final Translator trans = Application.getTranslator(); - - /** - * The margin. - */ - private static final int MARGIN = (int) PrintUnit.INCHES.toPoints(0.25f); - - /** - * The height (circumference) of the biggest body tube with a finset. - */ - private int maxHeight = 0; - - /** - * A map of body tubes, to a list of components that contains finsets and launch lugs. - */ - private Map<BodyTube, java.util.List<ExternalComponent>> markingGuideItems; - - /** - * Constructor. - * - * @param rocket the rocket instance - */ - public FinMarkingGuide(Rocket rocket) { - super(false); - setBackground(Color.white); - markingGuideItems = init(rocket); - //Max of 2 drawing guides horizontally per page. - setSize((int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH) * 2 + 3 * MARGIN, maxHeight); - } - - /** - * Initialize the marking guide class by iterating over a rocket and finding all finsets. - * - * @param component the root rocket component - this is iterated to find all finset and launch lugs - * @return a map of body tubes to lists of finsets and launch lugs. - */ - private Map<BodyTube, java.util.List<ExternalComponent>> init(Rocket component) { - Iterator<RocketComponent> iter = component.iterator(false); - Map<BodyTube, java.util.List<ExternalComponent>> results = new LinkedHashMap<BodyTube, List<ExternalComponent>>(); - BodyTube current = null; - int totalHeight = 0; - int iterationHeight = 0; - int count = 0; - - while (iter.hasNext()) { - RocketComponent next = iter.next(); - if (next instanceof BodyTube) { - current = (BodyTube) next; - } else if (next instanceof FinSet || next instanceof LaunchLug) { - java.util.List<ExternalComponent> list = results.get(current); - if (list == null && current != null) { - list = new ArrayList<ExternalComponent>(); - results.put(current, list); - - double radius = current.getOuterRadius(); - int circumferenceInPoints = (int) PrintUnit.METERS.toPoints(radius * TWO_PI); - - // Find the biggest body tube circumference. - if (iterationHeight < (circumferenceInPoints + MARGIN)) { - iterationHeight = circumferenceInPoints + MARGIN; - } - //At most, two marking guides horizontally. After that, move down and back to the left margin. - count++; - if (count % 2 == 0) { - totalHeight += iterationHeight; - iterationHeight = 0; - } - } - if (list != null) { - list.add((ExternalComponent) next); - } - } - } - maxHeight = totalHeight + iterationHeight; - return results; - } - - /** - * Returns a generated image of the fin marking guide. May then be used wherever AWT images can be used, or - * converted to another image/picture format and used accordingly. - * - * @return an awt image of the fin marking guide - */ - public Image createImage() { - int width = getWidth() + 25; - int height = getHeight() + 25; - // Create a buffered image in which to draw - BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // Create a graphics context on the buffered image - Graphics2D g2d = bufferedImage.createGraphics(); - // Draw graphics - g2d.setBackground(Color.white); - g2d.clearRect(0, 0, width, height); - paintComponent(g2d); - // Graphics context no longer needed so dispose it - g2d.dispose(); - return bufferedImage; - } - - /** - * <pre> - * ---------------------- Page Edge -------------------------------------------------------- - * | ^ - * | | - * | - * | y - * | - * | | - * P v - * a --- +----------------------------+ ------------ - * g<------^-- x ------------>+ + ^ - * e | + + | - * | + + baseYOffset - * E | + + v - * d | +<----------Fin------------->+ ------------- - * g | + + - * e | + + - * | | + + - * | | + + - * | | + + baseSpacing - * | | + + - * | | + + - * | | + + - * | | + + - * | | +<----------Fin------------->+ -------------- - * | | + + - * | circumferenceInPoints + + - * | | + + - * | | + + - * | | + + baseSpacing - * | | +<------Launch Lug --------->+ ----- - * | | + + \ - * | | + + + yLLOffset - * | | + + / - * | | +<----------Fin------------->+ -------------- - * | | + + ^ - * | | + + | - * | | + + baseYOffset - * | v + + v - * | --- +----------------------------+ -------------- - * - * |<-------- width ----------->| - * - * yLLOffset is computed from the difference between the base rotation of the fin and the radial direction of the lug. - * - * Note: There is a current limitation that a tube with multiple launch lugs may not render the lug lines correctly. - * </pre> - * - * @param g the Graphics context - */ - @Override - public void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - paintFinMarkingGuide(g2); - } - - private void paintFinMarkingGuide(Graphics2D g2) { - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - g2.setColor(Color.BLACK); - g2.setStroke(thinStroke); - int x = MARGIN; - int y = MARGIN; - - int width = (int) PrintUnit.INCHES.toPoints(DEFAULT_GUIDE_WIDTH); - - int column = 0; - for (BodyTube next : markingGuideItems.keySet()) { - double circumferenceInPoints = PrintUnit.METERS.toPoints(next.getOuterRadius() * TWO_PI); - List<ExternalComponent> componentList = markingGuideItems.get(next); - //Don't draw the lug if there are no fins. - if (hasFins(componentList)) { - - drawMarkingGuide(g2, x, y, (int) (circumferenceInPoints), width); - - //Sort so that fins always precede lugs - sort(componentList); - - boolean hasMultipleComponents = componentList.size() > 1; - - double baseSpacing = 0d; - double baseYOrigin = 0; - double finRadial = 0d; - int yFirstFin = y; - int yLastFin = y; - boolean firstFinSet = true; - - //fin1: 42 fin2: 25 - for (ExternalComponent externalComponent : componentList) { - if (externalComponent instanceof FinSet) { - FinSet fins = (FinSet) externalComponent; - int finCount = fins.getFinCount(); - int offset = 0; - baseSpacing = (circumferenceInPoints / finCount); - double baseRotation = fins.getBaseRotation(); - if (!firstFinSet) { - //Adjust the rotation to a positive number. - while (baseRotation < 0) { - baseRotation += TWO_PI / finCount; - } - offset = computeYOffset(y, circumferenceInPoints, baseSpacing, baseYOrigin, finRadial, baseRotation); - } else { - //baseYOrigin is the distance from the top of the marking guide to the first fin of the first fin set. - //This measurement is used to base all subsequent finsets and lugs off of. - baseYOrigin = baseSpacing / 2; - offset = (int) (baseYOrigin) + y; - firstFinSet = false; - } - yFirstFin = y; - yLastFin = y; - finRadial = baseRotation; - - //Draw the fin marking lines. - for (int fin = 0; fin < finCount; fin++) { - if (fin > 0) { - offset += baseSpacing; - yLastFin = offset; - } else { - yFirstFin = offset; - } - drawDoubleArrowLine(g2, x, offset, x + width, offset); - // if (hasMultipleComponents) { - g2.drawString(externalComponent.getName(), x + (width / 3), offset - 2); - // } - } - } else if (externalComponent instanceof LaunchLug) { - LaunchLug lug = (LaunchLug) externalComponent; - double yLLOffset = (lug.getRadialDirection() - finRadial) / TWO_PI; - //The placement of the lug line must respect the boundary of the outer marking guide. In order - //to do that, place it above or below either the top or bottom fin line, based on the difference - //between their rotational directions. - if (yLLOffset < 0) { - yLLOffset = yLLOffset * circumferenceInPoints + yLastFin; - } else { - yLLOffset = yLLOffset * circumferenceInPoints + yFirstFin; - } - drawDoubleArrowLine(g2, x, (int) yLLOffset, x + width, (int) yLLOffset); - g2.drawString(lug.getName(), x + (width / 3), (int) yLLOffset - 2); - - } - } - //Only if the tube has a lug or multiple finsets does the orientation of the marking guide matter. So print 'Front'. - if (hasMultipleComponents) { - drawFrontIndication(g2, x, y, (int) baseSpacing, (int) circumferenceInPoints, width); - } - - //At most, two marking guides horizontally. After that, move down and back to the left margin. - column++; - if (column % 2 == 0) { - x = MARGIN; - y += circumferenceInPoints + MARGIN; - } else { - x += MARGIN + width; - } - } - } - } - - /** - * Compute the y offset for the next fin line. - * - * @param y the top margin - * @param circumferenceInPoints the circumference (height) of the guide - * @param baseSpacing the circumference / fin count - * @param baseYOrigin the offset from the top of the guide to the first fin of the first fin set drawn - * @param prevBaseRotation the rotation of the previous finset - * @param baseRotation the rotation of the current finset - * @return number of points from the top of the marking guide to the line to be drawn - */ - private int computeYOffset(int y, double circumferenceInPoints, double baseSpacing, double baseYOrigin, double prevBaseRotation, double baseRotation) { - int offset; - double finRadialDifference = (baseRotation - prevBaseRotation) / TWO_PI; - //If the fin line would be off the top of the marking guide, then readjust. - if (baseYOrigin + finRadialDifference * circumferenceInPoints < 0) { - offset = (int) (baseYOrigin + baseSpacing + finRadialDifference * circumferenceInPoints) + y; - } else if (baseYOrigin - finRadialDifference * circumferenceInPoints > 0) { - offset = (int) (finRadialDifference * circumferenceInPoints + baseYOrigin) + y; - } else { - offset = (int) (finRadialDifference * circumferenceInPoints - baseYOrigin) + y; - } - return offset; - } - - /** - * Determines if the list contains a FinSet. - * - * @param list a list of ExternalComponent - * @return true if the list contains at least one FinSet - */ - private boolean hasFins(List<ExternalComponent> list) { - for (ExternalComponent externalComponent : list) { - if (externalComponent instanceof FinSet) { - return true; - } - } - return false; - } - - /** - * Sort a list of ExternalComponent in-place. Forces FinSets to precede Launch Lugs. - * - * @param componentList a list of ExternalComponent - */ - private void sort(List<ExternalComponent> componentList) { - Collections.sort(componentList, new Comparator<ExternalComponent>() { - @Override - public int compare(ExternalComponent o1, ExternalComponent o2) { - if (o1 instanceof FinSet) { - return -1; - } - if (o2 instanceof FinSet) { - return 1; - } - return 0; - } - }); - } - - /** - * Draw the marking guide outline. - * - * @param g2 the graphics context - * @param x the starting x coordinate - * @param y the starting y coordinate - * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube circumference - * @param width the width of the marking guide in print units; somewhat arbitrary - */ - private void drawMarkingGuide(Graphics2D g2, int x, int y, int length, int width) { - Path2D outline = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4); - outline.moveTo(x, y); - outline.lineTo(width + x, y); - outline.lineTo(width + x, length + y); - outline.lineTo(x, length + y); - outline.closePath(); - g2.draw(outline); - - //Draw tick marks for alignment, 1/4 of the width in from either edge - int fromEdge = (width) / 4; - final int tickLength = 8; - //Upper left - g2.drawLine(x + fromEdge, y, x + fromEdge, y + tickLength); - //Upper right - g2.drawLine(x + width - fromEdge, y, x + width - fromEdge, y + tickLength); - //Lower left - g2.drawLine(x + fromEdge, y + length - tickLength, x + fromEdge, y + length); - //Lower right - g2.drawLine(x + width - fromEdge, y + length - tickLength, x + width - fromEdge, y + length); - } - - /** - * Draw a vertical string indicating the front of the rocket. This is necessary when a launch lug exists to - * give proper orientation of the guide (assuming that the lug is asymmetrically positioned with respect to a fin). - * - * @param g2 the graphics context - * @param x the starting x coordinate - * @param y the starting y coordinate - * @param spacing the space between fin lines - * @param length the length, or height, in print units of the marking guide; should be equivalent to the outer tube circumference - * @param width the width of the marking guide in print units; somewhat arbitrary - */ - private void drawFrontIndication(Graphics2D g2, int x, int y, int spacing, int length, int width) { - //The magic numbers here are fairly arbitrary. They're chosen in a manner that best positions 'Front' to be - //readable, without going to complex string layout prediction logic. - int rotateX = x + width - 16; - int rotateY = y + (int) (spacing * 1.5) + 20; - if (rotateY > y + length + 14) { - rotateY = y + length / 2 - 10; - } - g2.translate(rotateX, rotateY); - g2.rotate(Math.PI / 2); - g2.drawString(trans.get("FinMarkingGuide.lbl.Front"), 0, 0); - g2.rotate(-Math.PI / 2); - g2.translate(-rotateX, -rotateY); - } - - /** - * Draw a horizontal line with arrows on both endpoints. Depicts a fin alignment. - * - * @param g2 the graphics context - * @param x1 the starting x coordinate - * @param y1 the starting y coordinate - * @param x2 the ending x coordinate - * @param y2 the ending y coordinate - */ - void drawDoubleArrowLine(Graphics2D g2, int x1, int y1, int x2, int y2) { - int len = x2 - x1; - - g2.drawLine(x1, y1, x1 + len, y2); - g2.fillPolygon(new int[]{x1 + len, x1 + len - ARROW_SIZE, x1 + len - ARROW_SIZE, x1 + len}, - new int[]{y2, y2 - ARROW_SIZE / 2, y2 + ARROW_SIZE / 2, y2}, 4); - - g2.fillPolygon(new int[]{x1, x1 + ARROW_SIZE, x1 + ARROW_SIZE, x1}, - new int[]{y1, y1 - ARROW_SIZE / 2, y1 + ARROW_SIZE / 2, y1}, 4); - } - - -} diff --git a/src/net/sf/openrocket/gui/print/ITextHelper.java b/src/net/sf/openrocket/gui/print/ITextHelper.java deleted file mode 100644 index 0b2b6f95..00000000 --- a/src/net/sf/openrocket/gui/print/ITextHelper.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * ITextHelper.java - */ -package net.sf.openrocket.gui.print; - -import com.itextpdf.text.Chunk; -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Font; -import com.itextpdf.text.Paragraph; -import com.itextpdf.text.Phrase; -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.pdf.PdfContentByte; -import com.itextpdf.text.pdf.PdfPCell; -import com.itextpdf.text.pdf.PdfPTable; -import com.itextpdf.text.pdf.PdfWriter; - -import java.awt.*; -import java.awt.image.BufferedImage; - -/** - * A bunch of helper methods for creating iText components. - */ -public final class ITextHelper { - - /** - * Create a cell for an iText table. - * - * @return a cell with bottom border - */ - public static PdfPCell createCell () { - return createCell(Rectangle.BOTTOM); - } - - /** - * Create a cell for an iText table with the given border location. - * - * @param border the border location - * - * @return a cell with given border - */ - public static PdfPCell createCell (int border) { - PdfPCell result = new PdfPCell(); - result.setBorder(border); - - return result; - } - - /** - * Create a cell whose contents are a table. No border. - * - * @param table the table to insert into the cell - * - * @return the cell containing a table - */ - public static PdfPCell createCell (PdfPTable table) { - PdfPCell result = new PdfPCell(); - result.setBorder(PdfPCell.NO_BORDER); - result.addElement(table); - - return result; - } - - /** - * Create a cell whose contents are the given string. No border. Standard PrintUtilities.NORMAL font. - * - * @param v the text of the cell. - * - * @return the cell containing the text - */ - public static PdfPCell createCell (String v) { - return createCell(v, Rectangle.NO_BORDER, PrintUtilities.NORMAL); - } - - /** - * Create a cell whose contents are the given string , rendered with the given font. No border. - * - * @param v the text of the cell - * @param font the font - * - * @return the cell containing the text - */ - public static PdfPCell createCell (String v, Font font) { - return createCell(v, Rectangle.NO_BORDER, font); - } - - /** - * Create a cell whose contents are the given string with specified left and right padding (spacing). - * - * @param v the text of the cell - * @param leftPad the number of points to precede the text - * @param rightPad the number of points to follow the text - * - * @return the cell containing the text - */ - public static PdfPCell createCell (String v, int leftPad, int rightPad) { - PdfPCell c = createCell(v, Rectangle.NO_BORDER, PrintUtilities.NORMAL); - c.setPaddingLeft(leftPad); - c.setPaddingRight(rightPad); - return c; - } - - /** - * Create a cell whose contents are the given string with the given border. Uses NORMAL font. - * - * @param v the text of the cell - * @param border the border type - * - * @return the cell containing the text - */ - public static PdfPCell createCell (String v, int border) { - return createCell(v, border, PrintUtilities.NORMAL); - } - - /** - * Complete create cell - fully qualified. Create a cell whose contents are the given string with the given border - * and font. - * - * @param v the text of the cell - * @param border the border type - * @param font the font - * - * @return the cell containing the text - */ - public static PdfPCell createCell (String v, int border, Font font) { - PdfPCell result = new PdfPCell(); - result.setBorder(border); - Chunk c = new Chunk(); - c.setFont(font); - c.append(v); - result.addElement(c); - return result; - } - - /** - * Create a phrase with the given text and font. - * - * @param text the text - * @param font the font - * - * @return an iText phrase - */ - public static Phrase createPhrase (String text, Font font) { - Phrase p = new Phrase(); - final Chunk chunk = new Chunk(text); - chunk.setFont(font); - p.add(chunk); - return p; - } - - /** - * Create a phrase with the given text. - * - * @param text the text - * - * @return an iText phrase - */ - public static Phrase createPhrase (String text) { - return createPhrase(text, PrintUtilities.NORMAL); - } - - /** - * Create a paragraph with the given text and font. - * - * @param text the text - * @param font the font - * - * @return an iText paragraph - */ - public static Paragraph createParagraph (String text, Font font) { - Paragraph p = new Paragraph(); - final Chunk chunk = new Chunk(text); - chunk.setFont(font); - p.add(chunk); - return p; - } - - /** - * Create a paragraph with the given text and using NORMAL font. - * - * @param text the text - * - * @return an iText paragraph - */ - public static Paragraph createParagraph (String text) { - return createParagraph(text, PrintUtilities.NORMAL); - } - - /** - * Break a large image up into page-size pieces and output each page in order to an iText document. The image is - * overlayed with an matrix of pages running from left to right until the right side of the image is reached. Then - * the next 'row' of pages is output from left to right, and so on. - * - * @param pageSize a rectangle that defines the bounds of the page size - * @param doc the iText document - * @param writer the underlying content writer - * @param image the source image - * - * @throws DocumentException thrown if the document could not be written - */ - public static void renderImageAcrossPages (Rectangle pageSize, Document doc, PdfWriter writer, java.awt.Image image) - throws DocumentException { - final int margin = (int)Math.min(doc.topMargin(), PrintUnit.POINTS_PER_INCH * 0.3f); - float wPage = pageSize.getWidth() - 2 * margin; - float hPage = pageSize.getHeight() - 2 * margin; - - float wImage = image.getWidth(null); - float hImage = image.getHeight(null); - java.awt.Rectangle crop = new java.awt.Rectangle(0, 0, (int) Math.min(wPage, wImage), (int) Math.min(hPage, - hImage)); - PdfContentByte content = writer.getDirectContent(); - - int ymargin = 0; - - while (true) { - BufferedImage subImage = ((BufferedImage) image).getSubimage((int) crop.getX(), (int) crop.getY(), - (int) crop.getWidth(), (int) crop.getHeight()); - - Graphics2D g2 = content.createGraphics(pageSize.getWidth(), pageSize.getHeight()); - g2.drawImage(subImage, margin, ymargin, null); - g2.dispose(); - - // After the first page, the y-margin needs to be set. - ymargin = margin; - - final int newX = (int) (crop.getWidth() + crop.getX()); - if (newX < wImage) { - double adjust = Math.min(wImage - newX, wPage); - crop = new java.awt.Rectangle(newX, (int) crop.getY(), (int) adjust, - (int) crop.getHeight()); - } - else { - final int newY = (int) (crop.getHeight() + crop.getY()); - if (newY < hImage) { - double adjust = Math.min(hImage - newY, hPage); - crop = new java.awt.Rectangle(0, newY, (int) Math.min(wPage, wImage), (int) adjust); - } - else { - break; - } - } - doc.newPage(); - } - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java b/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java deleted file mode 100644 index 456495f3..00000000 --- a/src/net/sf/openrocket/gui/print/OpenRocketPrintable.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * OpenRocketPrintable.java - */ -package net.sf.openrocket.gui.print; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -/** - * This enumeration identifies the various types of information that may be printed. - */ - -public enum OpenRocketPrintable { - // Design Report - DESIGN_REPORT("OpenRocketPrintable.DesignReport", false, 1), - // Parts detail - PARTS_DETAIL("OpenRocketPrintable.Partsdetail", true, 2), - // Nose Cone Templates - NOSE_CONE_TEMPLATE("OpenRocketPrintable.Noseconetemplates", false, 3), - // Transition Templates - TRANSITION_TEMPLATE("OpenRocketPrintable.Transitiontemplates", false, 4), - // Finset shape - FIN_TEMPLATE("OpenRocketPrintable.Fintemplates", true, 5), - // Fin marking guide. - FIN_MARKING_GUIDE("OpenRocketPrintable.Finmarkingguide", false, 6); - - - private static final Translator trans = Application.getTranslator(); - - /** - * The description - will be displayed in the JTree. - */ - private String description; - - /** - * Flag that indicates if the enum value is different depending upon stage. - */ - private boolean stageSpecific; - - /** - * The order of the item as it appears in the printed document. - */ - private int order; - - /** - * Constructor. - * - * @param s the displayable description - * @param staged indicates if the printable is stage dependent - * @param idx the relative print order - */ - OpenRocketPrintable(String s, boolean staged, int idx) { - description = s; - stageSpecific = staged; - order = idx; - } - - /** - * Get the description of this printable. - * - * @return a displayable string - */ - public String getDescription() { - return trans.get(description); - } - - /** - * Answers if this enum value has different meaning depending upon the stage. - * - * @return true if the printable is stage dependent - */ - public boolean isStageSpecific() { - return stageSpecific; - } - - /** - * Answer the print order. This is relative to other enum values. No two enum values will have the same print - * order value. - * - * @return a 0 based order (0 being first, or highest) - */ - public int getPrintOrder() { - return order; - } - - /** - * Look up an enum value based on the description. - * - * @param target the description - * - * @return an instance of this enum class or null if not found - */ - public static OpenRocketPrintable findByDescription(String target) { - OpenRocketPrintable[] values = values(); - for (OpenRocketPrintable value : values) { - if (value.getDescription().equalsIgnoreCase(target)) { - return value; - } - } - return null; - } - - /** - * Get a list of ordered enum values that do not have stage affinity. - * - * @return a list of OpenRocketPrintable - */ - public static List<OpenRocketPrintable> getUnstaged() { - List<OpenRocketPrintable> unstaged = new ArrayList<OpenRocketPrintable>(); - OpenRocketPrintable[] values = values(); - for (OpenRocketPrintable value : values) { - if (!value.isStageSpecific()) { - unstaged.add(value); - } - } - return unstaged; - } -} diff --git a/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java b/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java deleted file mode 100644 index 90cf4610..00000000 --- a/src/net/sf/openrocket/gui/print/PDFPrintStreamDoc.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * PDFPrintStreamDoc.java - */ -package net.sf.openrocket.gui.print; - -import javax.print.Doc; -import javax.print.DocFlavor; -import javax.print.attribute.AttributeSetUtilities; -import javax.print.attribute.DocAttributeSet; -import java.io.*; - -/** - * This class implements a javax Doc specifically for PDF printing. All reports in OpenRocket are PDF (iText) based. - */ -public class PDFPrintStreamDoc implements Doc { - - /** The source stream of the PDF document. */ - private InputStream stream; - - /** The document's attributes. */ - private DocAttributeSet attributeSet; - - /** - * Constructor. - * - * @param ostream an output stream representing the pdf doc - * @param attributes the attributes of the document - */ - public PDFPrintStreamDoc (ByteArrayOutputStream ostream, DocAttributeSet attributes) { - stream = new ByteArrayInputStream(ostream.toByteArray()); - if (attributes != null) { - attributeSet = AttributeSetUtilities.unmodifiableView(attributes); - } - } - - /** - * Flavor is PDF. - * - * @return PDF flavor - */ - @Override - public DocFlavor getDocFlavor () { - return DocFlavor.INPUT_STREAM.PDF; - } - - @Override - public DocAttributeSet getAttributes () { - return attributeSet; - } - - /* Since the data is to be supplied as an InputStream delegate to - * getStreamForBytes(). - */ - @Override - public Object getPrintData () throws IOException { - return getStreamForBytes(); - } - - /** - * Intentionally null since the flavor is PDF. - * - * @return null - */ - @Override - public Reader getReaderForText () { - return null; - } - - /* Return the print data as an InputStream. - * Always return the same instance. - */ - @Override - public InputStream getStreamForBytes () throws IOException { - return stream; - } -} diff --git a/src/net/sf/openrocket/gui/print/PaperOrientation.java b/src/net/sf/openrocket/gui/print/PaperOrientation.java deleted file mode 100644 index 0e973e0f..00000000 --- a/src/net/sf/openrocket/gui/print/PaperOrientation.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.sf.openrocket.gui.print; - -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.RectangleReadOnly; - -public enum PaperOrientation { - - PORTRAIT("Portrait") { - @Override - public Rectangle orient(Rectangle rect) { - return new RectangleReadOnly(rect); - } - }, - LANDSCAPE("Landscape") { - @Override - public Rectangle orient(Rectangle rect) { - return new RectangleReadOnly(new Rectangle(rect).rotate()); - } - }; - - - private final String name; - - private PaperOrientation(String name) { - this.name = name; - } - - /** - * Change the orientation of a portrait paper to the orientation represented by this - * orientation. - * - * @param rect the original paper size rectangle - * @return the oriented paper size rectangle - */ - public abstract Rectangle orient(Rectangle rect); - - - @Override - public String toString() { - return name; - } -} diff --git a/src/net/sf/openrocket/gui/print/PaperSize.java b/src/net/sf/openrocket/gui/print/PaperSize.java deleted file mode 100644 index d3d07410..00000000 --- a/src/net/sf/openrocket/gui/print/PaperSize.java +++ /dev/null @@ -1,193 +0,0 @@ -package net.sf.openrocket.gui.print; - -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; -import java.util.Locale; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -import com.itextpdf.text.PageSize; -import com.itextpdf.text.Rectangle; - -public enum PaperSize { - A3("A3", PageSize.A3), - A4("A4", PageSize.A4), - A5("A5", PageSize.A5), - LETTER("Letter", PageSize.LETTER), - LEGAL("Legal", PageSize.LEGAL); - - private final String name; - private final Rectangle size; - - private PaperSize(String name, Rectangle size) { - this.name = name; - this.size = size; - } - - public Rectangle getSize() { - return size; - } - - @Override - public String toString() { - return name; - } - - - - ////////////////////////// - - private static final LogHelper log = Application.getLogger(); - private static PaperSize defaultSize = null; - - /** - * Return the default paper size for the current system. - * @return the default paper size - */ - public static PaperSize getDefault() { - if (defaultSize == null) { - - // Test environment variable "PAPERSIZE" (Unix) - defaultSize = getDefaultFromEnvironmentVariable(); - if (defaultSize != null) { - log.info("Selecting default paper size from PAPERSIZE environment variable: " + defaultSize); - return defaultSize; - } - - // Test /etc/papersize (Unix) - defaultSize = getDefaultFromEtcPapersize(); - if (defaultSize != null) { - log.info("Selecting default paper size from /etc/papersize: " + defaultSize); - return defaultSize; - } - - // Test user.country - defaultSize = getDefaultForCountry(System.getProperty("user.country")); - if (defaultSize != null) { - log.info("Selecting default paper size based on user.country: " + defaultSize); - return defaultSize; - } - - // Test locale country - defaultSize = getDefaultForCountry(Locale.getDefault().getCountry()); - if (defaultSize != null) { - log.info("Selecting default paper size based on locale country: " + defaultSize); - return defaultSize; - } - - // Fallback to A4 - defaultSize = A4; - log.info("Selecting default paper size fallback: " + defaultSize); - } - - return defaultSize; - } - - - /** - * Attempt to read the default paper size from the "PAPERSIZE" environment variable. - * - * @return the default paper size if successful, or <code>null</code> if unable to read/parse file. - */ - private static PaperSize getDefaultFromEnvironmentVariable() { - String str = System.getenv("PAPERSIZE"); - return getSizeFromString(str); - } - - /** - * Attempt to read the default paper size from the file defined by the environment variable - * PAPERCONF or from /etc/papersize. - * - * @return the default paper size if successful, or <code>null</code> if unable to read/parse file. - */ - private static PaperSize getDefaultFromEtcPapersize() { - - // Find file to read - String file = System.getenv("PAPERCONF"); - if (file == null) { - file = "/etc/papersize"; - } - - // Attempt to read the file - BufferedReader in = null; - try { - - String str; - in = new BufferedReader(new FileReader(file)); - while ((str = in.readLine()) != null) { - if (str.matches("^\\s*(#.*|$)")) { - continue; - } - break; - } - - return getSizeFromString(str); - - } catch (IOException e) { - - // Could not read file - return null; - - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException e) { - } - } - } - } - - - /** - * Get a paper size based on a string. The string is trimmed and case-insensitively - * compared to the base names of the paper sizes. - * - * @param size the size string (may be null) - * @return the corresponding paper size, or null if unknown - */ - static PaperSize getSizeFromString(String size) { - if (size == null) { - return null; - } - - size = size.trim(); - for (PaperSize p : PaperSize.values()) { - if (p.name.equalsIgnoreCase(size)) { - return p; - } - } - return null; - } - - - /** - * Get default paper size for a specific country. This method falls back to A4 for - * any country not known to use Letter. - * - * @param country the 2-char country code (may be null) - * @return the paper size, or <code>null</code> if country is not a country code - */ - static PaperSize getDefaultForCountry(String country) { - /* - * List is based on info from http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/territory_language_information.html - * OpenOffice.org agrees with this: http://wiki.services.openoffice.org/wiki/DefaultPaperSize#Summary - */ - final String[] letterCountries = { "BZ", "CA", "CL", "CO", "CR", "SV", "GT", "MX", "NI", "PA", "PH", "PR", "US", "VE" }; - - if (country == null || !country.matches("^[a-zA-Z][a-zA-Z]$")) { - return null; - } - - country = country.toUpperCase(); - for (String c : letterCountries) { - if (c.equals(country)) { - return LETTER; - } - } - return A4; - } - -} diff --git a/src/net/sf/openrocket/gui/print/PrintController.java b/src/net/sf/openrocket/gui/print/PrintController.java deleted file mode 100644 index 69e4df05..00000000 --- a/src/net/sf/openrocket/gui/print/PrintController.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * PrintController.java - * - */ -package net.sf.openrocket.gui.print; - -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.ExceptionConverter; -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.pdf.PdfBoolean; -import com.itextpdf.text.pdf.PdfName; -import com.itextpdf.text.pdf.PdfWriter; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.gui.print.visitor.FinMarkingGuideStrategy; -import net.sf.openrocket.gui.print.visitor.FinSetPrintStrategy; -import net.sf.openrocket.gui.print.visitor.PartsDetailVisitorStrategy; -import net.sf.openrocket.gui.print.visitor.TransitionStrategy; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.Iterator; -import java.util.Set; - -/** - * This is the main active object for printing. It performs all actions necessary to create and populate the print - * file. - */ -public class PrintController { - - /** - * Print the selected components to a PDF document. - * - * @param doc the OR document - * @param toBePrinted the user chosen items to print - * @param outputFile the file being written to - * @param settings the print settings - */ - public void print(OpenRocketDocument doc, Iterator<PrintableContext> toBePrinted, OutputStream outputFile, - PrintSettings settings) { - - Document idoc = new Document(getSize(settings)); - PdfWriter writer = null; - try { - writer = PdfWriter.getInstance(idoc, outputFile); - writer.setStrictImageSequence(true); - - writer.addViewerPreference(PdfName.PRINTSCALING, PdfName.NONE); - writer.addViewerPreference(PdfName.PICKTRAYBYPDFSIZE, PdfBoolean.PDFTRUE); - try { - idoc.open(); - Thread.sleep(1000); - } catch (InterruptedException e) { - } - while (toBePrinted.hasNext()) { - PrintableContext printableContext = toBePrinted.next(); - - Set<Integer> stages = printableContext.getStageNumber(); - - switch (printableContext.getPrintable()) { - case DESIGN_REPORT: - DesignReport dp = new DesignReport(doc, idoc); - dp.writeToDocument(writer); - idoc.newPage(); - break; - case FIN_TEMPLATE: - final FinSetPrintStrategy finWriter = new FinSetPrintStrategy(idoc, - writer, - stages); - finWriter.writeToDocument(doc.getRocket()); - break; - case PARTS_DETAIL: - final PartsDetailVisitorStrategy detailVisitor = new PartsDetailVisitorStrategy(idoc, - writer, - stages); - detailVisitor.writeToDocument(doc.getRocket()); - detailVisitor.close(); - idoc.newPage(); - break; - case TRANSITION_TEMPLATE: - final TransitionStrategy tranWriter = new TransitionStrategy(idoc, writer, stages); - tranWriter.writeToDocument(doc.getRocket(), false); - idoc.newPage(); - break; - - case NOSE_CONE_TEMPLATE: - final TransitionStrategy coneWriter = new TransitionStrategy(idoc, writer, stages); - coneWriter.writeToDocument(doc.getRocket(), true); - idoc.newPage(); - break; - - case FIN_MARKING_GUIDE: - final FinMarkingGuideStrategy fmg = new FinMarkingGuideStrategy(idoc, writer); - fmg.writeToDocument(doc.getRocket()); - idoc.newPage(); - break; - } - } - //Stupid iText throws a really nasty exception if there is no data when close is called. - if (writer.getCurrentDocumentSize() <= 140) { - writer.setPageEmpty(false); - } - writer.close(); - idoc.close(); - } catch (DocumentException e) { - } catch (ExceptionConverter ec) { - } finally { - if (outputFile != null) { - try { - outputFile.close(); - } catch (IOException e) { - } - } - } - } - - /** - * Get the correct paper size from the print settings. - * - * @param settings the print settings - * @return the paper size - */ - private Rectangle getSize(PrintSettings settings) { - PaperSize size = settings.getPaperSize(); - PaperOrientation orientation = settings.getPaperOrientation(); - return orientation.orient(size.getSize()); - } - -} diff --git a/src/net/sf/openrocket/gui/print/PrintFigure.java b/src/net/sf/openrocket/gui/print/PrintFigure.java deleted file mode 100644 index 3f4655c6..00000000 --- a/src/net/sf/openrocket/gui/print/PrintFigure.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * PrintFigure.java - */ -package net.sf.openrocket.gui.print; - -import net.sf.openrocket.gui.scalefigure.RocketFigure; -import net.sf.openrocket.rocketcomponent.Configuration; - -/** - * A figure used to override the scale factor in RocketFigure. This allows pinpoint scaling to allow a diagram - * to fit in the width of the chosen page size. - */ -public class PrintFigure extends RocketFigure { - - /** - * Constructor. - * - * @param configuration the configuration - */ - public PrintFigure(final Configuration configuration) { - super(configuration); - } - - @Override - protected double computeTy(int heightPx) { - super.computeTy(heightPx); - return 0; - } - - public void setScale(final double theScale) { - this.scale = theScale; //dpi/0.0254*scaling; - updateFigure(); - } -} diff --git a/src/net/sf/openrocket/gui/print/PrintSettings.java b/src/net/sf/openrocket/gui/print/PrintSettings.java deleted file mode 100644 index 6537f400..00000000 --- a/src/net/sf/openrocket/gui/print/PrintSettings.java +++ /dev/null @@ -1,89 +0,0 @@ -package net.sf.openrocket.gui.print; - -import java.awt.Color; - -import net.sf.openrocket.util.AbstractChangeSource; - -/** - * A class containing all printing settings. - */ -public class PrintSettings extends AbstractChangeSource { - - private Color templateFillColor = Color.LIGHT_GRAY; - private Color templateBorderColor = Color.DARK_GRAY; - - private PaperSize paperSize = PaperSize.getDefault(); - private PaperOrientation paperOrientation = PaperOrientation.PORTRAIT; - - - public Color getTemplateFillColor() { - return templateFillColor; - } - - public void setTemplateFillColor(Color templateFillColor) { - // Implicitly tests against setting null - if (templateFillColor.equals(this.templateFillColor)) { - return; - } - this.templateFillColor = templateFillColor; - fireChangeEvent(); - } - - public Color getTemplateBorderColor() { - return templateBorderColor; - } - - public void setTemplateBorderColor(Color templateBorderColor) { - // Implicitly tests against setting null - if (templateBorderColor.equals(this.templateBorderColor)) { - return; - } - this.templateBorderColor = templateBorderColor; - fireChangeEvent(); - } - - public PaperSize getPaperSize() { - return paperSize; - } - - public void setPaperSize(PaperSize paperSize) { - if (paperSize.equals(this.paperSize)) { - return; - } - this.paperSize = paperSize; - fireChangeEvent(); - } - - public PaperOrientation getPaperOrientation() { - return paperOrientation; - } - - public void setPaperOrientation(PaperOrientation orientation) { - if (orientation.equals(paperOrientation)) { - return; - } - this.paperOrientation = orientation; - fireChangeEvent(); - } - - - - /** - * Load settings from the specified print settings. - * @param settings the settings to load - */ - public void loadFrom(PrintSettings settings) { - this.templateFillColor = settings.templateFillColor; - this.templateBorderColor = settings.templateBorderColor; - this.paperSize = settings.paperSize; - this.paperOrientation = settings.paperOrientation; - fireChangeEvent(); - } - - - @Override - public String toString() { - return "PrintSettings [templateFillColor=" + templateFillColor + ", templateBorderColor=" + templateBorderColor + ", paperSize=" + paperSize + ", paperOrientation=" + paperOrientation + "]"; - } - -} diff --git a/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java b/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java deleted file mode 100644 index 53a710c8..00000000 --- a/src/net/sf/openrocket/gui/print/PrintSimulationWorker.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * PrintSimulationWorker.java - */ -package net.sf.openrocket.gui.print; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.main.SimulationWorker; -import net.sf.openrocket.simulation.FlightData; - -/** - * A SimulationWorker that simulates the rocket flight in the background and sets the results to the extra text when - * finished. The worker can be cancelled if necessary. - */ -public class PrintSimulationWorker { - - public static FlightData doit(Simulation sim) { - return new InnerPrintSimulationWorker(sim).doit(); - } - - static class InnerPrintSimulationWorker extends SimulationWorker { - - public InnerPrintSimulationWorker(Simulation sim) { - super(sim); - } - - public FlightData doit() { - return doInBackground(); - } - - @Override - protected void simulationDone() { - // Do nothing if cancelled - if (isCancelled()) { - return; - } - - simulation.getSimulatedData(); - } - - - /** - * Called if the simulation is interrupted due to an exception. - * - * @param t the Throwable that caused the interruption - */ - @Override - protected void simulationInterrupted(final Throwable t) { - } - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/PrintUnit.java b/src/net/sf/openrocket/gui/print/PrintUnit.java deleted file mode 100644 index c603f2f9..00000000 --- a/src/net/sf/openrocket/gui/print/PrintUnit.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * PrintUnit.java - */ -package net.sf.openrocket.gui.print; - -/** - * Utilities for print units. - */ -public enum PrintUnit { - INCHES { - public double toInches(double d) { return d; } - public double toMillis(double d) { return d/INCHES_PER_MM; } - public double toCentis(double d) { return d/(INCHES_PER_MM*TEN); } - public double toMeters(double d) { return d/(INCHES_PER_MM*TEN*TEN*TEN); } - public long toPoints(double d) { return (long)(d * POINTS_PER_INCH); } - public double convert(double d, PrintUnit u) { return u.toInches(d); } - }, - MILLIMETERS { - public double toInches(double d) { return d * INCHES_PER_MM; } - public double toMillis(double d) { return d; } - public double toCentis(double d) { return d/TEN; } - public double toMeters(double d) { return d/(TEN*TEN*TEN); } - public long toPoints(double d) { return INCHES.toPoints(toInches(d)); } - public double convert(double d, PrintUnit u) { return u.toMillis(d); } - }, - CENTIMETERS { - public double toInches(double d) { return d * INCHES_PER_MM * TEN; } - public double toMillis(double d) { return d * TEN; } - public double toCentis(double d) { return d; } - public double toMeters(double d) { return d/(TEN*TEN); } - public long toPoints(double d) { return INCHES.toPoints(toInches(d)); } - public double convert(double d, PrintUnit u) { return u.toCentis(d); } - }, - METERS { - public double toInches(double d) { return d * INCHES_PER_MM * TEN * TEN * TEN; } - public double toMillis(double d) { return d * TEN * TEN * TEN; } - public double toCentis(double d) { return d * TEN * TEN; } - public double toMeters(double d) { return d; } - public long toPoints(double d) { return INCHES.toPoints(toInches(d)); } - public double convert(double d, PrintUnit u) { return u.toMeters(d); } - }, - POINTS { - public double toInches(double d) { return d/POINTS_PER_INCH; } - public double toMillis(double d) { return d/(POINTS_PER_INCH * INCHES_PER_MM); } - public double toCentis(double d) { return toMillis(d)/TEN; } - public double toMeters(double d) { return toMillis(d)/(TEN*TEN*TEN); } - public long toPoints(double d) { return (long)d; } - public double convert(double d, PrintUnit u) { return u.toPoints(d); } - }; - - // Handy constants for conversion methods - public static final double INCHES_PER_MM = 0.0393700787d; - public static final double MM_PER_INCH = 1.0d/INCHES_PER_MM; - public static final long TEN = 10; - /** - * PPI is Postscript Point and is a standard of 72. Java2D also uses this internally as a pixel-per-inch, so pixels - * and points are for the most part interchangeable (unless the defaults are changed), which makes translating - * between the screen and a print job easier. - * - * Not to be confused with Dots-Per-Inch, which is printer and print mode dependent. - */ - public static final int POINTS_PER_INCH = 72; - - // To maintain full signature compatibility with 1.5, and to improve the - // clarity of the generated javadoc (see 6287639: Abstract methods in - // enum classes should not be listed as abstract), method convert - // etc. are not declared abstract but otherwise act as abstract methods. - - /** - * Convert the given length in the given unit to this - * unit. Conversions from finer to coarser granularities - * truncate, so may lose precision. - * - * <p>For example, to convert 10 inches to point, use: - * <tt>PrintUnit.POINTS.convert(10L, PrintUnit.INCHES)</tt> - * - * @param sourceLength the length in the given <tt>sourceUnit</tt> - * @param sourceUnit the unit of the <tt>sourceDuration</tt> argument - * - * @return the converted length in this unit, - * or <tt>Long.MIN_VALUE</tt> if conversion would negatively - * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow. - */ - public double convert(double sourceLength, PrintUnit sourceUnit) { - throw new AbstractMethodError(); - } - - /** - * Equivalent to <tt>INCHES.convert(length, this)</tt>. - * - * @param length the length - * - * @return the converted length, - * or <tt>Long.MIN_VALUE</tt> if conversion would negatively - * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow. - * @see #convert - */ - public double toInches(double length) { - throw new AbstractMethodError(); - } - - /** - * Equivalent to <tt>MILLIMETERS.convert(length, this)</tt>. - * - * @param length the length - * - * @return the converted length, - * or <tt>Long.MIN_VALUE</tt> if conversion would negatively - * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow. - * @see #convert - */ - public double toMillis(double length) { - throw new AbstractMethodError(); - } - - /** - * Equivalent to <tt>CENTIMETERS.convert(length, this)</tt>. - * - * @param length the length - * - * @return the converted length, - * or <tt>Long.MIN_VALUE</tt> if conversion would negatively - * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow. - * @see #convert - */ - public double toCentis(double length) { - throw new AbstractMethodError(); - } - - /** - * Equivalent to <tt>METERS.convert(length, this)</tt>. - * - * @param length the length - * - * @return the converted length, - * or <tt>Long.MIN_VALUE</tt> if conversion would negatively - * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow. - * @see #convert - */ - public double toMeters(double length) { - throw new AbstractMethodError(); - } - - /** - * Equivalent to <tt>POINTS.convert(length, this)</tt>. - * - * @param length the length - * - * @return the converted length, - * or <tt>Long.MIN_VALUE</tt> if conversion would negatively - * overflow, or <tt>Long.MAX_VALUE</tt> if it would positively overflow. - * @see #convert - */ - public long toPoints(double length) { - throw new AbstractMethodError(); - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/PrintUtilities.java b/src/net/sf/openrocket/gui/print/PrintUtilities.java deleted file mode 100644 index 7f096efc..00000000 --- a/src/net/sf/openrocket/gui/print/PrintUtilities.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * PrintUtilities.java - */ -package net.sf.openrocket.gui.print; - - -import java.awt.Component; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.print.PageFormat; -import java.awt.print.Printable; - -import javax.swing.RepaintManager; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -import com.itextpdf.text.Chunk; -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Font; -import com.itextpdf.text.Paragraph; - -/** - * Utilities methods and fonts used for printing. - */ -public class PrintUtilities implements Printable { - - /** - * The logger. - */ - private static final LogHelper log = Application.getLogger(); - - public static final int NORMAL_FONT_SIZE = Font.DEFAULTSIZE - 3; - public static final int SMALL_FONT_SIZE = NORMAL_FONT_SIZE - 3; - - public static final Font BOLD = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE, Font.BOLD); - public static final Font BIG_BOLD = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE + 3, Font.BOLD); - public static final Font BOLD_UNDERLINED = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE, - Font.BOLD | Font.UNDERLINE); - public static final Font NORMAL = new Font(Font.FontFamily.HELVETICA, NORMAL_FONT_SIZE); - public static final Font SMALL = new Font(Font.FontFamily.HELVETICA, SMALL_FONT_SIZE); - - - private Component componentToBePrinted; - - public PrintUtilities(Component componentToBePrinted) { - this.componentToBePrinted = componentToBePrinted; - } - - @Override - public int print(Graphics g, PageFormat pageFormat, int pageIndex) { - if (pageIndex > 0) { - return (NO_SUCH_PAGE); - } else { - Graphics2D g2d = (Graphics2D) g; - translateToJavaOrigin(g2d, pageFormat); - disableDoubleBuffering(componentToBePrinted); - componentToBePrinted.paint(g2d); - enableDoubleBuffering(componentToBePrinted); - return (PAGE_EXISTS); - } - } - - public static void disableDoubleBuffering(Component c) { - RepaintManager currentManager = RepaintManager.currentManager(c); - currentManager.setDoubleBufferingEnabled(false); - } - - public static void enableDoubleBuffering(Component c) { - RepaintManager currentManager = RepaintManager.currentManager(c); - currentManager.setDoubleBufferingEnabled(true); - } - - - /** - * Translate the page format coordinates onto the graphics object using Java's origin (top left). - * - * @param g2d the graphics object - * @param pageFormat the print page format - */ - public static void translateToJavaOrigin(Graphics2D g2d, PageFormat pageFormat) { - g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY()); - } - - /** - * Add text as a new paragraph in a given font to the document. - * - * @param document the document - * @param font the font - * @param title the title - */ - public static void addText(Document document, com.itextpdf.text.Font font, String title) { - Chunk sectionHeader = new Chunk(title); - sectionHeader.setFont(font); - try { - Paragraph p = new Paragraph(); - p.add(sectionHeader); - document.add(p); - } catch (DocumentException e) { - log.error("Could not add paragraph.", e); - } - } -} diff --git a/src/net/sf/openrocket/gui/print/PrintableContext.java b/src/net/sf/openrocket/gui/print/PrintableContext.java deleted file mode 100644 index 4a5ddcac..00000000 --- a/src/net/sf/openrocket/gui/print/PrintableContext.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * PrintableContext.java - * - */ -package net.sf.openrocket.gui.print; - -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; - -/** - * Instances of this class are meant to keep track of what the user has selected to be printed. - */ -public class PrintableContext implements Comparable<PrintableContext>, Iterable<PrintableContext> { - - /** - * The stage number. May be null for printables that have no stage meaning. - */ - private Set<Integer> stageNumber; - - /** - * The type of thing to be printed. - */ - private OpenRocketPrintable printable; - - /** - * Sort of a reverse map that tracks each type of printable item and the stages for which that item is to be printed. - */ - private final Map<OpenRocketPrintable, Set<Integer>> previous = new TreeMap<OpenRocketPrintable, Set<Integer>>(); - - /** - * Constructor. - */ - public PrintableContext() { - } - - /** - * Constructor. - * - * @param theStageNumber the stage number of the printable; may be null if not applicable - * @param thePrintable the type of the thing to be printed - * - * @throws IllegalArgumentException thrown if thePrintable.isStageSpecific - */ - private PrintableContext(final Set<Integer> theStageNumber, final OpenRocketPrintable thePrintable) - throws IllegalArgumentException { - if (thePrintable.isStageSpecific() && theStageNumber == null) { - throw new IllegalArgumentException("A stage number must be provided when a printable is stage specific."); - } - stageNumber = theStageNumber; - printable = thePrintable; - } - - /** - * Add a type of printable to a stage (number). - * - * @param theStageNumber the stage number - * @param thePrintable the printable to associate with the stage - */ - public void add(final Integer theStageNumber, final OpenRocketPrintable thePrintable) { - Set<Integer> stages = previous.get(thePrintable); - if (stages == null) { - stages = new TreeSet<Integer>(); - previous.put(thePrintable, stages); - } - if (theStageNumber != null) { - stages.add(theStageNumber); - } - } - - /** PrintableContext iterator. */ - @Override - public Iterator<PrintableContext> iterator() { - return new Iterator<PrintableContext>() { - - Iterator<OpenRocketPrintable> keyIter = previous.keySet().iterator(); - - @Override - public boolean hasNext() { - return keyIter.hasNext(); - } - - @Override - public PrintableContext next() { - final OpenRocketPrintable key = keyIter.next(); - return new PrintableContext(previous.get(key), key); - } - - @Override - public void remove() { - } - }; - - } - - /** - * Get the stage number, if it's applicable to the printable. - * - * @return the stage number - */ - public Set<Integer> getStageNumber() { - return stageNumber; - } - - /** - * Get the printable. - * - * @return the printable - */ - public OpenRocketPrintable getPrintable() { - return printable; - } - - @Override - public int compareTo(final PrintableContext other) { - return this.printable.getPrintOrder() - other.printable.getPrintOrder(); - } - -} diff --git a/src/net/sf/openrocket/gui/print/PrintableFinSet.java b/src/net/sf/openrocket/gui/print/PrintableFinSet.java deleted file mode 100644 index cf07cb13..00000000 --- a/src/net/sf/openrocket/gui/print/PrintableFinSet.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * PrintableFinSet.java - */ -package net.sf.openrocket.gui.print; - -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.util.Coordinate; - -import javax.swing.*; -import java.awt.*; -import java.awt.geom.GeneralPath; -import java.awt.image.BufferedImage; -import java.awt.print.PageFormat; -import java.awt.print.Printable; -import java.awt.print.PrinterException; - -/** - * This class allows for a FinSet to be printable. It does so by decorating an existing finset (which will not be - * modified) and rendering it within a JPanel. The JPanel is not actually visualized on a display, but instead renders - * it to a print device. - */ -public class PrintableFinSet extends JPanel implements Printable { - - /** - * The object that represents the shape (outline) of the fin. This gets drawn onto the Swing component. - */ - protected GeneralPath polygon = null; - - /** - * The X margin. - */ - private final int marginX = (int)(PrintUnit.POINTS_PER_INCH * 0.3f); - /** - * The Y margin. - */ - private final int marginY = (int)(PrintUnit.POINTS_PER_INCH * 0.3f); - /** - * The minimum X coordinate. - */ - private int minX = 0; - /** - * The minimum Y coordinate. - */ - private int minY = 0; - - /** - * Constructor. - * - * @param fs the finset to print - */ - public PrintableFinSet (FinSet fs) { - this(fs.getFinPointsWithTab()); - } - - /** - * Construct a fin set from a set of points. - * - * @param points an array of points. - */ - public PrintableFinSet (Coordinate[] points) { - super(false); - init(points); - setBackground(Color.white); - } - - /** - * Initialize the fin set polygon and set the size of the component. - * - * @param points an array of points. - */ - private void init (Coordinate[] points) { - - polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, points.length); - polygon.moveTo(0, 0); - - int maxX = 0; - int maxY = 0; - - for (Coordinate point : points) { - final long x = PrintUnit.METERS.toPoints(point.x); - final long y = PrintUnit.METERS.toPoints(point.y); - minX = (int) Math.min(x, minX); - minY = (int) Math.min(y, minY); - maxX = (int) Math.max(x, maxX); - maxY = (int) Math.max(y, maxY); - polygon.lineTo(x, y); - } - polygon.closePath(); - - setSize(maxX - minX, maxY - minY); - } - - /** - * Get the X-axis margin value. - * - * @return margin, in points - */ - protected double getMarginX () { - return marginX; - } - - /** - * Get the Y-axis margin value. - * - * @return margin, in points - */ - protected double getMarginY () { - return marginY; - } - - /** - * From the java.awt.print.Printable interface. - * <p/> - * Prints the page at the specified index into the specified {@link java.awt.Graphics} context in the specified - * format. A <code>PrinterJob</code> calls the <code>Printable</code> interface to request that a page be rendered - * into the context specified by <code>graphics</code>. The format of the page to be drawn is specified by - * <code>pageFormat</code>. The zero based index of the requested page is specified by <code>pageIndex</code>. If - * the requested page does not exist then this method returns NO_SUCH_PAGE; otherwise PAGE_EXISTS is returned. The - * <code>Graphics</code> class or subclass implements the {@link java.awt.print.PrinterGraphics} interface to - * provide additional information. If the <code>Printable</code> object aborts the print job then it throws a - * {@link java.awt.print.PrinterException}. - * <p/> - * Note: This is not currently used in OpenRocket. It's only here for reference. - * - * @param graphics the context into which the page is drawn - * @param pageFormat the size and orientation of the page being drawn - * @param pageIndex the zero based index of the page to be drawn - * - * @return PAGE_EXISTS if the page is rendered successfully or NO_SUCH_PAGE if <code>pageIndex</code> specifies a - * non-existent page. - * - * @throws java.awt.print.PrinterException - * thrown when the print job is terminated. - */ - @Override - public int print (final Graphics graphics, final PageFormat pageFormat, final int pageIndex) - throws PrinterException { - - Graphics2D g2d = (Graphics2D) graphics; - PrintUtilities.translateToJavaOrigin(g2d, pageFormat); - PrintUtilities.disableDoubleBuffering(this); - paint(g2d); - PrintUtilities.enableDoubleBuffering(this); - return Printable.PAGE_EXISTS; - } - - /** - * Returns a generated image of the fin set. May then be used wherever AWT images can be used, or converted to - * another image/picture format and used accordingly. - * - * @return an awt image of the fin set - */ - public Image createImage () { - int width = getWidth() + marginX; - int height = getHeight() + marginY; - // Create a buffered image in which to draw - BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - // Create a graphics contents on the buffered image - Graphics2D g2d = bufferedImage.createGraphics(); - // Draw graphics - g2d.setBackground(Color.white); - g2d.clearRect(0, 0, width, height); - paintComponent(g2d); - // Graphics context no longer needed so dispose it - g2d.dispose(); - return bufferedImage; - } - - /** - * Render the fin set onto the graphics context. This is done by creating a GeneralPath component that follows the - * outline of the fin set coordinates to create a polygon, which is then drawn onto the graphics context. - * Through-the-wall fin tabs are supported if they are present. - * - * @param g the Java2D graphics context - */ - @Override - public void paintComponent (Graphics g) { - super.paintComponent(g); - Graphics2D g2d = (Graphics2D) g; - - int x = 0; - int y = 0; - - // The minimum X/Y can be negative (primarily only Y due to fin tabs; rarely (never) X, but protect both anyway). - if (minX < marginX) { - x = marginX + Math.abs(minX); - } - if (minY < marginY) { - y = marginY + Math.abs(minY); - } - // Reset the origin. - g2d.translate(x, y); - g2d.setPaint(TemplateProperties.getFillColor()); - g2d.fill(polygon); - g2d.setPaint(TemplateProperties.getLineColor()); - g2d.draw(polygon); - } - -} diff --git a/src/net/sf/openrocket/gui/print/PrintableNoseCone.java b/src/net/sf/openrocket/gui/print/PrintableNoseCone.java deleted file mode 100644 index 78afe6f0..00000000 --- a/src/net/sf/openrocket/gui/print/PrintableNoseCone.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.sf.openrocket.gui.print; - -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.Shape; - -import net.sf.openrocket.gui.rocketfigure.TransitionShapes; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.util.Transformation; - -public class PrintableNoseCone extends AbstractPrintableTransition { - - /** - * If the component to be drawn is a nose cone, save a reference to it. - */ - private NoseCone target; - - /** - * Construct a printable nose cone. - * - * @param noseCone the component to print - */ - public PrintableNoseCone(Transition noseCone) { - super(false, noseCone); - } - - @Override - protected void init(Transition component) { - - target = (NoseCone) component; - double radius = target.getForeRadius(); - if (radius < target.getAftRadius()) { - radius = target.getAftRadius(); - } - setSize((int) PrintUnit.METERS.toPoints(2 * radius) + marginX, - (int) PrintUnit.METERS.toPoints(target.getLength() + target.getAftShoulderLength()) + marginY); - } - - /** - * Draw a nose cone. - * - * @param g2 the graphics context - */ - @Override - protected void draw(Graphics2D g2) { - Shape[] shapes = TransitionShapes.getShapesSide(target, Transformation.rotate_x(0d), PrintUnit.METERS.toPoints(1)); - - if (shapes != null && shapes.length > 0) { - Rectangle r = shapes[0].getBounds(); - g2.translate(marginX + r.getHeight() / 2, marginY); - g2.rotate(Math.PI / 2); - for (Shape shape : shapes) { - g2.draw(shape); - } - g2.rotate(-Math.PI / 2); - } - } -} diff --git a/src/net/sf/openrocket/gui/print/PrintableTransition.java b/src/net/sf/openrocket/gui/print/PrintableTransition.java deleted file mode 100644 index a70703da..00000000 --- a/src/net/sf/openrocket/gui/print/PrintableTransition.java +++ /dev/null @@ -1,207 +0,0 @@ -package net.sf.openrocket.gui.print; - -import java.awt.BasicStroke; -import java.awt.Graphics2D; -import java.awt.geom.Arc2D; -import java.awt.geom.GeneralPath; -import java.awt.geom.Line2D; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; - -import net.sf.openrocket.rocketcomponent.Transition; - -/** - * This class allows for a Transition to be printable. It does so by decorating an existing transition (which will not be - * modified) and rendering it within a JPanel. The JPanel is not actually visualized on a display, but instead renders - * it to a print device. - * <p/> - * Note: Currently nose cones are only supported by drawing the 2D projection of the profile. A more useful approach - * may be to draw a myriahedral projection that can be cut out and bent to form the shape. - */ -public class PrintableTransition extends AbstractPrintableTransition { - - /** - * Dashed array value. - */ - private final static float dash1[] = { 4.0f }; - /** - * The dashed stroke for glue tab. - */ - private final static BasicStroke dashed = new BasicStroke(1.0f, - BasicStroke.CAP_BUTT, - BasicStroke.JOIN_MITER, - 10.0f, dash1, 0.0f); - - /** - * The layout is an outer arc, an inner arc, and two lines one either endpoints that connect the arcs. - * Most of the math involves transposing geometric cartesian coordinates to the Java AWT coordinate system. - */ - private Path2D gp; - - /** - * The glue tab. - */ - private Path2D glueTab1; - - /** - * The alignment marks. - */ - private Line2D tick1, tick2; - - /** - * The x coordinates for the two ticks drawn at theta degrees. - */ - private int tick3X, tick4X; - - /** - * The angle, in degrees. - */ - private float theta; - - /** - * The x,y coordinates for where the virtual circle center is located. - */ - private int circleCenterX, circleCenterY; - - /** - * Constructor. - * - * @param transition the transition to print - */ - public PrintableTransition(Transition transition) { - super(false, transition); - } - - @Override - protected void init(Transition component) { - - double r1 = component.getAftRadius(); - double r2 = component.getForeRadius(); - - //Regardless of orientation, we have the convention of R1 as the smaller radius. Flip if different. - if (r1 > r2) { - r1 = r2; - r2 = component.getAftRadius(); - } - double len = component.getLength(); - double v = r2 - r1; - double tmp = Math.sqrt(v * v + len * len); - double factor = tmp / v; - - theta = (float) (360d * v / tmp); - - int r1InPoints = (int) PrintUnit.METERS.toPoints(r1 * factor); - int r2InPoints = (int) PrintUnit.METERS.toPoints(r2 * factor); - - int x = marginX; - int tabOffset = 35; - int y = tabOffset + marginY; - - Arc2D.Double outerArc = new Arc2D.Double(); - Arc2D.Double innerArc = new Arc2D.Double(); - - //If the arcs are more than 3/4 of a circle, then assume the height (y) is the same as the radius of the bigger arc. - if (theta >= 270) { - y += r2InPoints; - } - //If the arc is between 1/2 and 3/4 of a circle, then compute the actual height based upon the angle and radius - //of the bigger arc. - else if (theta >= 180) { - double thetaRads = Math.toRadians(theta - 180); - y += (int) ((Math.cos(thetaRads) * r2InPoints) * Math.tan(thetaRads)); - } - - circleCenterY = y; - circleCenterX = r2InPoints + x; - - //Create the larger arc. - outerArc.setArcByCenter(circleCenterX, circleCenterY, r2InPoints, 180, theta, Arc2D.OPEN); - - //Create the smaller arc. - innerArc.setArcByCenter(circleCenterX, circleCenterY, r1InPoints, 180, theta, Arc2D.OPEN); - - //Create the line between the start of the larger arc and the start of the smaller arc. - Path2D.Double line = new Path2D.Double(); - line.setWindingRule(Path2D.WIND_NON_ZERO); - line.moveTo(x, y); - final int width = r2InPoints - r1InPoints; - line.lineTo(width + x, y); - - //Create the line between the endpoint of the larger arc and the endpoint of the smaller arc. - Path2D.Double closingLine = new Path2D.Double(); - closingLine.setWindingRule(Path2D.WIND_NON_ZERO); - Point2D innerArcEndPoint = innerArc.getEndPoint(); - closingLine.moveTo(innerArcEndPoint.getX(), innerArcEndPoint.getY()); - Point2D outerArcEndPoint = outerArc.getEndPoint(); - closingLine.lineTo(outerArcEndPoint.getX(), outerArcEndPoint.getY()); - - //Add all shapes to the polygon path. - gp = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4); - gp.append(line, false); - gp.append(outerArc, false); - gp.append(closingLine, false); - gp.append(innerArc, false); - - //Create the glue tab. - glueTab1 = new Path2D.Float(GeneralPath.WIND_EVEN_ODD, 4); - glueTab1.moveTo(x, y); - glueTab1.lineTo(x + tabOffset, y - tabOffset); - glueTab1.lineTo(width + x - tabOffset, y - tabOffset); - glueTab1.lineTo(width + x, y); - - //Create tick marks for alignment, 1/4 of the width in from either edge - int fromEdge = width / 4; - final int tickLength = 8; - //Upper left - tick1 = new Line2D.Float(x + fromEdge, y, x + fromEdge, y + tickLength); - //Upper right - tick2 = new Line2D.Float(x + width - fromEdge, y, x + width - fromEdge, y + tickLength); - - tick3X = r2InPoints - fromEdge; - tick4X = r1InPoints + fromEdge; - - setSize(gp.getBounds().width, gp.getBounds().height + tabOffset); - } - - /** - * Draw alignment marks on an angle. - * - * @param g2 the graphics context - * @param x the center of the circle's x coordinate - * @param y the center of the circle's y - * @param line the line to draw - * @param theta the angle - */ - private void drawAlignmentMarks(Graphics2D g2, int x, int y, Line2D.Float line, float theta) { - g2.translate(x, y); - g2.rotate(Math.toRadians(-theta)); - g2.draw(line); - g2.rotate(Math.toRadians(theta)); - g2.translate(-x, -y); - } - - /** - * Draw a transition. - * - * @param g2 the graphics context - */ - @Override - protected void draw(Graphics2D g2) { - //Render it. - g2.draw(gp); - g2.draw(tick1); - g2.draw(tick2); - drawAlignmentMarks(g2, circleCenterX, - circleCenterY, - new Line2D.Float(-tick3X, 0, -tick3X, -8), - theta); - drawAlignmentMarks(g2, circleCenterX, - circleCenterY, - new Line2D.Float(-tick4X, 0, -tick4X, -8), - theta); - - g2.setStroke(dashed); - g2.draw(glueTab1); - } - -} diff --git a/src/net/sf/openrocket/gui/print/TemplateProperties.java b/src/net/sf/openrocket/gui/print/TemplateProperties.java deleted file mode 100644 index 6f88e576..00000000 --- a/src/net/sf/openrocket/gui/print/TemplateProperties.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * TemplateProperties.java - */ -package net.sf.openrocket.gui.print; - -import java.awt.Color; - -import javax.swing.UIManager; - -/** - * This class is responsible for managing various properties of print templates (fin, nose cone, transitions, etc.). - * - * TODO: HIGH: Remove this entire class, and instead pass the PrintSettings object to the print methods. - */ -public class TemplateProperties { - - /** - * The property that defines the fill color. - */ - public static final String TEMPLATE_FILL_COLOR_PROPERTY = "template.fill.color"; - - /** - * The property that defines the line color. - */ - public static final String TEMPLATE_LINE_COLOR_PROPERTY = "template.line.color"; - - /** - * Get the current fill color. - * - * @return a color to be used as the fill in template shapes - */ - public static Color getFillColor() { - Color fillColor = UIManager.getColor(TemplateProperties.TEMPLATE_FILL_COLOR_PROPERTY); - if (fillColor == null) { - fillColor = Color.lightGray; - } - return fillColor; - } - - - /** - * Set the template fill color. - */ - public static void setFillColor(Color c) { - UIManager.put(TemplateProperties.TEMPLATE_FILL_COLOR_PROPERTY, c); - } - - - /** - * Get the current line color. - * - * @return a color to be used as the line in template shapes - */ - public static Color getLineColor() { - Color lineColor = UIManager.getColor(TemplateProperties.TEMPLATE_LINE_COLOR_PROPERTY); - if (lineColor == null) { - lineColor = Color.darkGray; - } - return lineColor; - } - - /** - * Set the template line color. - */ - public static void setLineColor(Color c) { - UIManager.put(TemplateProperties.TEMPLATE_LINE_COLOR_PROPERTY, c); - } - - /** - * Set the template colors from the print settings. - */ - public static void setColors(PrintSettings settings) { - setFillColor(settings.getTemplateFillColor()); - setLineColor(settings.getTemplateBorderColor()); - } -} diff --git a/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java b/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java deleted file mode 100644 index 7fbab0a6..00000000 --- a/src/net/sf/openrocket/gui/print/components/CheckBoxNode.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * CheckBoxNode.java - */ -package net.sf.openrocket.gui.print.components; - -/** - * A class that acts as the textual node of the check box within the JTree. - */ -public class CheckBoxNode { - - /** - * The text label of the check box. - */ - String text; - - /** - * State flag indicating if the check box has been selected. - */ - boolean selected; - - /** - * Constructor. - * - * @param theText the check box label - * @param isSelected true if selected - */ - public CheckBoxNode (String theText, boolean isSelected) { - text = theText; - selected = isSelected; - } - - /** - * Get the current state of the check box. - * - * @return true if selected - */ - public boolean isSelected () { - return selected; - } - - /** - * Set the current state of the check box. Note: this just tracks the state - it - * does NOT actually set the state of the check box. - * - * @param isSelected true if selected - */ - public void setSelected (boolean isSelected) { - selected = isSelected; - } - - /** - * Get the text of the label. - * - * @return the text of the label - */ - public String getText () { - return text; - } - - /** - * Set the text of the label of the check box. - * - * @param theText the text of the label - */ - public void setText (String theText) { - text = theText; - } - - /** - * If someone prints this object, the text label will be displayed. - * - * @return the text label - */ - public String toString () { - return text; - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java b/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java deleted file mode 100644 index e4a93373..00000000 --- a/src/net/sf/openrocket/gui/print/components/CheckTreeCellRenderer.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * CheckTreeCellRenderer.java - */ -package net.sf.openrocket.gui.print.components; - -import javax.swing.JCheckBox; -import javax.swing.JPanel; -import javax.swing.JTree; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeCellRenderer; -import javax.swing.tree.TreeCellRenderer; -import javax.swing.tree.TreePath; -import java.awt.BorderLayout; -import java.awt.Component; - -/** - * A cell renderer for JCheckBoxes within nodes of a JTree. - * <p/> - * Based in part on a blog by Santhosh Kumar. http://www.jroller.com/santhosh/date/20050610 - */ -public class CheckTreeCellRenderer extends JPanel implements TreeCellRenderer { - - /** - * The selection model. - */ - private CheckTreeSelectionModel selectionModel; - /** - * The delegated cell renderer. - */ - private DefaultTreeCellRenderer delegate; - /** - * The check box within this cell. - */ - private JCheckBox checkBox = new JCheckBox(); - - /** - * Constructor. - * - * @param theDelegate the delegated cell renderer - * @param theSelectionModel the selection model - */ - public CheckTreeCellRenderer (DefaultTreeCellRenderer theDelegate, CheckTreeSelectionModel theSelectionModel) { - delegate = theDelegate; - - delegate.setLeafIcon(null); - delegate.setClosedIcon(null); - delegate.setOpenIcon(null); - - - selectionModel = theSelectionModel; - setLayout(new BorderLayout()); - setOpaque(false); - checkBox.setOpaque(false); - checkBox.setSelected(true); - } - - /** - * @{inheritDoc} - */ - @Override - public Component getTreeCellRendererComponent (JTree tree, Object value, boolean selected, boolean expanded, - boolean leaf, int row, boolean hasFocus) { - Component renderer = delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, - hasFocus); - - TreePath path = tree.getPathForRow(row); - if (path != null) { - final boolean b = selectionModel.isPathSelected(path, true); - checkBox.setSelected(b); - if (value instanceof DefaultMutableTreeNode) { - Object obj = ((DefaultMutableTreeNode) value).getUserObject(); - if (obj instanceof CheckBoxNode) { - ((CheckBoxNode) obj).setSelected(b); - } - } - } - - removeAll(); - add(checkBox, BorderLayout.WEST); - add(renderer, BorderLayout.CENTER); - return this; - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java b/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java deleted file mode 100644 index 719e50d7..00000000 --- a/src/net/sf/openrocket/gui/print/components/CheckTreeManager.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * CheckTreeManager.java - */ -package net.sf.openrocket.gui.print.components; - -import javax.swing.JCheckBox; -import javax.swing.JTree; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.DefaultTreeCellRenderer; -import javax.swing.tree.TreePath; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - -/** - * This class manages mouse clicks within the JTree, handling selection/deselections within the JCheckBox of each cell in the tree. - */ -public class CheckTreeManager extends MouseAdapter implements TreeSelectionListener { - - /** The selection model. */ - private CheckTreeSelectionModel selectionModel; - /** The actual JTree instance. */ - private JTree tree; - /** The number of pixels of width of the check box. Clicking anywhere within the box will trigger actions. */ - int hotspot = new JCheckBox().getPreferredSize().width; - - /** - * Construct a check box tree manager. - * - * @param theTree the actual tree being managed - */ - public CheckTreeManager (RocketPrintTree theTree) { - tree = theTree; - selectionModel = new CheckTreeSelectionModel(tree.getModel()); - theTree.setCheckBoxSelectionModel(selectionModel); - tree.setCellRenderer(new CheckTreeCellRenderer((DefaultTreeCellRenderer)tree.getCellRenderer(), selectionModel)); - tree.addMouseListener(this); - selectionModel.addTreeSelectionListener(this); - - for (int x = 0; x < tree.getRowCount(); x++) { - tree.getSelectionModel().setSelectionPath(tree.getPathForRow(x)); - } - } - - public void addTreeSelectionListener (TreeSelectionListener tsl) { - selectionModel.addTreeSelectionListener(tsl); - } - - /** - * Called when the mouse clicks within the tree. - * - * @param me the event that triggered this - */ - @Override - public void mouseClicked (MouseEvent me) { - TreePath path = tree.getPathForLocation(me.getX(), me.getY()); - if (path == null) { - return; - } - if (me.getX() > tree.getPathBounds(path).x + hotspot) { - return; - } - - boolean selected = selectionModel.isPathSelected(path, true); - selectionModel.removeTreeSelectionListener(this); - - try { - if (selected) { - selectionModel.removeSelectionPath(path); - } - else { - selectionModel.addSelectionPath(path); - } - } - finally { - selectionModel.addTreeSelectionListener(this); - tree.treeDidChange(); - } - } - - /** - * Get the selection model being used by this manager. - * - * @return the selection model - */ - public CheckTreeSelectionModel getSelectionModel () { - return selectionModel; - } - - /** - * Notify the tree that it changed. - * - * @param e unused - */ - @Override - public void valueChanged (TreeSelectionEvent e) { - tree.treeDidChange(); - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java b/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java deleted file mode 100644 index 9f34b4b5..00000000 --- a/src/net/sf/openrocket/gui/print/components/CheckTreeSelectionModel.java +++ /dev/null @@ -1,245 +0,0 @@ -/* - * CheckTreeSelectionModel.java - */ -package net.sf.openrocket.gui.print.components; - -import javax.swing.tree.DefaultTreeSelectionModel; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; -import java.util.ArrayList; -import java.util.Stack; - -/** - * This class implements the selection model for the checkbox tree. This specifically is used to keep - * track of the TreePaths that have a selected CheckBox. - */ -public class CheckTreeSelectionModel extends DefaultTreeSelectionModel { - - /** - * The tree model. - */ - private TreeModel model; - - /** - * Constructor. - * - * @param theModel the model in use for the tree - */ - public CheckTreeSelectionModel (TreeModel theModel) { - model = theModel; - setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); - } - - /** - * Tests whether there is any selected node in the subtree of given path. - * - * @param path the path to walk - * - * @return true if any item in the path or its descendants are selected - */ - public boolean isPartiallySelected (TreePath path) { - if (isPathSelected(path, true)) { - return false; - } - TreePath[] selectionPaths = getSelectionPaths(); - if (selectionPaths == null) { - return false; - } - for (TreePath selectionPath : selectionPaths) { - if (isDescendant(selectionPath, path)) { - return true; - } - } - return false; - } - - /** - * Tells whether given path is selected. If dig is true, then a path is assumed to be selected, if one of its - * ancestor is selected. - * - * @param path the path to interrogate - * @param dig if true then check if an ancestor is selected - * - * @return true if the path is selected - */ - public boolean isPathSelected (TreePath path, boolean dig) { - if (!dig) { - return super.isPathSelected(path); - } - while (path != null && !super.isPathSelected(path)) { - path = path.getParentPath(); - } - return path != null; - } - - /** - * Determines if path1 is a descendant of path2. - * - * @param path1 descendant? - * @param path2 ancestor? - * - * @return true if path1 is a descendant of path2 - */ - private boolean isDescendant (TreePath path1, TreePath path2) { - Object obj1[] = path1.getPath(); - Object obj2[] = path2.getPath(); - for (int i = 0; i < obj2.length; i++) { - if (i < obj1.length) { - if (obj1[i] != obj2[i]) { - return false; - } - } - else { - return false; - } - } - return true; - } - - - /** - * Unsupported exception. - * - * @param pPaths an array of paths - */ - public void setSelectionPaths (TreePath[] pPaths) { - TreePath selected[] = getSelectionPaths(); - for (TreePath aSelected : selected) { - removeSelectionPath(aSelected); - } - for (TreePath pPath : pPaths) { - addSelectionPath(pPath); - } - } - - /** - * Add a set of TreePath nodes to the selection model. - * - * @param paths an array of tree path nodes - */ - public void addSelectionPaths (TreePath[] paths) { - // deselect all descendants of paths[] - for (TreePath path : paths) { - TreePath[] selectionPaths = getSelectionPaths(); - if (selectionPaths == null) { - break; - } - ArrayList<TreePath> toBeRemoved = new ArrayList<TreePath>(); - for (TreePath selectionPath : selectionPaths) { - if (isDescendant(selectionPath, path)) { - toBeRemoved.add(selectionPath); - } - } - super.removeSelectionPaths(toBeRemoved.toArray(new TreePath[toBeRemoved.size()])); - } - - // if all siblings are selected then deselect them and select parent recursively - // otherwise just select that path. - for (TreePath path : paths) { - TreePath temp = null; - while (areSiblingsSelected(path)) { - temp = path; - if (path.getParentPath() == null) { - break; - } - path = path.getParentPath(); - } - if (temp != null) { - if (temp.getParentPath() != null) { - addSelectionPath(temp.getParentPath()); - } - else { - if (!isSelectionEmpty()) { - removeSelectionPaths(getSelectionPaths()); - } - super.addSelectionPaths(new TreePath[]{temp}); - } - } - else { - super.addSelectionPaths(new TreePath[]{path}); - } - } - } - - /** - * Tells whether all siblings of given path are selected. - * - * @param path the tree path node - * - * @return true if all sibling nodes are selected - */ - private boolean areSiblingsSelected (TreePath path) { - TreePath parent = path.getParentPath(); - if (parent == null) { - return true; - } - Object node = path.getLastPathComponent(); - Object parentNode = parent.getLastPathComponent(); - - int childCount = model.getChildCount(parentNode); - for (int i = 0; i < childCount; i++) { - Object childNode = model.getChild(parentNode, i); - if (childNode == node) { - continue; - } - if (!isPathSelected(parent.pathByAddingChild(childNode))) { - return false; - } - } - return true; - } - - /** - * Remove paths from the selection model. - * - * @param paths the array of path nodes - */ - public void removeSelectionPaths (TreePath[] paths) { - for (TreePath path : paths) { - if (path.getPathCount() == 1) { - super.removeSelectionPaths(new TreePath[]{path}); - } - else { - toggleRemoveSelection(path); - } - } - } - - /** - * If any ancestor node of given path is selected then deselect it and selection all its descendants except given - * path and descendants. otherwise just deselect the given path. - * - * @param path the tree path node - */ - private void toggleRemoveSelection (TreePath path) { - Stack<TreePath> stack = new Stack<TreePath>(); - TreePath parent = path.getParentPath(); - while (parent != null && !isPathSelected(parent)) { - stack.push(parent); - parent = parent.getParentPath(); - } - if (parent != null) { - stack.push(parent); - } - else { - super.removeSelectionPaths(new TreePath[]{path}); - return; - } - - while (!stack.isEmpty()) { - TreePath temp = stack.pop(); - TreePath peekPath = stack.isEmpty() ? path : stack.peek(); - Object node = temp.getLastPathComponent(); - Object peekNode = peekPath.getLastPathComponent(); - int childCount = model.getChildCount(node); - for (int i = 0; i < childCount; i++) { - Object childNode = model.getChild(node, i); - if (childNode != peekNode) { - super.addSelectionPaths(new TreePath[]{temp.pathByAddingChild(childNode)}); - } - } - } - super.removeSelectionPaths(new TreePath[]{parent}); - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java b/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java deleted file mode 100644 index 5296ea47..00000000 --- a/src/net/sf/openrocket/gui/print/components/RocketPrintTree.java +++ /dev/null @@ -1,262 +0,0 @@ -/* - * RocketPrintTree.java - */ -package net.sf.openrocket.gui.print.components; - -import net.sf.openrocket.gui.print.OpenRocketPrintable; -import net.sf.openrocket.gui.print.PrintableContext; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Stage; - -import javax.swing.*; -import javax.swing.event.TreeExpansionEvent; -import javax.swing.event.TreeWillExpandListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.ExpandVetoException; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Vector; - -/** - * A specialized JTree for displaying various rocket items that can be printed. - */ -public class RocketPrintTree extends JTree { - - /** - * All check boxes are initially set to true (selected). - */ - public static final boolean INITIAL_CHECKBOX_SELECTED = true; - - /** - * The selection model that tracks the check box state. - */ - private TreeSelectionModel theCheckBoxSelectionModel; - - /** - * Constructor. - * - * @param root the vector of check box nodes (rows) to place into the tree - */ - private RocketPrintTree(Vector root) { - super(root); - - //Remove the little down and sideways arrows. These are not needed because the tree expansion is fixed. - ((javax.swing.plaf.basic.BasicTreeUI) this.getUI()). - setExpandedIcon(null); - ((javax.swing.plaf.basic.BasicTreeUI) this.getUI()). - setCollapsedIcon(null); - } - - /** - * Factory method to create a specialized JTree. This version is for rocket's that have more than one stage. - * - * @param rocketName the name of the rocket - * @param stages the array of all stages - * - * @return an instance of JTree - */ - public static RocketPrintTree create(String rocketName, List<RocketComponent> stages) { - Vector root = new Vector(); - Vector toAddTo = root; - - if (stages != null) { - if (stages.size() > 1) { - final Vector parent = new NamedVector(rocketName != null ? rocketName : "Rocket"); - - root.add(parent); - toAddTo = parent; - } - for (RocketComponent stage : stages) { - if (stage instanceof Stage) { - toAddTo.add(createNamedVector(stage.getName(), createPrintTreeNode(true), stage.getStageNumber())); - } - } - } - - List<OpenRocketPrintable> unstaged = OpenRocketPrintable.getUnstaged(); - for (int i = 0; i < unstaged.size(); i++) { - toAddTo.add(new CheckBoxNode(unstaged.get(i).getDescription(), - INITIAL_CHECKBOX_SELECTED)); - } - - RocketPrintTree tree = new RocketPrintTree(root); - - tree.addTreeWillExpandListener - (new TreeWillExpandListener() { - @Override - public void treeWillExpand(TreeExpansionEvent e) { - } - - @Override - public void treeWillCollapse(TreeExpansionEvent e) - throws ExpandVetoException { - throw new ExpandVetoException(e, "you can't collapse this JTree"); - } - }); - - return tree; - } - - /** - * Factory method to create a specialized JTree. This version is for a rocket with only one stage. - * - * @param rocketName the name of the rocket - * - * @return an instance of JTree - */ - public static RocketPrintTree create(String rocketName) { - Vector root = new Vector(); - root.add(new NamedVector(rocketName != null ? rocketName : "Rocket", createPrintTreeNode(false))); - - RocketPrintTree tree = new RocketPrintTree(root); - - tree.addTreeWillExpandListener - (new TreeWillExpandListener() { - @Override - public void treeWillExpand(TreeExpansionEvent e) { - } - - @Override - public void treeWillCollapse(TreeExpansionEvent e) - throws ExpandVetoException { - throw new ExpandVetoException(e, "you can't collapse this JTree"); - } - }); - - return tree; - } - - /** - * This tree needs to have access both to the normal selection model (for the textual row) which is managed by the - * superclass, as well as the selection model for the check boxes. This mutator method allows an external class to - * set the model back onto this class. Because of some unfortunate circular dependencies this cannot be set at - * construction. - * <p/> - * TODO: Ensure these circular references get cleaned up properly at dialog disposal so everything can be GC'd. - * - * @param checkBoxSelectionModel the selection model used to keep track of the check box state - */ - public void setCheckBoxSelectionModel(TreeSelectionModel checkBoxSelectionModel) { - theCheckBoxSelectionModel = checkBoxSelectionModel; - } - - /** - * Add a selection path to the internal check box selection model. The normal JTree selection model is unaffected. - * This has the effect of "selecting" the check box, but not highlighting the row. - * - * @param path the path (row) - */ - @Override - public void addSelectionPath(TreePath path) { - theCheckBoxSelectionModel.addSelectionPath(path); - } - - /** - * Helper to construct a named vector. - * - * @param name the name of the vector - * @param nodes the array of nodes to put into the vector - * @param stage the stage number - * - * @return a NamedVector suitable for adding to a JTree - */ - private static Vector createNamedVector(String name, CheckBoxNode[] nodes, int stage) { - return new NamedVector(name, nodes, stage); - } - - /** - * Helper to construct the set of check box rows for each stage. - * - * @param onlyStageSpecific if true then only stage specific OpenRocketPrintable rows are represented (in this part - * of the tree). - * - * @return an array of CheckBoxNode - */ - private static CheckBoxNode[] createPrintTreeNode(boolean onlyStageSpecific) { - List<CheckBoxNode> nodes = new ArrayList<CheckBoxNode>(); - OpenRocketPrintable[] printables = OpenRocketPrintable.values(); - for (OpenRocketPrintable openRocketPrintable : printables) { - if (!onlyStageSpecific || openRocketPrintable.isStageSpecific()) { - nodes.add(new CheckBoxNode(openRocketPrintable.getDescription(), - INITIAL_CHECKBOX_SELECTED)); - } - } - return nodes.toArray(new CheckBoxNode[nodes.size()]); - } - - /** - * Get the set of items to be printed, as selected by the user. - * - * @return the things to be printed, returned as an Iterator<PrintableContext> - */ - public Iterator<PrintableContext> getToBePrinted() { - final DefaultMutableTreeNode mutableTreeNode = (DefaultMutableTreeNode) getModel().getRoot(); - PrintableContext pc = new PrintableContext(); - add(pc, mutableTreeNode); - return pc.iterator(); - } - - /** - * Walk a tree, finding everything that has been selected and aggregating it into something that can be iterated upon - * This method is recursive. - * - * @param pc the printable context that aggregates the choices into an iterator - * @param theMutableTreeNode the root node - */ - private void add(final PrintableContext pc, final DefaultMutableTreeNode theMutableTreeNode) { - int children = theMutableTreeNode.getChildCount(); - for (int x = 0; x < children; x++) { - - final DefaultMutableTreeNode at = (DefaultMutableTreeNode) theMutableTreeNode.getChildAt(x); - if (at.getUserObject() instanceof CheckBoxNode) { - CheckBoxNode cbn = (CheckBoxNode) at.getUserObject(); - if (cbn.isSelected()) { - final OpenRocketPrintable printable = OpenRocketPrintable.findByDescription(cbn.getText()); - pc.add( - printable.isStageSpecific() ? ((NamedVector) theMutableTreeNode.getUserObject()) - .getStage() : null, - printable); - } - } - add(pc, at); - } - } - -} - -/** - * JTree's work off of Vector's (unfortunately). This class is tailored for use with check boxes in the JTree. - */ -class NamedVector extends Vector<CheckBoxNode> { - String name; - - int stageNumber; - - public NamedVector(String theName) { - name = theName; - } - - public NamedVector(String theName, CheckBoxNode elements[], int stage) { - this(theName, elements); - stageNumber = stage; - } - - public NamedVector(String theName, CheckBoxNode elements[]) { - name = theName; - for (int i = 0, n = elements.length; i < n; i++) { - add(elements[i]); - } - } - - @Override - public String toString() { - return name; - } - - public int getStage() { - return stageNumber; - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/visitor/FinMarkingGuideStrategy.java b/src/net/sf/openrocket/gui/print/visitor/FinMarkingGuideStrategy.java deleted file mode 100644 index f4e5a526..00000000 --- a/src/net/sf/openrocket/gui/print/visitor/FinMarkingGuideStrategy.java +++ /dev/null @@ -1,167 +0,0 @@ -package net.sf.openrocket.gui.print.visitor; - -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.pdf.PdfContentByte; -import com.itextpdf.text.pdf.PdfWriter; -import net.sf.openrocket.gui.print.FinMarkingGuide; -import net.sf.openrocket.gui.print.ITextHelper; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.startup.Application; - -import java.awt.*; -import java.awt.image.BufferedImage; - -/** - * A strategy for drawing a fin marking guide. As currently implemented, each body tube with a finset will have - * a marking guide. If a tube has multiple fin sets, they are combined onto one marking guide. Launch lugs are supported - * as well. - */ -public class FinMarkingGuideStrategy { - - /** - * The logger. - */ - private static final LogHelper log = Application.getLogger(); - - /** - * The iText document. - */ - protected Document document; - - /** - * The direct iText writer. - */ - protected PdfWriter writer; - - /** - * Constructor. - * - * @param doc The iText document - * @param theWriter The direct iText writer - */ - public FinMarkingGuideStrategy(Document doc, PdfWriter theWriter) { - document = doc; - writer = theWriter; - } - - /** - * Recurse through the given rocket component. - * - * @param root the root component; all children will be visited recursively - */ - public void writeToDocument(final Rocket root) { - render(root); - } - - - /** - * The core behavior of this strategy. - * - * @param rocket the rocket to render all - */ - private void render(final Rocket rocket) { - try { - FinMarkingGuide pfs = new FinMarkingGuide(rocket); - - java.awt.Dimension size = pfs.getSize(); - final Dimension pageSize = getPageSize(); - if (fitsOnOnePage(pageSize, size.getWidth(), size.getHeight())) { - printOnOnePage(pfs); - } else { - BufferedImage image = (BufferedImage) pfs.createImage(); - ITextHelper.renderImageAcrossPages(new Rectangle(pageSize.getWidth(), pageSize.getHeight()), - document, writer, image); - } - } catch (DocumentException e) { - log.error("Could not render the fin marking guide.", e); - } - } - - /** - * Determine if the image will fit on the given page. - * - * @param pageSize the page size - * @param wImage the width of the thing to be printed - * @param hImage the height of the thing to be printed - * @return true if the thing to be printed will fit on a single page - */ - private boolean fitsOnOnePage(Dimension pageSize, double wImage, double hImage) { - double wPage = pageSize.getWidth(); - double hPage = pageSize.getHeight(); - - int wRatio = (int) Math.ceil(wImage / wPage); - int hRatio = (int) Math.ceil(hImage / hPage); - - return wRatio <= 1.0d && hRatio <= 1.0d; - } - - /** - * Print the transition. - * - * @param theMarkingGuide the fin marking guide - */ - private void printOnOnePage(final FinMarkingGuide theMarkingGuide) { - Dimension d = getPageSize(); - PdfContentByte cb = writer.getDirectContent(); - Graphics2D g2 = cb.createGraphics(d.width, d.height); - theMarkingGuide.print(g2); - g2.dispose(); - document.newPage(); - } - - /** - * Get the dimensions of the paper page. - * - * @return an internal Dimension - */ - protected Dimension getPageSize() { - return new Dimension(document.getPageSize().getWidth(), - document.getPageSize().getHeight()); - } - - /** - * Convenience class to model a dimension. - */ - class Dimension { - /** - * Width, in points. - */ - public float width; - /** - * Height, in points. - */ - public float height; - - /** - * Constructor. - * - * @param w width - * @param h height - */ - public Dimension(float w, float h) { - width = w; - height = h; - } - - /** - * Get the width. - * - * @return the width - */ - public float getWidth() { - return width; - } - - /** - * Get the height. - * - * @return the height - */ - public float getHeight() { - return height; - } - } -} diff --git a/src/net/sf/openrocket/gui/print/visitor/FinSetPrintStrategy.java b/src/net/sf/openrocket/gui/print/visitor/FinSetPrintStrategy.java deleted file mode 100644 index 80e15a21..00000000 --- a/src/net/sf/openrocket/gui/print/visitor/FinSetPrintStrategy.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * FinSetPrintStrategy.java - */ -package net.sf.openrocket.gui.print.visitor; - -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.pdf.PdfContentByte; -import com.itextpdf.text.pdf.PdfWriter; -import net.sf.openrocket.gui.print.ITextHelper; -import net.sf.openrocket.gui.print.PrintableFinSet; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.List; -import java.util.Set; - -/** - * A strategy for drawing fin templates. - */ -public class FinSetPrintStrategy { - - /** - * The logger. - */ - private static final LogHelper log = Application.getLogger(); - - /** - * The iText document. - */ - protected Document document; - - /** - * The direct iText writer. - */ - protected PdfWriter writer; - - /** - * The stages selected. - */ - protected Set<Integer> stages; - - /** - * Constructor. - * - * @param doc The iText document - * @param theWriter The direct iText writer - * @param theStages The stages to be printed by this strategy - */ - public FinSetPrintStrategy(Document doc, PdfWriter theWriter, Set<Integer> theStages) { - document = doc; - writer = theWriter; - stages = theStages; - } - - /** - * Recurse through the given rocket component. - * - * @param root the root component; all children will be printed recursively - */ - public void writeToDocument (final RocketComponent root) { - List<RocketComponent> rc = root.getChildren(); - goDeep(rc); - } - - - /** - * Recurse through the given rocket component. - * - * @param theRc an array of rocket components; all children will be printed recursively - */ - protected void goDeep (final List<RocketComponent> theRc) { - for (RocketComponent rocketComponent : theRc) { - if (rocketComponent instanceof FinSet) { - printFinSet((FinSet) rocketComponent); - } - else if (rocketComponent.getChildCount() > 0) { - goDeep(rocketComponent.getChildren()); - } - } - } - - /** - * The core behavior of this strategy. - * - * @param finSet the object to extract info about; a graphical image of the fin shape is drawn to the document - */ - private void printFinSet(final FinSet finSet) { - if (shouldPrintStage(finSet.getStageNumber())) { - try { - PrintableFinSet pfs = new PrintableFinSet(finSet); - - java.awt.Dimension finSize = pfs.getSize(); - final Dimension pageSize = getPageSize(); - if (fitsOnOnePage(pageSize, finSize.getWidth(), finSize.getHeight())) { - printOnOnePage(pfs); - } - else { - BufferedImage image = (BufferedImage) pfs.createImage(); - ITextHelper.renderImageAcrossPages(new Rectangle(pageSize.getWidth(), pageSize.getHeight()), - document, writer, image); - } - document.newPage(); - } - catch (DocumentException e) { - log.error("Could not render fin.", e); - } - } - } - - /** - * Determine if the strategy's set of stage numbers (to print) contains the specified stage. - * - * @param stageNumber a stage number - * - * @return true if the strategy contains the stage number provided - */ - public boolean shouldPrintStage(int stageNumber) { - if (stages == null || stages.isEmpty()) { - return false; - } - - for (final Integer stage : stages) { - if (stage == stageNumber) { - return true; - } - } - - return false; - } - - /** - * Determine if the image will fit on the given page. - * - * @param pageSize the page size - * @param wImage the width of the thing to be printed - * @param hImage the height of the thing to be printed - * - * @return true if the thing to be printed will fit on a single page - */ - private boolean fitsOnOnePage (Dimension pageSize, double wImage, double hImage) { - double wPage = pageSize.getWidth(); - double hPage = pageSize.getHeight(); - - int wRatio = (int) Math.ceil(wImage / wPage); - int hRatio = (int) Math.ceil(hImage / hPage); - - return wRatio <= 1.0d && hRatio <= 1.0d; - } - - /** - * Print the fin set. - * - * @param thePfs the printable fin set - */ - private void printOnOnePage (final PrintableFinSet thePfs) { - Dimension d = getPageSize(); - PdfContentByte cb = writer.getDirectContent(); - Graphics2D g2 = cb.createGraphics(d.width, d.height); - thePfs.print(g2); - g2.dispose(); - } - - /** - * Get the dimensions of the paper page. - * - * @return an internal Dimension - */ - protected Dimension getPageSize () { - return new Dimension(document.getPageSize().getWidth(), - document.getPageSize().getHeight()); - } - - /** - * Convenience class to model a dimension. - */ - class Dimension { - /** Width, in points. */ - public float width; - /** Height, in points. */ - public float height; - - /** - * Constructor. - * @param w width - * @param h height - */ - public Dimension (float w, float h) { - width = w; - height = h; - } - - /** - * Get the width. - * - * @return the width - */ - public float getWidth () { - return width; - } - - /** - * Get the height. - * - * @return the height - */ - public float getHeight () { - return height; - } - } -} diff --git a/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java deleted file mode 100644 index cdeb9247..00000000 --- a/src/net/sf/openrocket/gui/print/visitor/PartsDetailVisitorStrategy.java +++ /dev/null @@ -1,664 +0,0 @@ -/* - * PartsDetailVisitorStrategy.java - */ -package net.sf.openrocket.gui.print.visitor; - -import com.itextpdf.text.*; -import com.itextpdf.text.pdf.PdfPCell; -import com.itextpdf.text.pdf.PdfPTable; -import com.itextpdf.text.pdf.PdfWriter; -import net.sf.openrocket.gui.main.ComponentIcons; -import net.sf.openrocket.gui.print.ITextHelper; -import net.sf.openrocket.gui.print.PrintUtilities; -import net.sf.openrocket.gui.print.PrintableFinSet; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.*; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; - -import javax.swing.*; -import java.text.NumberFormat; -import java.util.Collection; -import java.util.List; -import java.util.Set; - -/** - * A visitor strategy for creating documentation about parts details. - */ -public class PartsDetailVisitorStrategy { - - /** - * The logger. - */ - private static final LogHelper log = Application.getLogger(); - - /** - * The number of columns in the table. - */ - private static final int TABLE_COLUMNS = 7; - - /** - * The parts detail is represented as an iText table. - */ - PdfPTable grid; - - /** - * The iText document. - */ - protected Document document; - - /** - * The direct iText writer. - */ - protected PdfWriter writer; - - /** - * The stages selected. - */ - protected Set<Integer> stages; - - /** - * State variable to track the level of hierarchy. - */ - protected int level = 0; - - private static final String LINES = "Lines: "; - private static final String MASS = "Mass: "; - private static final String LEN = "Len: "; - private static final String THICK = "Thick: "; - private static final String INNER = "in "; - private static final String DIAMETER = "Dia"; - private static final String OUTER = "out"; - private static final String WIDTH = "Width"; - private static final String LENGTH = "Length"; - private static final String SHROUD_LINES = "Shroud Lines"; - private static final String AFT_DIAMETER = "Aft Dia: "; - private static final String FORE_DIAMETER = "Fore Dia: "; - private static final String PARTS_DETAIL = "Parts Detail"; - - /** - * Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts. - * - * @param doc The iText document - * @param theWriter The direct iText writer - * @param theStagesToVisit The stages to be visited by this strategy - */ - public PartsDetailVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) { - document = doc; - writer = theWriter; - stages = theStagesToVisit; - PrintUtilities.addText(doc, PrintUtilities.BIG_BOLD, PARTS_DETAIL); - } - - /** - * Print the parts detail. - * - * @param root the root component - */ - public void writeToDocument (final RocketComponent root) { - goDeep(root.getChildren()); - } - - /** - * Recurse through the given rocket component. - * - * @param theRc an array of rocket components; all children will be visited recursively - */ - protected void goDeep (final List<RocketComponent> theRc) { - level++; - for (RocketComponent rocketComponent : theRc) { - handle(rocketComponent); - } - level--; - } - - /** - * Add a line to the detail report based upon the type of the component. - * - * @param component the component to print the detail for - */ - private void handle (RocketComponent component) { - //This ugly if-then-else construct is not object oriented. Originally it was an elegant, and very OO savy, design - //using the Visitor pattern. Unfortunately, it was misunderstood and was removed. - if (component instanceof Stage) { - try { - if (grid != null) { - document.add(grid); - } - document.add(ITextHelper.createPhrase(component.getName())); - grid = new PdfPTable(TABLE_COLUMNS); - grid.setWidthPercentage(100); - grid.setHorizontalAlignment(Element.ALIGN_LEFT); - } - catch (DocumentException e) { - } - - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof LaunchLug) { - LaunchLug ll = (LaunchLug) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - - grid.addCell(createMaterialCell(ll.getMaterial())); - grid.addCell(createOuterInnerDiaCell(ll)); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - } - else if (component instanceof NoseCone) { - NoseCone nc = (NoseCone) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(nc.getMaterial())); - grid.addCell(ITextHelper.createCell(nc.getType().getName(), PdfPCell.BOTTOM)); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof Transition) { - Transition tran = (Transition) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(tran.getMaterial())); - - Chunk fore = new Chunk(FORE_DIAMETER + toLength(tran.getForeRadius() * 2)); - fore.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - Chunk aft = new Chunk(AFT_DIAMETER + toLength(tran.getAftRadius() * 2)); - aft.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - final PdfPCell cell = ITextHelper.createCell(); - cell.addElement(fore); - cell.addElement(aft); - grid.addCell(cell); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof BodyTube) { - BodyTube bt = (BodyTube) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(bt.getMaterial())); - grid.addCell(createOuterInnerDiaCell(bt)); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof FinSet) { - handleFins((FinSet) component); - } - else if (component instanceof BodyComponent) { - grid.addCell(component.getName()); - grid.completeRow(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof ExternalComponent) { - ExternalComponent ext = (ExternalComponent) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - - grid.addCell(createMaterialCell(ext.getMaterial())); - grid.addCell(ITextHelper.createCell()); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof InnerTube) { - InnerTube it = (InnerTube) component; - grid.addCell(iconToImage(component)); - final PdfPCell pCell = createNameCell(component.getName(), true); - grid.addCell(pCell); - grid.addCell(createMaterialCell(it.getMaterial())); - grid.addCell(createOuterInnerDiaCell(it)); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof RadiusRingComponent) { - RadiusRingComponent rrc = (RadiusRingComponent) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(rrc.getMaterial())); - if (component instanceof Bulkhead) { - grid.addCell(createDiaCell(rrc.getOuterRadius()*2)); - } - else { - grid.addCell(createOuterInnerDiaCell(rrc)); - } - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof RingComponent) { - RingComponent ring = (RingComponent) component; - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(ring.getMaterial())); - grid.addCell(createOuterInnerDiaCell(ring)); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof ShockCord) { - ShockCord ring = (ShockCord) component; - PdfPCell cell = ITextHelper.createCell(); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setPaddingBottom(12f); - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(ring.getMaterial())); - grid.addCell(cell); - grid.addCell(createLengthCell(ring.getCordLength())); - grid.addCell(createMassCell(component.getMass())); - } - else if (component instanceof Parachute) { - Parachute chute = (Parachute) component; - PdfPCell cell = ITextHelper.createCell(); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setPaddingBottom(12f); - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(chute.getMaterial())); - grid.addCell(createDiaCell(chute.getDiameter())); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - - grid.addCell(iconToImage(null)); - grid.addCell(createNameCell(SHROUD_LINES, true)); - grid.addCell(createMaterialCell(chute.getLineMaterial())); - grid.addCell(createLinesCell(chute.getLineCount())); - grid.addCell(createLengthCell(chute.getLineLength())); - grid.addCell(cell); - } - else if (component instanceof Streamer) { - Streamer ring = (Streamer) component; - PdfPCell cell = ITextHelper.createCell(); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setPaddingBottom(12f); - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(ring.getMaterial())); - grid.addCell(createStrip(ring)); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - } - else if (component instanceof RecoveryDevice) { - RecoveryDevice device = (RecoveryDevice) component; - PdfPCell cell = ITextHelper.createCell(); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setPaddingBottom(12f); - grid.addCell(iconToImage(component)); - grid.addCell(createNameCell(component.getName(), true)); - grid.addCell(createMaterialCell(device.getMaterial())); - grid.addCell(cell); - grid.addCell(createLengthCell(component.getLength())); - grid.addCell(createMassCell(component.getMass())); - } - else if (component instanceof MassObject) { - PdfPCell cell = ITextHelper.createCell(); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setPaddingBottom(12f); - - grid.addCell(iconToImage(component)); - final PdfPCell nameCell = createNameCell(component.getName(), true); - nameCell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - nameCell.setPaddingBottom(12f); - grid.addCell(nameCell); - grid.addCell(cell); - grid.addCell(createDiaCell(((MassObject) component).getRadius() * 2)); - grid.addCell(cell); - grid.addCell(createMassCell(component.getMass())); - } - } - - /** - * Close the strategy by adding the last grid to the document. - */ - public void close () { - try { - if (grid != null) { - document.add(grid); - } - } - catch (DocumentException e) { - log.error("Could not write last cell to document.", e); - } - } - - /** - * Create a cell to document an outer 'diameter'. This is used for components that have no inner diameter, such as - * a solid parachute or bulkhead. - * - * @param diameter the diameter in default length units - * - * @return a formatted cell containing the diameter - */ - private PdfPCell createDiaCell (final double diameter) { - PdfPCell result = new PdfPCell(); - Phrase p = new Phrase(); - p.setLeading(12f); - result.setVerticalAlignment(Element.ALIGN_TOP); - result.setBorder(Rectangle.BOTTOM); - Chunk c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(DIAMETER); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); - c.append(OUTER); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(" " + toLength(diameter)); - p.add(c); - result.addElement(p); - return result; - } - - /** - * Create a PDF cell for a streamer. - * - * @param component a component that is a Coaxial - * @return the PDF cell that has the streamer documented - */ - private PdfPCell createStrip (final Streamer component) { - PdfPCell result = new PdfPCell(); - Phrase p = new Phrase(); - p.setLeading(12f); - result.setVerticalAlignment(Element.ALIGN_TOP); - result.setBorder(Rectangle.BOTTOM); - Chunk c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(LENGTH); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(" " + toLength(component.getStripLength())); - p.add(c); - result.addElement(p); - - Phrase pw = new Phrase(); - pw.setLeading(14f); - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(WIDTH); - pw.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(" " + toLength(component.getStripWidth())); - pw.add(c); - result.addElement(pw); - - return result; - } - - /** - * Create a PDF cell that documents both an outer and an inner diameter of a component. - * - * @param component a component that is a Coaxial - * @return the PDF cell that has the outer and inner diameters documented - */ - private PdfPCell createOuterInnerDiaCell (final Coaxial component) { - PdfPCell result = new PdfPCell(); - Phrase p = new Phrase(); - p.setLeading(12f); - result.setVerticalAlignment(Element.ALIGN_TOP); - result.setBorder(Rectangle.BOTTOM); - Chunk c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(DIAMETER); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); - c.append(OUTER); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(" " + toLength(component.getOuterRadius() * 2)); - p.add(c); - createInnerDiaCell(component, result); - result.addElement(p); - return result; - } - - /** - * Add inner diameter data to a cell. - * - * @param component a component that is a Coaxial - * @param cell the PDF cell to add the inner diameter data to - */ - private void createInnerDiaCell (final Coaxial component, PdfPCell cell) { - Phrase p = new Phrase(); - p.setLeading(14f); - Chunk c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(DIAMETER); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); - c.append(INNER); - p.add(c); - - c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(" " + toLength(component.getInnerRadius() * 2)); - p.add(c); - cell.addElement(p); - } - - /** - * Add PDF cells for a fin set. - * - * @param theFinSet the fin set - */ - private void handleFins (FinSet theFinSet) { - - Image img = null; - java.awt.Image awtImage = new PrintableFinSet(theFinSet).createImage(); - - Collection<Coordinate> x = theFinSet.getComponentBounds(); - - try { - img = Image.getInstance(writer, awtImage, 0.25f); - } - catch (Exception e) { - log.error("Could not write image to document.", e); - } - - grid.addCell(iconToImage(theFinSet)); - grid.addCell(createNameCell(theFinSet.getName() + " (" + theFinSet.getFinCount() + ")", true)); - grid.addCell(createMaterialCell(theFinSet.getMaterial())); - grid.addCell(ITextHelper.createCell(THICK + toLength(theFinSet.getThickness()), PdfPCell.BOTTOM)); - final PdfPCell pCell = new PdfPCell(); - pCell.setBorder(Rectangle.BOTTOM); - pCell.addElement(img); - - grid.addCell(ITextHelper.createCell()); - grid.addCell(createMassCell(theFinSet.getMass())); - - List<RocketComponent> rc = theFinSet.getChildren(); - goDeep(rc); - } - - /** - * Create a length formatted cell. - * - * @param length the length, in default length units - * - * @return a PdfPCell that is formatted with the length - */ - protected PdfPCell createLengthCell (double length) { - return ITextHelper.createCell(LEN + toLength(length), PdfPCell.BOTTOM); - } - - /** - * Create a mass formatted cell. - * - * @param mass the mass, in default mass units - * - * @return a PdfPCell that is formatted with the mass - */ - protected PdfPCell createMassCell (double mass) { - return ITextHelper.createCell(MASS + toMass(mass), PdfPCell.BOTTOM); - } - - /** - * Create a (shroud) line count formatted cell. - * - * @param count the number of shroud lines - * - * @return a PdfPCell that is formatted with the line count - */ - protected PdfPCell createLinesCell (int count) { - return ITextHelper.createCell(LINES + count, PdfPCell.BOTTOM); - } - - /** - * Create a cell formatted for a name (or any string for that matter). - * - * @param v the string to format into a PDF cell - * @param withIndent if true, then an indention is made scaled to the level of the part in the parent hierarchy - * - * @return a PdfPCell that is formatted with the string <code>v</code> - */ - protected PdfPCell createNameCell (String v, boolean withIndent) { - PdfPCell result = new PdfPCell(); - result.setBorder(Rectangle.BOTTOM); - Chunk c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - if (withIndent) { - for (int x = 0; x < (level - 2) * 10; x++) { - c.append(" "); - } - } - c.append(v); - result.setColspan(2); - result.addElement(c); - return result; - } - - /** - * Create a cell that describes a material. - * - * @param material the material - * - * @return a PdfPCell that is formatted with a description of the material - */ - protected PdfPCell createMaterialCell (Material material) { - PdfPCell cell = ITextHelper.createCell(); - cell.setLeading(13f, 0); - - Chunk c = new Chunk(); - c.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.NORMAL_FONT_SIZE)); - c.append(toMaterialName(material)); - cell.addElement(c); - Chunk density = new Chunk(); - density.setFont(new Font(Font.FontFamily.HELVETICA, PrintUtilities.SMALL_FONT_SIZE)); - density.append(toMaterialDensity(material)); - cell.addElement(density); - return cell; - } - - /** - * Get the icon of the particular type of rocket component and conver it to an image in a PDF cell. - * - * @param visitable the rocket component to create a cell with it's image - * - * @return a PdfPCell that is just an image that can be put into a PDF - */ - protected PdfPCell iconToImage (final RocketComponent visitable) { - if (visitable != null) { - final ImageIcon icon = (ImageIcon) ComponentIcons.getLargeIcon(visitable.getClass()); - try { - if (icon != null) { - Image im = Image.getInstance(icon.getImage(), null); - if (im != null) { - im.scaleToFit(icon.getIconWidth() * 0.6f, icon.getIconHeight() * 0.6f); - PdfPCell cell = new PdfPCell(im); - cell.setFixedHeight(icon.getIconHeight() * 0.6f); - cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setBorder(PdfPCell.NO_BORDER); - return cell; - } - } - } - catch (Exception e) { - } - } - PdfPCell cell = new PdfPCell(); - cell.setHorizontalAlignment(PdfPCell.ALIGN_CENTER); - cell.setVerticalAlignment(PdfPCell.ALIGN_MIDDLE); - cell.setBorder(PdfPCell.NO_BORDER); - return cell; - } - - /** - * Format the length as a displayable string. - * - * @param length the length (assumed to be in default length units) - * - * @return a string representation of the length with unit abbreviation - */ - protected String toLength (double length) { - final Unit defaultUnit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(length)) + defaultUnit.toString(); - } - - /** - * Format the mass as a displayable string. - * - * @param mass the mass (assumed to be in default mass units) - * - * @return a string representation of the mass with mass abbreviation - */ - protected String toMass (double mass) { - final Unit defaultUnit = UnitGroup.UNITS_MASS.getDefaultUnit(); - return NumberFormat.getNumberInstance().format(defaultUnit.toUnit(mass)) + defaultUnit.toString(); - } - - /** - * Get a displayable string of the material's name. - * - * @param material the material to output - * - * @return the material name - */ - protected String toMaterialName (Material material) { - return material.getName(); - } - - /** - * Format the material density as a displayable string. - * - * @param material the material to output - * - * @return a string representation of the material density - */ - protected String toMaterialDensity (Material material) { - return " (" + material.getType().getUnitGroup().getDefaultUnit().toStringUnit(material.getDensity()) + ")"; - } - -} diff --git a/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java b/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java deleted file mode 100644 index e8ecc0ba..00000000 --- a/src/net/sf/openrocket/gui/print/visitor/PartsListVisitorStrategy.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * PartsListVisitorStrategy.java - */ -package net.sf.openrocket.gui.print.visitor; - -import com.itextpdf.text.Document; -import com.itextpdf.text.pdf.PdfWriter; -import net.sf.openrocket.rocketcomponent.*; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * A visitor strategy for creating documentation about a parts list. - */ -public class PartsListVisitorStrategy { - - /** - * Accumulator for parts data. - */ - private Map<PartsAccumulator, PartsAccumulator> crap = new HashMap<PartsAccumulator, PartsAccumulator>(); - - /** - * The iText document. - */ - protected Document document; - - /** - * The direct iText writer. - */ - protected PdfWriter writer; - - /** - * The stages selected. - */ - protected Set<Integer> stages; - - - /** - * Construct a strategy for visiting a parts hierarchy for the purposes of collecting details on those parts. - * - * @param doc The iText document - * @param theWriter The direct iText writer - * @param theStagesToVisit The stages to be visited by this strategy - */ - public PartsListVisitorStrategy (Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) { - document = doc; - writer = theWriter; - stages = theStagesToVisit; - } - - - /** - * Print the parts detail. - * - * @param root the root component - */ - public void doVisit (final RocketComponent root) { - goDeep(root.getChildren()); - } - - /** - * Recurse through the given rocket component. - * - * @param theRc an array of rocket components; all children will be visited recursively - */ - protected void goDeep (final List<RocketComponent> theRc) { - for (RocketComponent rocketComponent : theRc) { - doIt(rocketComponent); - } - } - - /** - * {@inheritDoc} - */ - private void doIt (final RocketComponent component) { - if (component instanceof InnerTube) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof LaunchLug) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - } - - else if (component instanceof NoseCone) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof Transition) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof RadiusRingComponent) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof RingComponent) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof BodyTube) { - final PartsAccumulator key = new PartsAccumulator(component); - PartsAccumulator pa = crap.get(key); - if (pa == null) { - pa = key; - crap.put(pa, pa); - } - pa.increment(); - List<RocketComponent> rc = component.getChildren(); - goDeep(rc); - } - else if (component instanceof TrapezoidFinSet) { - } - else if (component instanceof EllipticalFinSet) { - } - else if (component instanceof FreeformFinSet) { - } - } - - - /** - * {@inheritDoc} - */ - public void close () { - for (PartsAccumulator partsAccumulator : crap.keySet()) { - System.err.println(partsAccumulator.component.getComponentName() + " " + partsAccumulator.quantity); - } - } - -} - -class PartsAccumulator { - - int quantity = 0; - - RocketComponent component; - - PartsAccumulator (RocketComponent theComponent) { - component = theComponent; - } - - void increment () { - quantity++; - } - - int quantity () { - return quantity; - } - - @Override - public boolean equals (final Object o1) { - if (this == o1) { - return true; - } - - RocketComponent that; - if (o1 instanceof net.sf.openrocket.gui.print.visitor.PartsAccumulator) { - that = ((net.sf.openrocket.gui.print.visitor.PartsAccumulator) o1).component; - } - else if (o1 instanceof RocketComponent) { - that = (RocketComponent) o1; - } - else { - return false; - } - - if (this.component.getClass().equals(that.getClass())) { - //If - if (that.getLength() == this.component.getLength()) { - if (that.getMass() == this.component.getMass()) { - return true; - } - } - if (this.component instanceof Coaxial && - that instanceof Coaxial) { - Coaxial cThis = (Coaxial) this.component; - Coaxial cThat = (Coaxial) that; - if (cThis.getInnerRadius() == cThat.getInnerRadius() && - cThis.getOuterRadius() == cThat.getOuterRadius()) { - return true; - } - } - return false; - } - return false; - } - - @Override - public int hashCode () { - return component.getComponentName().hashCode(); - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/print/visitor/TransitionStrategy.java b/src/net/sf/openrocket/gui/print/visitor/TransitionStrategy.java deleted file mode 100644 index 1988ab8a..00000000 --- a/src/net/sf/openrocket/gui/print/visitor/TransitionStrategy.java +++ /dev/null @@ -1,205 +0,0 @@ -package net.sf.openrocket.gui.print.visitor; - -import com.itextpdf.text.Document; -import com.itextpdf.text.DocumentException; -import com.itextpdf.text.Rectangle; -import com.itextpdf.text.pdf.PdfContentByte; -import com.itextpdf.text.pdf.PdfWriter; -import net.sf.openrocket.gui.print.AbstractPrintableTransition; -import net.sf.openrocket.gui.print.ITextHelper; -import net.sf.openrocket.gui.print.PrintableNoseCone; -import net.sf.openrocket.gui.print.PrintableTransition; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.startup.Application; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.List; -import java.util.Set; - -/** - * A strategy for drawing transition/shroud/nose cone templates. - */ -public class TransitionStrategy { - - /** - * The logger. - */ - private static final LogHelper log = Application.getLogger(); - - /** - * The iText document. - */ - protected Document document; - - /** - * The direct iText writer. - */ - protected PdfWriter writer; - - /** - * The stages selected. - */ - protected Set<Integer> stages; - - /** - * Constructor. - * - * @param doc The iText document - * @param theWriter The direct iText writer - * @param theStagesToVisit The stages to be visited by this strategy - */ - public TransitionStrategy(Document doc, PdfWriter theWriter, Set<Integer> theStagesToVisit) { - document = doc; - writer = theWriter; - stages = theStagesToVisit; - } - - /** - * Recurse through the given rocket component. - * - * @param root the root component; all children will be visited recursively - * @param noseCones nose cones are a special form of a transition; if true, then print nose cones - */ - public void writeToDocument(final RocketComponent root, boolean noseCones) { - List<RocketComponent> rc = root.getChildren(); - goDeep(rc, noseCones); - } - - - /** - * Recurse through the given rocket component. - * - * @param theRc an array of rocket components; all children will be visited recursively - * @param noseCones nose cones are a special form of a transition; if true, then print nose cones - */ - protected void goDeep(final List<RocketComponent> theRc, boolean noseCones) { - for (RocketComponent rocketComponent : theRc) { - if (rocketComponent instanceof NoseCone) { - if (noseCones) { - render((Transition) rocketComponent); - } - } else if (rocketComponent instanceof Transition && !noseCones) { - render((Transition) rocketComponent); - } else if (rocketComponent.getChildCount() > 0) { - goDeep(rocketComponent.getChildren(), noseCones); - } - } - } - - /** - * The core behavior of this visitor. - * - * @param component the object to extract info about; a graphical image of the transition shape is drawn to the document - */ - private void render(final Transition component) { - try { - AbstractPrintableTransition pfs; - if (component instanceof NoseCone) { - pfs = new PrintableNoseCone(component); - } else { - pfs = new PrintableTransition(component); - } - - java.awt.Dimension size = pfs.getSize(); - final Dimension pageSize = getPageSize(); - if (fitsOnOnePage(pageSize, size.getWidth(), size.getHeight())) { - printOnOnePage(pfs); - } else { - BufferedImage image = (BufferedImage) pfs.createImage(); - ITextHelper.renderImageAcrossPages(new Rectangle(pageSize.getWidth(), pageSize.getHeight()), - document, writer, image); - } - } catch (DocumentException e) { - log.error("Could not render the transition.", e); - } - } - - /** - * Determine if the image will fit on the given page. - * - * @param pageSize the page size - * @param wImage the width of the thing to be printed - * @param hImage the height of the thing to be printed - * @return true if the thing to be printed will fit on a single page - */ - private boolean fitsOnOnePage(Dimension pageSize, double wImage, double hImage) { - double wPage = pageSize.getWidth(); - double hPage = pageSize.getHeight(); - - int wRatio = (int) Math.ceil(wImage / wPage); - int hRatio = (int) Math.ceil(hImage / hPage); - - return wRatio <= 1.0d && hRatio <= 1.0d; - } - - /** - * Print the transition. - * - * @param theTransition the printable transition - */ - private void printOnOnePage(final AbstractPrintableTransition theTransition) { - Dimension d = getPageSize(); - PdfContentByte cb = writer.getDirectContent(); - Graphics2D g2 = cb.createGraphics(d.width, d.height); - theTransition.print(g2); - g2.dispose(); - document.newPage(); - } - - /** - * Get the dimensions of the paper page. - * - * @return an internal Dimension - */ - protected Dimension getPageSize() { - return new Dimension(document.getPageSize().getWidth(), - document.getPageSize().getHeight()); - } - - /** - * Convenience class to model a dimension. - */ - class Dimension { - /** - * Width, in points. - */ - public float width; - /** - * Height, in points. - */ - public float height; - - /** - * Constructor. - * - * @param w width - * @param h height - */ - public Dimension(float w, float h) { - width = w; - height = h; - } - - /** - * Get the width. - * - * @return the width - */ - public float getWidth() { - return width; - } - - /** - * Get the height. - * - * @return the height - */ - public float getHeight() { - return height; - } - } -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java b/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java deleted file mode 100644 index aa299490..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/BodyTubeShapes.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Transformation; - -import java.awt.Shape; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Rectangle2D; - - -public class BodyTubeShapes extends RocketComponentShapes { - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component; - - double length = tube.getLength(); - double radius = tube.getOuterRadius(); - Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); - - Shape[] s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S, - length*S,2*radius*S); - } - return s; - } - - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component; - - double or = tube.getOuterRadius(); - - Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); - - Shape[] s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S); - } - return s; - } - - -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java b/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java deleted file mode 100644 index 0eb856f5..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/FinSetShapes.java +++ /dev/null @@ -1,297 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - -import java.awt.Shape; -import java.awt.geom.Path2D; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Transformation; - - -public class FinSetShapes extends RocketComponentShapes { - - // TODO: LOW: Clustering is ignored (FinSet cannot currently be clustered) - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component; - - - int fins = finset.getFinCount(); - Transformation cantRotation = finset.getCantRotation(); - Transformation baseRotation = finset.getBaseRotationTransformation(); - Transformation finRotation = finset.getFinRotationTransformation(); - - Coordinate finPoints[] = finset.getFinPointsWithTab(); - - - // TODO: MEDIUM: sloping radius - double radius = finset.getBodyRadius(); - - // Translate & rotate the coordinates - for (int i=0; i<finPoints.length; i++) { - finPoints[i] = cantRotation.transform(finPoints[i]); - finPoints[i] = baseRotation.transform(finPoints[i].add(0,radius,0)); - } - - - // Generate shapes - Shape[] s = new Shape[fins]; - for (int fin=0; fin<fins; fin++) { - Coordinate a; - Path2D.Float p; - - // Make polygon - p = new Path2D.Float(); - for (int i=0; i<finPoints.length; i++) { - a = transformation.transform(finset.toAbsolute(finPoints[i])[0]); - if (i==0) - p.moveTo(a.x*S, a.y*S); - else - p.lineTo(a.x*S, a.y*S); - } - - p.closePath(); - s[fin] = p; - - // Rotate fin coordinates - for (int i=0; i<finPoints.length; i++) - finPoints[i] = finRotation.transform(finPoints[i]); - } - - return s; - } - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - - net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component; - - if (MathUtil.equals(finset.getCantAngle(),0)) - return uncantedShapesBack(finset, transformation); - else - return cantedShapesBack(finset, transformation); - - } - - - private static Shape[] uncantedShapesBack(net.sf.openrocket.rocketcomponent.FinSet finset, - Transformation transformation) { - - int fins = finset.getFinCount(); - double radius = finset.getBodyRadius(); - double thickness = finset.getThickness(); - double height = finset.getSpan(); - - Transformation baseRotation = finset.getBaseRotationTransformation(); - Transformation finRotation = finset.getFinRotationTransformation(); - - - // Generate base coordinates for a single fin - Coordinate c[] = new Coordinate[4]; - c[0]=new Coordinate(0,radius,-thickness/2); - c[1]=new Coordinate(0,radius,thickness/2); - c[2]=new Coordinate(0,height+radius,thickness/2); - c[3]=new Coordinate(0,height+radius,-thickness/2); - - // Apply base rotation - transformPoints(c,baseRotation); - - // Generate shapes - Shape[] s = new Shape[fins]; - for (int fin=0; fin<fins; fin++) { - Coordinate a; - Path2D.Double p; - - // Make polygon - p = new Path2D.Double(); - a = transformation.transform(finset.toAbsolute(c[0])[0]); - p.moveTo(a.z*S, a.y*S); - a = transformation.transform(finset.toAbsolute(c[1])[0]); - p.lineTo(a.z*S, a.y*S); - a = transformation.transform(finset.toAbsolute(c[2])[0]); - p.lineTo(a.z*S, a.y*S); - a = transformation.transform(finset.toAbsolute(c[3])[0]); - p.lineTo(a.z*S, a.y*S); - p.closePath(); - s[fin] = p; - - // Rotate fin coordinates - transformPoints(c,finRotation); - } - - return s; - } - - - // TODO: LOW: Jagged shapes from back draw incorrectly. - private static Shape[] cantedShapesBack(net.sf.openrocket.rocketcomponent.FinSet finset, - Transformation transformation) { - int i; - int fins = finset.getFinCount(); - double radius = finset.getBodyRadius(); - double thickness = finset.getThickness(); - - Transformation baseRotation = finset.getBaseRotationTransformation(); - Transformation finRotation = finset.getFinRotationTransformation(); - Transformation cantRotation = finset.getCantRotation(); - - Coordinate[] sidePoints; - Coordinate[] backPoints; - int maxIndex; - - Coordinate[] points = finset.getFinPoints(); - for (maxIndex = points.length-1; maxIndex > 0; maxIndex--) { - if (points[maxIndex-1].y < points[maxIndex].y) - break; - } - - transformPoints(points,cantRotation); - transformPoints(points,new Transformation(0,radius,0)); - transformPoints(points,baseRotation); - - - sidePoints = new Coordinate[points.length]; - backPoints = new Coordinate[2*(points.length-maxIndex)]; - double sign; - if (finset.getCantAngle() > 0) { - sign = 1.0; - } else { - sign = -1.0; - } - - // Calculate points for the side panel - for (i=0; i < points.length; i++) { - sidePoints[i] = points[i].add(0,0,sign*thickness/2); - } - - // Calculate points for the back portion - i=0; - for (int j=points.length-1; j >= maxIndex; j--, i++) { - backPoints[i] = points[j].add(0,0,sign*thickness/2); - } - for (int j=maxIndex; j <= points.length-1; j++, i++) { - backPoints[i] = points[j].add(0,0,-sign*thickness/2); - } - - // Generate shapes - Shape[] s; - if (thickness > 0.0005) { - - s = new Shape[fins*2]; - for (int fin=0; fin<fins; fin++) { - - s[2*fin] = makePolygonBack(sidePoints,finset,transformation); - s[2*fin+1] = makePolygonBack(backPoints,finset,transformation); - - // Rotate fin coordinates - transformPoints(sidePoints,finRotation); - transformPoints(backPoints,finRotation); - } - - } else { - - s = new Shape[fins]; - for (int fin=0; fin<fins; fin++) { - s[fin] = makePolygonBack(sidePoints,finset,transformation); - transformPoints(sidePoints,finRotation); - } - - } - - return s; - } - - - - private static void transformPoints(Coordinate[] array, Transformation t) { - for (int i=0; i < array.length; i++) { - array[i] = t.transform(array[i]); - } - } - - private static Shape makePolygonBack(Coordinate[] array, net.sf.openrocket.rocketcomponent.FinSet finset, - Transformation t) { - Path2D.Float p; - - // Make polygon - p = new Path2D.Float(); - for (int i=0; i < array.length; i++) { - Coordinate a = t.transform(finset.toAbsolute(array[i])[0]); - if (i==0) - p.moveTo(a.z*S, a.y*S); - else - p.lineTo(a.z*S, a.y*S); - } - p.closePath(); - return p; - } - - - /* Side painting with thickness: - - Coordinate c[] = new Coordinate[8]; - - c[0]=new Coordinate(0-position*rootChord,radius,thickness/2); - c[1]=new Coordinate(rootChord-position*rootChord,radius,thickness/2); - c[2]=new Coordinate(sweep+tipChord-position*rootChord,height+radius,thickness/2); - c[3]=new Coordinate(sweep-position*rootChord,height+radius,thickness/2); - - c[4]=new Coordinate(0-position*rootChord,radius,-thickness/2); - c[5]=new Coordinate(rootChord-position*rootChord,radius,-thickness/2); - c[6]=new Coordinate(sweep+tipChord-position*rootChord,height+radius,-thickness/2); - c[7]=new Coordinate(sweep-position*rootChord,height+radius,-thickness/2); - - if (rotation != 0) { - rot = Transformation.rotate_x(rotation); - for (int i=0; i<8; i++) - c[i] = rot.transform(c[i]); - } - - Shape[] s = new Shape[fins*6]; - rot = Transformation.rotate_x(2*Math.PI/fins); - - for (int fin=0; fin<fins; fin++) { - Coordinate a,b; - Path2D.Float p; - - // First polygon - p = new Path2D.Float(); - a = finset.toAbsolute(c[0]); - p.moveTo(a.x(), a.y()); - a = finset.toAbsolute(c[1]); - p.lineTo(a.x(), a.y()); - a = finset.toAbsolute(c[2]); - p.lineTo(a.x(), a.y()); - a = finset.toAbsolute(c[3]); - p.lineTo(a.x(), a.y()); - p.closePath(); - s[fin*6] = p; - - // Second polygon - p = new Path2D.Float(); - a = finset.toAbsolute(c[4]); - p.moveTo(a.x(), a.y()); - a = finset.toAbsolute(c[5]); - p.lineTo(a.x(), a.y()); - a = finset.toAbsolute(c[6]); - p.lineTo(a.x(), a.y()); - a = finset.toAbsolute(c[7]); - p.lineTo(a.x(), a.y()); - p.closePath(); - s[fin*6+1] = p; - - // Single lines - for (int i=0; i<4; i++) { - a = finset.toAbsolute(c[i]); - b = finset.toAbsolute(c[i+4]); - s[fin*6+2+i] = new Line2D.Float((float)a.x(),(float)a.y(),(float)b.x(),(float)b.y()); - } - - // Rotate fin coordinates - for (int i=0; i<8; i++) - c[i] = rot.transform(c[i]); - } - - */ -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java b/src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java deleted file mode 100644 index f7084ee6..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/LaunchLugShapes.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Transformation; - -import java.awt.Shape; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Rectangle2D; - - -public class LaunchLugShapes extends RocketComponentShapes { - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component; - - double length = lug.getLength(); - double radius = lug.getOuterRadius(); - Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0))); - - Shape[] s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S, - length*S,2*radius*S); - } - return s; - } - - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component; - - double or = lug.getOuterRadius(); - - Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0))); - - Shape[] s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S); - } - return s; - } -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/MassObjectShapes.java b/src/net/sf/openrocket/gui/rocketfigure/MassObjectShapes.java deleted file mode 100644 index c8ea511e..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/MassObjectShapes.java +++ /dev/null @@ -1,47 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - -import java.awt.Shape; -import java.awt.geom.Ellipse2D; -import java.awt.geom.RoundRectangle2D; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Transformation; - - -public class MassObjectShapes extends RocketComponentShapes { - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.MassObject tube = (net.sf.openrocket.rocketcomponent.MassObject)component; - - double length = tube.getLength(); - double radius = tube.getRadius(); - double arc = Math.min(length, 2*radius) * 0.7; - Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); - - Shape[] s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new RoundRectangle2D.Double(start[i].x*S,(start[i].y-radius)*S, - length*S,2*radius*S,arc*S,arc*S); - } - return s; - } - - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.MassObject tube = (net.sf.openrocket.rocketcomponent.MassObject)component; - - double or = tube.getRadius(); - - Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); - - Shape[] s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S); - } - return s; - } - - -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/RingComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/RingComponentShapes.java deleted file mode 100644 index 6cf6fe33..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/RingComponentShapes.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - - -import java.awt.Shape; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Rectangle2D; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Transformation; - - -public class RingComponentShapes extends RocketComponentShapes { - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component; - Shape[] s; - - double length = tube.getLength(); - double or = tube.getOuterRadius(); - double ir = tube.getInnerRadius(); - - - Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); - - if ((or-ir >= 0.0012) && (ir > 0)) { - // Draw outer and inner - s = new Shape[start.length*2]; - for (int i=0; i < start.length; i++) { - s[2*i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S, - length*S,2*or*S); - s[2*i+1] = new Rectangle2D.Double(start[i].x*S,(start[i].y-ir)*S, - length*S,2*ir*S); - } - } else { - // Draw only outer - s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S, - length*S,2*or*S); - } - } - return s; - } - - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component; - Shape[] s; - - double or = tube.getOuterRadius(); - double ir = tube.getInnerRadius(); - - - Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0))); - - if ((ir < or) && (ir > 0)) { - // Draw inner and outer - s = new Shape[start.length*2]; - for (int i=0; i < start.length; i++) { - s[2*i] = new Ellipse2D.Double((start[i].z-or)*S, (start[i].y-or)*S, - 2*or*S, 2*or*S); - s[2*i+1] = new Ellipse2D.Double((start[i].z-ir)*S, (start[i].y-ir)*S, - 2*ir*S, 2*ir*S); - } - } else { - // Draw only outer - s = new Shape[start.length]; - for (int i=0; i < start.length; i++) { - s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S); - } - } - return s; - } - -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java deleted file mode 100644 index 43096273..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/RocketComponentShapes.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - - -import java.awt.Shape; - -import net.sf.openrocket.gui.scalefigure.RocketFigure; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Transformation; - - -/** - * A catch-all, no-operation drawing component. - */ -public class RocketComponentShapes { - - protected static final double S = RocketFigure.EXTRA_SCALE; - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation t) { - // no-op - Application.getExceptionHandler().handleErrorCondition("ERROR: RocketComponent.getShapesSide called with " - + component); - return new Shape[0]; - } - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation t) { - // no-op - Application.getExceptionHandler().handleErrorCondition("ERROR: RocketComponent.getShapesBack called with " - +component); - return new Shape[0]; - } - - -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java deleted file mode 100644 index 791057c7..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java +++ /dev/null @@ -1,114 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Transformation; - -import java.awt.*; -import java.awt.geom.Path2D; -import java.util.ArrayList; - - -public class SymmetricComponentShapes extends RocketComponentShapes { - private static final int MINPOINTS = 91; - private static final double ACCEPTABLE_ANGLE = Math.cos(7.0 * Math.PI / 180.0); - - // TODO: HIGH: adaptiveness sucks, remove it. - - // TODO: LOW: Uses only first component of cluster (not currently clusterable) - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - return getShapesSide(component, transformation, S); - } - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation, final double scaleFactor) { - net.sf.openrocket.rocketcomponent.SymmetricComponent c = (net.sf.openrocket.rocketcomponent.SymmetricComponent) component; - int i; - - final double delta = 0.0000001; - double x; - - ArrayList<Coordinate> points = new ArrayList<Coordinate>(); - x = delta; - points.add(new Coordinate(x, c.getRadius(x), 0)); - for (i = 1; i < MINPOINTS - 1; i++) { - x = c.getLength() * i / (MINPOINTS - 1); - points.add(new Coordinate(x, c.getRadius(x), 0)); - //System.out.println("Starting with x="+x); - } - x = c.getLength() - delta; - points.add(new Coordinate(x, c.getRadius(x), 0)); - - - i = 0; - while (i < points.size() - 2) { - if (angleAcceptable(points.get(i), points.get(i + 1), points.get(i + 2)) || - points.get(i + 1).x - points.get(i).x < 0.001) { // 1mm - i++; - continue; - } - - // Split the longer of the areas - int n; - if (points.get(i + 2).x - points.get(i + 1).x > points.get(i + 1).x - points.get(i).x) - n = i + 1; - else - n = i; - - x = (points.get(n).x + points.get(n + 1).x) / 2; - points.add(n + 1, new Coordinate(x, c.getRadius(x), 0)); - } - - - //System.out.println("Final points: "+points.size()); - - final int len = points.size(); - - for (i = 0; i < len; i++) { - points.set(i, c.toAbsolute(points.get(i))[0]); - } - - /* Show points: - Shape[] s = new Shape[len+1]; - final double d=0.001; - for (i=0; i<len; i++) { - s[i] = new Ellipse2D.Double(points.get(i).x()-d/2,points.get(i).y()-d/2,d,d); - } - */ - - //System.out.println("here"); - - // TODO: LOW: curved path instead of linear - Path2D.Double path = new Path2D.Double(); - path.moveTo(points.get(len - 1).x * scaleFactor, points.get(len - 1).y * scaleFactor); - for (i = len - 2; i >= 0; i--) { - path.lineTo(points.get(i).x * scaleFactor, points.get(i).y * scaleFactor); - } - for (i = 0; i < len; i++) { - path.lineTo(points.get(i).x * scaleFactor, -points.get(i).y * scaleFactor); - } - path.lineTo(points.get(len - 1).x * scaleFactor, points.get(len - 1).y * scaleFactor); - path.closePath(); - - //s[len] = path; - //return s; - return new Shape[] { path }; - } - - private static boolean angleAcceptable(Coordinate v1, Coordinate v2, Coordinate v3) { - return (cosAngle(v1, v2, v3) > ACCEPTABLE_ANGLE); - } - - /* - * cosAngle = v1.v2 / |v1|*|v2| = v1.v2 / sqrt(v1.v1*v2.v2) - */ - private static double cosAngle(Coordinate v1, Coordinate v2, Coordinate v3) { - double cos; - double len; - cos = Coordinate.dot(v1.sub(v2), v2.sub(v3)); - len = MathUtil.safeSqrt(v1.sub(v2).length2() * v2.sub(v3).length2()); - return cos / len; - } -} diff --git a/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java b/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java deleted file mode 100644 index 2312c1bd..00000000 --- a/src/net/sf/openrocket/gui/rocketfigure/TransitionShapes.java +++ /dev/null @@ -1,101 +0,0 @@ -package net.sf.openrocket.gui.rocketfigure; - -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Transformation; - -import java.awt.*; -import java.awt.geom.Ellipse2D; -import java.awt.geom.Path2D; -import java.awt.geom.Rectangle2D; - - -public class TransitionShapes extends RocketComponentShapes { - - // TODO: LOW: Uses only first component of cluster (not currently clusterable). - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - return getShapesSide(component, transformation, S); - } - - public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation, final double scaleFactor) { - net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component; - - Shape[] mainShapes; - - // Simpler shape for conical transition, others use the method from SymmetricComponent - if (transition.getType() == Transition.Shape.CONICAL) { - double length = transition.getLength(); - double r1 = transition.getForeRadius(); - double r2 = transition.getAftRadius(); - Coordinate start = transformation.transform(transition. - toAbsolute(Coordinate.NUL)[0]); - - Path2D.Float path = new Path2D.Float(); - path.moveTo(start.x* scaleFactor, r1* scaleFactor); - path.lineTo((start.x+length)* scaleFactor, r2* scaleFactor); - path.lineTo((start.x+length)* scaleFactor, -r2* scaleFactor); - path.lineTo(start.x* scaleFactor, -r1* scaleFactor); - path.closePath(); - - mainShapes = new Shape[] { path }; - } else { - mainShapes = SymmetricComponentShapes.getShapesSide(component, transformation, scaleFactor); - } - - Rectangle2D.Double shoulder1=null, shoulder2=null; - int arrayLength = mainShapes.length; - - if (transition.getForeShoulderLength() > 0.0005) { - Coordinate start = transformation.transform(transition. - toAbsolute(Coordinate.NUL)[0]); - double r = transition.getForeShoulderRadius(); - double l = transition.getForeShoulderLength(); - shoulder1 = new Rectangle2D.Double((start.x-l)* scaleFactor, -r* scaleFactor, l* scaleFactor, 2*r* scaleFactor); - arrayLength++; - } - if (transition.getAftShoulderLength() > 0.0005) { - Coordinate start = transformation.transform(transition. - toAbsolute(new Coordinate(transition.getLength()))[0]); - double r = transition.getAftShoulderRadius(); - double l = transition.getAftShoulderLength(); - shoulder2 = new Rectangle2D.Double(start.x* scaleFactor, -r* scaleFactor, l* scaleFactor, 2*r* scaleFactor); - arrayLength++; - } - if (shoulder1==null && shoulder2==null) - return mainShapes; - - Shape[] shapes = new Shape[arrayLength]; - int i; - - for (i=0; i < mainShapes.length; i++) { - shapes[i] = mainShapes[i]; - } - if (shoulder1 != null) { - shapes[i] = shoulder1; - i++; - } - if (shoulder2 != null) { - shapes[i] = shoulder2; - } - return shapes; - } - - - public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, - Transformation transformation) { - net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component; - - double r1 = transition.getForeRadius(); - double r2 = transition.getAftRadius(); - - Shape[] s = new Shape[2]; - s[0] = new Ellipse2D.Double(-r1*S,-r1*S,2*r1*S,2*r1*S); - s[1] = new Ellipse2D.Double(-r2*S,-r2*S,2*r2*S,2*r2*S); - return s; - } - - -} diff --git a/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java b/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java deleted file mode 100644 index e22243de..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java +++ /dev/null @@ -1,133 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - -import java.awt.Color; -import java.awt.Dimension; -import java.util.EventListener; -import java.util.EventObject; -import java.util.LinkedList; -import java.util.List; - -import javax.swing.JPanel; - -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.util.StateChangeListener; - - -public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure { - - // Number of pixels to leave at edges when fitting figure - private static final int DEFAULT_BORDER_PIXELS_WIDTH = 30; - private static final int DEFAULT_BORDER_PIXELS_HEIGHT = 20; - - - protected final double dpi; - - protected double scale = 1.0; - protected double scaling = 1.0; - - protected int borderPixelsWidth = DEFAULT_BORDER_PIXELS_WIDTH; - protected int borderPixelsHeight = DEFAULT_BORDER_PIXELS_HEIGHT; - - protected final List<EventListener> listeners = new LinkedList<EventListener>(); - - - public AbstractScaleFigure() { - this.dpi = GUIUtil.getDPI(); - this.scaling = 1.0; - this.scale = dpi / 0.0254 * scaling; - - setBackground(Color.WHITE); - setOpaque(true); - } - - - - public abstract void updateFigure(); - - public abstract double getFigureWidth(); - - public abstract double getFigureHeight(); - - - @Override - public double getScaling() { - return scaling; - } - - @Override - public double getAbsoluteScale() { - return scale; - } - - @Override - public void setScaling(double scaling) { - if (Double.isInfinite(scaling) || Double.isNaN(scaling)) - scaling = 1.0; - if (scaling < 0.001) - scaling = 0.001; - if (scaling > 1000) - scaling = 1000; - if (Math.abs(this.scaling - scaling) < 0.01) - return; - this.scaling = scaling; - this.scale = dpi / 0.0254 * scaling; - updateFigure(); - } - - @Override - public void setScaling(Dimension bounds) { - double zh = 1, zv = 1; - int w = bounds.width - 2 * borderPixelsWidth - 20; - int h = bounds.height - 2 * borderPixelsHeight - 20; - - if (w < 10) - w = 10; - if (h < 10) - h = 10; - - zh = (w) / getFigureWidth(); - zv = (h) / getFigureHeight(); - - double s = Math.min(zh, zv) / dpi * 0.0254 - 0.001; - - setScaling(s); - } - - - @Override - public Dimension getBorderPixels() { - return new Dimension(borderPixelsWidth, borderPixelsHeight); - } - - @Override - public void setBorderPixels(int width, int height) { - this.borderPixelsWidth = width; - this.borderPixelsHeight = height; - } - - - @Override - public void addChangeListener(EventListener listener) { - listeners.add(0, listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - listeners.remove(listener); - } - - private EventObject changeEvent = null; - - protected void fireChangeEvent() { - if (changeEvent == null) - changeEvent = new EventObject(this); - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] list = listeners.toArray(new EventListener[0]); - for (EventListener l : list) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(changeEvent); - } - } - } - -} diff --git a/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java b/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java deleted file mode 100644 index 0f08be21..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java +++ /dev/null @@ -1,344 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.geom.AffineTransform; -import java.awt.geom.Line2D; -import java.awt.geom.NoninvertibleTransformException; -import java.awt.geom.Path2D; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; - -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.unit.Tick; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -// TODO: MEDIUM: the figure jumps and bugs when using automatic fitting - -public class FinPointFigure extends AbstractScaleFigure { - - private static final int BOX_SIZE = 4; - - private final FreeformFinSet finset; - private int modID = -1; - - private double minX, maxX, maxY; - private double figureWidth = 0; - private double figureHeight = 0; - private double translateX = 0; - private double translateY = 0; - - private AffineTransform transform; - private Rectangle2D.Double[] handles = null; - - - public FinPointFigure(FreeformFinSet finset) { - this.finset = finset; - } - - - @Override - public void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - - - double tx, ty; - // Calculate translation for figure centering - if (figureWidth * scale + 2 * borderPixelsWidth < getWidth()) { - - // Figure fits in the viewport - tx = (getWidth() - figureWidth * scale) / 2 - minX * scale; - - } else { - - // Figure does not fit in viewport - tx = borderPixelsWidth - minX * scale; - - } - - - if (figureHeight * scale + 2 * borderPixelsHeight < getHeight()) { - ty = getHeight() - borderPixelsHeight; - } else { - ty = borderPixelsHeight + figureHeight * scale; - } - - if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { - // Origin has changed, fire event - translateX = tx; - translateY = ty; - fireChangeEvent(); - } - - - if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { - // Origin has changed, fire event - translateX = tx; - translateY = ty; - fireChangeEvent(); - } - - - // Calculate and store the transformation used - transform = new AffineTransform(); - transform.translate(translateX, translateY); - transform.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); - - // TODO: HIGH: border Y-scale upwards - - g2.transform(transform); - - // Set rendering hints appropriately - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - - - Rectangle visible = g2.getClipBounds(); - double x0 = ((double) visible.x - 3) / EXTRA_SCALE; - double x1 = ((double) visible.x + visible.width + 4) / EXTRA_SCALE; - double y0 = ((double) visible.y - 3) / EXTRA_SCALE; - double y1 = ((double) visible.y + visible.height + 4) / EXTRA_SCALE; - - - // Background grid - - g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(new Color(0, 0, 255, 30)); - - Unit unit; - if (this.getParent() != null && - this.getParent().getParent() instanceof ScaleScrollPane) { - unit = ((ScaleScrollPane) this.getParent().getParent()).getCurrentUnit(); - } else { - unit = UnitGroup.UNITS_LENGTH.getDefaultUnit(); - } - - // vertical - Tick[] ticks = unit.getTicks(x0, x1, - ScaleScrollPane.MINOR_TICKS / scale, - ScaleScrollPane.MAJOR_TICKS / scale); - Line2D.Double line = new Line2D.Double(); - for (Tick t : ticks) { - if (t.major) { - line.setLine(t.value * EXTRA_SCALE, y0 * EXTRA_SCALE, - t.value * EXTRA_SCALE, y1 * EXTRA_SCALE); - g2.draw(line); - } - } - - // horizontal - ticks = unit.getTicks(y0, y1, - ScaleScrollPane.MINOR_TICKS / scale, - ScaleScrollPane.MAJOR_TICKS / scale); - for (Tick t : ticks) { - if (t.major) { - line.setLine(x0 * EXTRA_SCALE, t.value * EXTRA_SCALE, - x1 * EXTRA_SCALE, t.value * EXTRA_SCALE); - g2.draw(line); - } - } - - - - - - // Base rocket line - g2.setStroke(new BasicStroke((float) (3.0 * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(Color.GRAY); - - g2.drawLine((int) (x0 * EXTRA_SCALE), 0, (int) (x1 * EXTRA_SCALE), 0); - - - // Fin shape - Coordinate[] points = finset.getFinPoints(); - Path2D.Double shape = new Path2D.Double(); - shape.moveTo(0, 0); - for (int i = 1; i < points.length; i++) { - shape.lineTo(points[i].x * EXTRA_SCALE, points[i].y * EXTRA_SCALE); - } - - g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(Color.BLACK); - g2.draw(shape); - - - // Fin point boxes - g2.setColor(new Color(150, 0, 0)); - double s = BOX_SIZE * EXTRA_SCALE / scale; - handles = new Rectangle2D.Double[points.length]; - for (int i = 0; i < points.length; i++) { - Coordinate c = points[i]; - handles[i] = new Rectangle2D.Double(c.x * EXTRA_SCALE - s, c.y * EXTRA_SCALE - s, 2 * s, 2 * s); - g2.draw(handles[i]); - } - - } - - - - public int getIndexByPoint(double x, double y) { - if (handles == null) - return -1; - - // Calculate point in shapes' coordinates - Point2D.Double p = new Point2D.Double(x, y); - try { - transform.inverseTransform(p, p); - } catch (NoninvertibleTransformException e) { - return -1; - } - - for (int i = 0; i < handles.length; i++) { - if (handles[i].contains(p)) - return i; - } - return -1; - } - - - public int getSegmentByPoint(double x, double y) { - if (handles == null) - return -1; - - // Calculate point in shapes' coordinates - Point2D.Double p = new Point2D.Double(x, y); - try { - transform.inverseTransform(p, p); - } catch (NoninvertibleTransformException e) { - return -1; - } - - double x0 = p.x / EXTRA_SCALE; - double y0 = p.y / EXTRA_SCALE; - double delta = BOX_SIZE / scale; - - System.out.println("Point: " + x0 + "," + y0); - System.out.println("delta: " + (BOX_SIZE / scale)); - - Coordinate[] points = finset.getFinPoints(); - for (int i = 1; i < points.length; i++) { - double x1 = points[i - 1].x; - double y1 = points[i - 1].y; - double x2 = points[i].x; - double y2 = points[i].y; - - // System.out.println("point1:"+x1+","+y1+" point2:"+x2+","+y2); - - double u = Math.abs((x2 - x1) * (y1 - y0) - (x1 - x0) * (y2 - y1)) / - MathUtil.hypot(x2 - x1, y2 - y1); - System.out.println("Distance of segment " + i + " is " + u); - if (u < delta) - return i; - } - - return -1; - } - - - public Point2D.Double convertPoint(double x, double y) { - Point2D.Double p = new Point2D.Double(x, y); - try { - transform.inverseTransform(p, p); - } catch (NoninvertibleTransformException e) { - assert (false) : "Should not occur"; - return new Point2D.Double(0, 0); - } - - p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE); - return p; - } - - - - @Override - public Dimension getOrigin() { - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - return new Dimension((int) translateX, (int) translateY); - } - - @Override - public double getFigureWidth() { - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - return figureWidth; - } - - @Override - public double getFigureHeight() { - if (modID != finset.getRocket().getAerodynamicModID()) { - modID = finset.getRocket().getAerodynamicModID(); - calculateDimensions(); - } - return figureHeight; - } - - - private void calculateDimensions() { - minX = 0; - maxX = 0; - maxY = 0; - - for (Coordinate c : finset.getFinPoints()) { - if (c.x < minX) - minX = c.x; - if (c.x > maxX) - maxX = c.x; - if (c.y > maxY) - maxY = c.y; - } - - if (maxX < 0.01) - maxX = 0.01; - - figureWidth = maxX - minX; - figureHeight = maxY; - - - Dimension d = new Dimension((int) (figureWidth * scale + 2 * borderPixelsWidth), - (int) (figureHeight * scale + 2 * borderPixelsHeight)); - - if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { - setPreferredSize(d); - setMinimumSize(d); - revalidate(); - } - } - - - - @Override - public void updateFigure() { - repaint(); - } - - - -} diff --git a/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java b/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java deleted file mode 100644 index 854eb17a..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java +++ /dev/null @@ -1,561 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - - -import java.awt.BasicStroke; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.Shape; -import java.awt.geom.AffineTransform; -import java.awt.geom.Ellipse2D; -import java.awt.geom.NoninvertibleTransformException; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashSet; - -import net.sf.openrocket.gui.figureelements.FigureElement; -import net.sf.openrocket.gui.util.ColorConversion; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.LineStyle; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.Transformation; - -/** - * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can - * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)}, - * {@link #clearRelativeExtra()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocketFigure extends AbstractScaleFigure { - private static final long serialVersionUID = 1L; - - private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure"; - private static final String ROCKET_FIGURE_SUFFIX = "Shapes"; - - public static final int TYPE_SIDE = 1; - public static final int TYPE_BACK = 2; - - // Width for drawing normal and selected components - public static final double NORMAL_WIDTH = 1.0; - public static final double SELECTED_WIDTH = 2.0; - - - private Configuration configuration; - private RocketComponent[] selection = new RocketComponent[0]; - - private int type = TYPE_SIDE; - - private double rotation; - private Transformation transformation; - - private double translateX, translateY; - - - - /* - * figureComponents contains the corresponding RocketComponents of the figureShapes - */ - private final ArrayList<Shape> figureShapes = new ArrayList<Shape>(); - private final ArrayList<RocketComponent> figureComponents = - new ArrayList<RocketComponent>(); - - private double minX = 0, maxX = 0, maxR = 0; - // Figure width and height in SI-units and pixels - private double figureWidth = 0, figureHeight = 0; - private int figureWidthPx = 0, figureHeightPx = 0; - - private AffineTransform g2transformation = null; - - private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>(); - private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>(); - - - /** - * Creates a new rocket figure. - */ - public RocketFigure(Configuration configuration) { - super(); - - this.configuration = configuration; - - this.rotation = 0.0; - this.transformation = Transformation.rotate_x(0.0); - - updateFigure(); - } - - - /** - * Set the configuration displayed by the figure. It may use the same or different rocket. - * - * @param configuration the configuration to display. - */ - public void setConfiguration(Configuration configuration) { - this.configuration = configuration; - updateFigure(); - } - - - @Override - public Dimension getOrigin() { - return new Dimension((int) translateX, (int) translateY); - } - - @Override - public double getFigureHeight() { - return figureHeight; - } - - @Override - public double getFigureWidth() { - return figureWidth; - } - - - public RocketComponent[] getSelection() { - return selection; - } - - public void setSelection(RocketComponent[] selection) { - if (selection == null) { - this.selection = new RocketComponent[0]; - } else { - this.selection = selection; - } - updateFigure(); - } - - - public double getRotation() { - return rotation; - } - - public Transformation getRotateTransformation() { - return transformation; - } - - public void setRotation(double rot) { - if (MathUtil.equals(rotation, rot)) - return; - this.rotation = rot; - this.transformation = Transformation.rotate_x(rotation); - updateFigure(); - } - - - public int getType() { - return type; - } - - public void setType(int type) { - if (type != TYPE_BACK && type != TYPE_SIDE) { - throw new IllegalArgumentException("Illegal type: " + type); - } - if (this.type == type) - return; - this.type = type; - updateFigure(); - } - - - - - - /** - * Updates the figure shapes and figure size. - */ - @Override - public void updateFigure() { - figureShapes.clear(); - figureComponents.clear(); - - calculateSize(); - - // Get shapes for all active components - for (RocketComponent c : configuration) { - Shape[] s = getShapes(c); - for (int i = 0; i < s.length; i++) { - figureShapes.add(s[i]); - figureComponents.add(c); - } - } - - repaint(); - fireChangeEvent(); - } - - - public void addRelativeExtra(FigureElement p) { - relativeExtra.add(p); - } - - public void removeRelativeExtra(FigureElement p) { - relativeExtra.remove(p); - } - - public void clearRelativeExtra() { - relativeExtra.clear(); - } - - - public void addAbsoluteExtra(FigureElement p) { - absoluteExtra.add(p); - } - - public void removeAbsoluteExtra(FigureElement p) { - absoluteExtra.remove(p); - } - - public void clearAbsoluteExtra() { - absoluteExtra.clear(); - } - - - /** - * Paints the rocket on to the Graphics element. - * <p> - * Warning: If paintComponent is used outside the normal Swing usage, some Swing - * dependent parameters may be left wrong (mainly transformation). If it is used, - * the RocketFigure should be repainted immediately afterwards. - */ - @Override - public void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - - - AffineTransform baseTransform = g2.getTransform(); - - // Update figure shapes if necessary - if (figureShapes == null) - updateFigure(); - - - double tx, ty; - // Calculate translation for figure centering - if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) { - - // Figure fits in the viewport - if (type == TYPE_BACK) - tx = getWidth() / 2; - else - tx = (getWidth() - figureWidthPx) / 2 - minX * scale; - - } else { - - // Figure does not fit in viewport - if (type == TYPE_BACK) - tx = borderPixelsWidth + figureWidthPx / 2; - else - tx = borderPixelsWidth - minX * scale; - - } - - ty = computeTy(figureHeightPx); - - if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { - // Origin has changed, fire event - translateX = tx; - translateY = ty; - fireChangeEvent(); - } - - - // Calculate and store the transformation used - // (inverse is used in detecting clicks on objects) - g2transformation = new AffineTransform(); - g2transformation.translate(translateX, translateY); - // Mirror position Y-axis upwards - g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); - - g2.transform(g2transformation); - - // Set rendering hints appropriately - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - - - // Draw all shapes - - for (int i = 0; i < figureShapes.size(); i++) { - RocketComponent c = figureComponents.get(i); - Shape s = figureShapes.get(i); - boolean selected = false; - - // Check if component is in the selection - for (int j = 0; j < selection.length; j++) { - if (c == selection[j]) { - selected = true; - break; - } - } - - // Set component color and line style - net.sf.openrocket.util.Color color = c.getColor(); - if (color == null) { - color = Application.getPreferences().getDefaultColor(c.getClass()); - } - g2.setColor(ColorConversion.toAwtColor(color)); - - LineStyle style = c.getLineStyle(); - if (style == null) - style = Application.getPreferences().getDefaultLineStyle(c.getClass()); - - float[] dashes = style.getDashes(); - for (int j = 0; j < dashes.length; j++) { - dashes[j] *= EXTRA_SCALE / scale; - } - - if (selected) { - g2.setStroke(new BasicStroke((float) (SELECTED_WIDTH * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0)); - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_PURE); - } else { - g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, dashes, 0)); - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - } - g2.draw(s); - - } - - g2.setStroke(new BasicStroke((float) (NORMAL_WIDTH * EXTRA_SCALE / scale), - BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - - - // Draw motors - String motorID = configuration.getMotorConfigurationID(); - Color fillColor = ((SwingPreferences)Application.getPreferences()).getMotorFillColor(); - Color borderColor = ((SwingPreferences)Application.getPreferences()).getMotorBorderColor(); - Iterator<MotorMount> iterator = configuration.motorIterator(); - while (iterator.hasNext()) { - MotorMount mount = iterator.next(); - Motor motor = mount.getMotor(motorID); - double length = motor.getLength(); - double radius = motor.getDiameter() / 2; - - Coordinate[] position = ((RocketComponent) mount).toAbsolute( - new Coordinate(((RocketComponent) mount).getLength() + - mount.getMotorOverhang() - length)); - - for (int i = 0; i < position.length; i++) { - position[i] = transformation.transform(position[i]); - } - - for (Coordinate coord : position) { - Shape s; - if (type == TYPE_SIDE) { - s = new Rectangle2D.Double(EXTRA_SCALE * coord.x, - EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * length, - EXTRA_SCALE * 2 * radius); - } else { - s = new Ellipse2D.Double(EXTRA_SCALE * (coord.z - radius), - EXTRA_SCALE * (coord.y - radius), EXTRA_SCALE * 2 * radius, - EXTRA_SCALE * 2 * radius); - } - g2.setColor(fillColor); - g2.fill(s); - g2.setColor(borderColor); - g2.draw(s); - } - } - - - - // Draw relative extras - for (FigureElement e : relativeExtra) { - e.paint(g2, scale / EXTRA_SCALE); - } - - // Draw absolute extras - g2.setTransform(baseTransform); - Rectangle rect = this.getVisibleRect(); - - for (FigureElement e : absoluteExtra) { - e.paint(g2, 1.0, rect); - } - - } - - protected double computeTy(int heightPx) { - final double ty; - if (heightPx + 2 * borderPixelsHeight < getHeight()) { - ty = getHeight() / 2; - } else { - ty = borderPixelsHeight + heightPx / 2; - } - return ty; - } - - - public RocketComponent[] getComponentsByPoint(double x, double y) { - // Calculate point in shapes' coordinates - Point2D.Double p = new Point2D.Double(x, y); - try { - g2transformation.inverseTransform(p, p); - } catch (NoninvertibleTransformException e) { - return new RocketComponent[0]; - } - - LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>(); - - for (int i = 0; i < figureShapes.size(); i++) { - if (figureShapes.get(i).contains(p)) - l.add(figureComponents.get(i)); - } - return l.toArray(new RocketComponent[0]); - } - - - - /** - * Gets the shapes required to draw the component. - * - * @param component - * @param params - * @return - */ - private Shape[] getShapes(RocketComponent component) { - Reflection.Method m; - - // Find the appropriate method - switch (type) { - case TYPE_SIDE: - m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide", - RocketComponent.class, Transformation.class); - break; - - case TYPE_BACK: - m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack", - RocketComponent.class, Transformation.class); - break; - - default: - throw new BugException("Unknown figure type = " + type); - } - - if (m == null) { - Application.getExceptionHandler().handleErrorCondition("ERROR: Rocket figure paint method not found for " - + component); - return new Shape[0]; - } - - return (Shape[]) m.invokeStatic(component, transformation); - } - - - - /** - * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions. - * The bounds are stored in the variables minX, maxX and maxR. - */ - private void calculateFigureBounds() { - Collection<Coordinate> bounds = configuration.getBounds(); - - if (bounds.isEmpty()) { - minX = 0; - maxX = 0; - maxR = 0; - return; - } - - minX = Double.MAX_VALUE; - maxX = Double.MIN_VALUE; - maxR = 0; - for (Coordinate c : bounds) { - double x = c.x, r = MathUtil.hypot(c.y, c.z); - if (x < minX) - minX = x; - if (x > maxX) - maxX = x; - if (r > maxR) - maxR = r; - } - } - - - public double getBestZoom(Rectangle2D bounds) { - double zh = 1, zv = 1; - if (bounds.getWidth() > 0.0001) - zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth(); - if (bounds.getHeight() > 0.0001) - zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight(); - return Math.min(zh, zv); - } - - - - /** - * Calculates the necessary size of the figure and set the PreferredSize - * property accordingly. - */ - private void calculateSize() { - calculateFigureBounds(); - - switch (type) { - case TYPE_SIDE: - figureWidth = maxX - minX; - figureHeight = 2 * maxR; - break; - - case TYPE_BACK: - figureWidth = 2 * maxR; - figureHeight = 2 * maxR; - break; - - default: - assert (false) : "Should not occur, type=" + type; - figureWidth = 0; - figureHeight = 0; - } - - figureWidthPx = (int) (figureWidth * scale); - figureHeightPx = (int) (figureHeight * scale); - - Dimension d = new Dimension(figureWidthPx + 2 * borderPixelsWidth, - figureHeightPx + 2 * borderPixelsHeight); - - if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { - setPreferredSize(d); - setMinimumSize(d); - revalidate(); - } - } - - public Rectangle2D getDimensions() { - switch (type) { - case TYPE_SIDE: - return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR); - - case TYPE_BACK: - return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR); - - default: - throw new BugException("Illegal figure type = " + type); - } - } - -} diff --git a/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java b/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java deleted file mode 100644 index cf458724..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ /dev/null @@ -1,742 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - - -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Point; -import java.awt.event.ActionEvent; -import java.awt.event.InputEvent; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.Collection; -import java.util.EventListener; -import java.util.EventObject; -import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.JSlider; -import javax.swing.JToggleButton; -import javax.swing.JViewport; -import javax.swing.SwingUtilities; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.TreePath; -import javax.swing.tree.TreeSelectionModel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.aerodynamics.AerodynamicCalculator; -import net.sf.openrocket.aerodynamics.BarrowmanCalculator; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.adaptors.MotorConfigurationModel; -import net.sf.openrocket.gui.components.BasicSlider; -import net.sf.openrocket.gui.components.StageSelector; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.gui.configdialog.ComponentConfigDialog; -import net.sf.openrocket.gui.figureelements.CGCaret; -import net.sf.openrocket.gui.figureelements.CPCaret; -import net.sf.openrocket.gui.figureelements.Caret; -import net.sf.openrocket.gui.figureelements.RocketInfo; -import net.sf.openrocket.gui.main.SimulationWorker; -import net.sf.openrocket.gui.main.componenttree.ComponentTreeModel; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; -import net.sf.openrocket.simulation.listeners.system.InterruptListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.StateChangeListener; - -/** - * A JPanel that contains a RocketFigure and buttons to manipulate the figure. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource { - - private static final Translator trans = Application.getTranslator(); - private final RocketFigure figure; - private final ScaleScrollPane scrollPane; - - private JLabel infoMessage; - - private TreeSelectionModel selectionModel = null; - - - /* Calculation of CP and CG */ - private AerodynamicCalculator aerodynamicCalculator; - private MassCalculator massCalculator; - - - private final OpenRocketDocument document; - private final Configuration configuration; - - private Caret extraCP = null; - private Caret extraCG = null; - private RocketInfo extraText = null; - - - private double cpAOA = Double.NaN; - private double cpTheta = Double.NaN; - private double cpMach = Double.NaN; - private double cpRoll = Double.NaN; - - // The functional ID of the rocket that was simulated - private int flightDataFunctionalID = -1; - private String flightDataMotorID = null; - - - private SimulationWorker backgroundSimulationWorker = null; - - private List<EventListener> listeners = new ArrayList<EventListener>(); - - - /** - * The executor service used for running the background simulations. - * This uses a fixed-sized thread pool for all background simulations - * with all threads in daemon mode and with minimum priority. - */ - private static final Executor backgroundSimulationExecutor; - static { - backgroundSimulationExecutor = Executors.newFixedThreadPool(SwingPreferences.getMaxThreadCount(), - new ThreadFactory() { - private ThreadFactory factory = Executors.defaultThreadFactory(); - - @Override - public Thread newThread(Runnable r) { - Thread t = factory.newThread(r); - t.setDaemon(true); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - } - - - public RocketPanel(OpenRocketDocument document) { - - this.document = document; - configuration = document.getDefaultConfiguration(); - - // TODO: FUTURE: calculator selection - aerodynamicCalculator = new BarrowmanCalculator(); - massCalculator = new BasicMassCalculator(); - - // Create figure and custom scroll pane - figure = new RocketFigure(configuration); - - scrollPane = new ScaleScrollPane(figure) { - @Override - public void mouseClicked(MouseEvent event) { - handleMouseClick(event); - } - }; - scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE); - scrollPane.setFitting(true); - - createPanel(); - - configuration.addChangeListener(new StateChangeListener() { - @Override - public void stateChanged(EventObject e) { - // System.out.println("Configuration changed, calling updateFigure"); - updateExtras(); - figure.updateFigure(); - } - }); - } - - - /** - * Creates the layout and components of the panel. - */ - private void createPanel() { - setLayout(new MigLayout("", "[shrink][grow]", "[shrink][shrink][grow][shrink]")); - - setPreferredSize(new Dimension(800, 300)); - - - //// Create toolbar - - // Side/back buttons - FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE); - //// Side view - action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Sideview")); - //// Side view - action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Sideview")); - JToggleButton toggle = new JToggleButton(action); - add(toggle, "spanx, split"); - - action = new FigureTypeAction(RocketFigure.TYPE_BACK); - //// Back view - action.putValue(Action.NAME, trans.get("RocketPanel.FigTypeAct.Backview")); - //// Back view - action.putValue(Action.SHORT_DESCRIPTION, trans.get("RocketPanel.FigTypeAct.ttip.Backview")); - toggle = new JToggleButton(action); - add(toggle, "gap rel"); - - - // Zoom level selector - ScaleSelector scaleSelector = new ScaleSelector(scrollPane); - add(scaleSelector); - - - - // Stage selector - StageSelector stageSelector = new StageSelector(configuration); - add(stageSelector, ""); - - - - // Motor configuration selector - //// Motor configuration: - JLabel label = new JLabel(trans.get("RocketPanel.lbl.Motorcfg")); - label.setHorizontalAlignment(JLabel.RIGHT); - add(label, "growx, right"); - add(new JComboBox(new MotorConfigurationModel(configuration)), "wrap"); - - - - - - // Create slider and scroll pane - - DoubleModel theta = new DoubleModel(figure, "Rotation", - UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI); - UnitSelector us = new UnitSelector(theta, true); - us.setHorizontalAlignment(JLabel.CENTER); - add(us, "alignx 50%, growx"); - - // Add the rocket figure - add(scrollPane, "grow, spany 2, wmin 300lp, hmin 100lp, wrap"); - - - // Add rotation slider - // Minimum size to fit "360deg" - JLabel l = new JLabel("360" + Chars.DEGREE); - Dimension d = l.getPreferredSize(); - - add(new BasicSlider(theta.getSliderModel(0, 2 * Math.PI), JSlider.VERTICAL, true), - "ax 50%, wrap, width " + (d.width + 6) + "px:null:null, growy"); - - - //// <html>Click to select    Shift+click to select other    Double-click to edit    Click+drag to move - infoMessage = new JLabel(trans.get("RocketPanel.lbl.infoMessage")); - infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9)); - add(infoMessage, "skip, span, gapleft 25, wrap"); - - - addExtras(); - } - - - - public RocketFigure getFigure() { - return figure; - } - - public AerodynamicCalculator getAerodynamicCalculator() { - return aerodynamicCalculator; - } - - public Configuration getConfiguration() { - return configuration; - } - - /** - * Get the center of pressure figure element. - * - * @return center of pressure info - */ - public Caret getExtraCP() { - return extraCP; - } - - /** - * Get the center of gravity figure element. - * - * @return center of gravity info - */ - public Caret getExtraCG() { - return extraCG; - } - - /** - * Get the extra text figure element. - * - * @return extra text that contains info about the rocket design - */ - public RocketInfo getExtraText() { - return extraText; - } - - public void setSelectionModel(TreeSelectionModel m) { - if (selectionModel != null) { - selectionModel.removeTreeSelectionListener(this); - } - selectionModel = m; - selectionModel.addTreeSelectionListener(this); - valueChanged((TreeSelectionEvent) null); // updates FigureParameters - } - - - - /** - * Return the angle of attack used in CP calculation. NaN signifies the default value - * of zero. - * @return the angle of attack used, or NaN. - */ - public double getCPAOA() { - return cpAOA; - } - - /** - * Set the angle of attack to be used in CP calculation. A value of NaN signifies that - * the default AOA (zero) should be used. - * @param aoa the angle of attack to use, or NaN - */ - public void setCPAOA(double aoa) { - if (MathUtil.equals(aoa, cpAOA) || - (Double.isNaN(aoa) && Double.isNaN(cpAOA))) - return; - cpAOA = aoa; - updateExtras(); - figure.updateFigure(); - fireChangeEvent(); - } - - public double getCPTheta() { - return cpTheta; - } - - public void setCPTheta(double theta) { - if (MathUtil.equals(theta, cpTheta) || - (Double.isNaN(theta) && Double.isNaN(cpTheta))) - return; - cpTheta = theta; - if (!Double.isNaN(theta)) - figure.setRotation(theta); - updateExtras(); - figure.updateFigure(); - fireChangeEvent(); - } - - public double getCPMach() { - return cpMach; - } - - public void setCPMach(double mach) { - if (MathUtil.equals(mach, cpMach) || - (Double.isNaN(mach) && Double.isNaN(cpMach))) - return; - cpMach = mach; - updateExtras(); - figure.updateFigure(); - fireChangeEvent(); - } - - public double getCPRoll() { - return cpRoll; - } - - public void setCPRoll(double roll) { - if (MathUtil.equals(roll, cpRoll) || - (Double.isNaN(roll) && Double.isNaN(cpRoll))) - return; - cpRoll = roll; - updateExtras(); - figure.updateFigure(); - fireChangeEvent(); - } - - - - @Override - public void addChangeListener(EventListener listener) { - listeners.add(0, listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - listeners.remove(listener); - } - - protected void fireChangeEvent() { - EventObject e = new EventObject(this); - for (EventListener l : listeners) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(e); - } - } - } - - - - - /** - * Handle clicking on figure shapes. The functioning is the following: - * - * Get the components clicked. - * If no component is clicked, do nothing. - * If the currently selected component is in the set, keep it, - * unless the selector specified is pressed. If it is pressed, cycle to - * the next component. Otherwise select the first component in the list. - */ - public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK; - - private void handleMouseClick(MouseEvent event) { - if (event.getButton() != MouseEvent.BUTTON1) - return; - Point p0 = event.getPoint(); - Point p1 = scrollPane.getViewport().getViewPosition(); - int x = p0.x + p1.x; - int y = p0.y + p1.y; - - RocketComponent[] clicked = figure.getComponentsByPoint(x, y); - - // If no component is clicked, do nothing - if (clicked.length == 0) - return; - - // Check whether the currently selected component is in the clicked components. - TreePath path = selectionModel.getSelectionPath(); - if (path != null) { - RocketComponent current = (RocketComponent) path.getLastPathComponent(); - path = null; - for (int i = 0; i < clicked.length; i++) { - if (clicked[i] == current) { - if (event.isShiftDown() && (event.getClickCount() == 1)) { - path = ComponentTreeModel.makeTreePath(clicked[(i + 1) % clicked.length]); - } else { - path = ComponentTreeModel.makeTreePath(clicked[i]); - } - break; - } - } - } - - // Currently selected component not clicked - if (path == null) { - if (event.isShiftDown() && event.getClickCount() == 1 && clicked.length > 1) { - path = ComponentTreeModel.makeTreePath(clicked[1]); - } else { - path = ComponentTreeModel.makeTreePath(clicked[0]); - } - } - - // Set selection and check for double-click - selectionModel.setSelectionPath(path); - if (event.getClickCount() == 2) { - RocketComponent component = (RocketComponent) path.getLastPathComponent(); - - ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this), - document, component); - } - } - - - - - /** - * Updates the extra data included in the figure. Currently this includes - * the CP and CG carets. - */ - private WarningSet warnings = new WarningSet(); - - private void updateExtras() { - Coordinate cp, cg; - double cpx, cgx; - - // TODO: MEDIUM: User-definable conditions - FlightConditions conditions = new FlightConditions(configuration); - warnings.clear(); - - if (!Double.isNaN(cpMach)) { - conditions.setMach(cpMach); - extraText.setMach(cpMach); - } else { - conditions.setMach(Application.getPreferences().getDefaultMach()); - extraText.setMach(Application.getPreferences().getDefaultMach()); - } - - if (!Double.isNaN(cpAOA)) { - conditions.setAOA(cpAOA); - } else { - conditions.setAOA(0); - } - extraText.setAOA(cpAOA); - - if (!Double.isNaN(cpRoll)) { - conditions.setRollRate(cpRoll); - } else { - conditions.setRollRate(0); - } - - if (!Double.isNaN(cpTheta)) { - conditions.setTheta(cpTheta); - cp = aerodynamicCalculator.getCP(configuration, conditions, warnings); - } else { - cp = aerodynamicCalculator.getWorstCP(configuration, conditions, warnings); - } - extraText.setTheta(cpTheta); - - - cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); - // System.out.println("CG computed as "+cg+ " CP as "+cp); - - if (cp.weight > 0.000001) - cpx = cp.x; - else - cpx = Double.NaN; - - if (cg.weight > 0.000001) - cgx = cg.x; - else - cgx = Double.NaN; - - // Length bound is assumed to be tight - double length = 0, diameter = 0; - Collection<Coordinate> bounds = configuration.getBounds(); - if (!bounds.isEmpty()) { - double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; - for (Coordinate c : bounds) { - if (c.x < minX) - minX = c.x; - if (c.x > maxX) - maxX = c.x; - } - length = maxX - minX; - } - - for (RocketComponent c : configuration) { - if (c instanceof SymmetricComponent) { - double d1 = ((SymmetricComponent) c).getForeRadius() * 2; - double d2 = ((SymmetricComponent) c).getAftRadius() * 2; - diameter = MathUtil.max(diameter, d1, d2); - } - } - - extraText.setCG(cgx); - extraText.setCP(cpx); - extraText.setLength(length); - extraText.setDiameter(diameter); - extraText.setMass(cg.weight); - extraText.setWarnings(warnings); - - - if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) { - - // TODO: LOW: Y-coordinate and rotation - extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0); - extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0); - - } else { - - extraCP.setPosition(Double.NaN, Double.NaN); - extraCG.setPosition(Double.NaN, Double.NaN); - - } - - - //////// Flight simulation in background - - // Check whether to compute or not - if (!((SwingPreferences) Application.getPreferences()).computeFlightInBackground()) { - extraText.setFlightData(null); - extraText.setCalculatingData(false); - stopBackgroundSimulation(); - return; - } - - // Check whether data is already up to date - if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() && - flightDataMotorID == configuration.getMotorConfigurationID()) { - return; - } - - flightDataFunctionalID = configuration.getRocket().getFunctionalModID(); - flightDataMotorID = configuration.getMotorConfigurationID(); - - // Stop previous computation (if any) - stopBackgroundSimulation(); - - // Check that configuration has motors - if (!configuration.hasMotors()) { - extraText.setFlightData(FlightData.NaN_DATA); - extraText.setCalculatingData(false); - return; - } - - // Start calculation process - extraText.setCalculatingData(true); - - Rocket duplicate = (Rocket) configuration.getRocket().copy(); - Simulation simulation = ((SwingPreferences)Application.getPreferences()).getBackgroundSimulation(duplicate); - simulation.getOptions().setMotorConfigurationID( - configuration.getMotorConfigurationID()); - - backgroundSimulationWorker = new BackgroundSimulationWorker(simulation); - backgroundSimulationExecutor.execute(backgroundSimulationWorker); - } - - /** - * Cancels the current background simulation worker, if any. - */ - private void stopBackgroundSimulation() { - if (backgroundSimulationWorker != null) { - backgroundSimulationWorker.cancel(true); - backgroundSimulationWorker = null; - } - } - - - /** - * A SimulationWorker that simulates the rocket flight in the background and - * sets the results to the extra text when finished. The worker can be cancelled - * if necessary. - */ - private class BackgroundSimulationWorker extends SimulationWorker { - - public BackgroundSimulationWorker(Simulation sim) { - super(sim); - } - - @Override - protected FlightData doInBackground() { - - // Pause a little while to allow faster UI reaction - try { - Thread.sleep(300); - } catch (InterruptedException ignore) { - } - if (isCancelled() || backgroundSimulationWorker != this) - return null; - - return super.doInBackground(); - } - - @Override - protected void simulationDone() { - // Do nothing if cancelled - if (isCancelled() || backgroundSimulationWorker != this) - return; - - backgroundSimulationWorker = null; - extraText.setFlightData(simulation.getSimulatedData()); - extraText.setCalculatingData(false); - figure.repaint(); - } - - @Override - protected SimulationListener[] getExtraListeners() { - return new SimulationListener[] { - InterruptListener.INSTANCE, - ApogeeEndListener.INSTANCE }; - } - - @Override - protected void simulationInterrupted(Throwable t) { - // Do nothing on cancel, set N/A data otherwise - if (isCancelled() || backgroundSimulationWorker != this) // Double-check - return; - - backgroundSimulationWorker = null; - extraText.setFlightData(FlightData.NaN_DATA); - extraText.setCalculatingData(false); - figure.repaint(); - } - } - - - - /** - * Adds the extra data to the figure. Currently this includes the CP and CG carets. - */ - private void addExtras() { - figure.clearRelativeExtra(); - extraCG = new CGCaret(0, 0); - extraCP = new CPCaret(0, 0); - extraText = new RocketInfo(configuration); - updateExtras(); - figure.addRelativeExtra(extraCP); - figure.addRelativeExtra(extraCG); - figure.addAbsoluteExtra(extraText); - } - - - /** - * Updates the selection in the FigureParameters and repaints the figure. - * Ignores the event itself. - */ - @Override - public void valueChanged(TreeSelectionEvent e) { - TreePath[] paths = selectionModel.getSelectionPaths(); - if (paths == null) { - figure.setSelection(null); - return; - } - - RocketComponent[] components = new RocketComponent[paths.length]; - for (int i = 0; i < paths.length; i++) - components[i] = (RocketComponent) paths[i].getLastPathComponent(); - figure.setSelection(components); - } - - - - /** - * An <code>Action</code> that shows whether the figure type is the type - * given in the constructor. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - private class FigureTypeAction extends AbstractAction implements StateChangeListener { - private final int type; - - public FigureTypeAction(int type) { - this.type = type; - stateChanged(null); - figure.addChangeListener(this); - } - - @Override - public void actionPerformed(ActionEvent e) { - boolean state = (Boolean) getValue(Action.SELECTED_KEY); - if (state == true) { - // This view has been selected - figure.setType(type); - updateExtras(); - } - stateChanged(null); - } - - @Override - public void stateChanged(EventObject e) { - putValue(Action.SELECTED_KEY, figure.getType() == type); - } - } - -} diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java b/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java deleted file mode 100644 index 48440fe3..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - -import java.awt.Dimension; - -import net.sf.openrocket.util.ChangeSource; - - -public interface ScaleFigure extends ChangeSource { - - /** - * Extra scaling applied to the figure. The f***ing Java JRE doesn't know - * how to draw shapes when using very large scaling factors, so this must - * be manually applied to every single shape used. - * <p> - * The scaling factor used is divided by this value, and every coordinate used - * in the figures must be multiplied by this factor. - */ - public static final double EXTRA_SCALE = 1000; - - /** - * Shorthand for {@link #EXTRA_SCALE}. - */ - public static final double S = EXTRA_SCALE; - - - /** - * Set the scale level of the figure. A scale value of 1.0 indicates an original - * size when using the current DPI level. - * - * @param scale the scale level. - */ - public void setScaling(double scale); - - - /** - * Set the scale level so that the figure fits into the given bounds. - * - * @param bounds the bounds of the figure. - */ - public void setScaling(Dimension bounds); - - - /** - * Return the scale level of the figure. A scale value of 1.0 indicates an original - * size when using the current DPI level. - * - * @return the current scale level. - */ - public double getScaling(); - - - /** - * Return the scale of the figure on px/m. - * - * @return the current scale value. - */ - public double getAbsoluteScale(); - - - /** - * Return the pixel coordinates of the figure origin. - * - * @return the pixel coordinates of the figure origin. - */ - public Dimension getOrigin(); - - - /** - * Get the amount of blank space left around the figure. - * - * @return the amount of horizontal and vertical space left on both sides of the figure. - */ - public Dimension getBorderPixels(); - - /** - * Set the amount of blank space left around the figure. - * - * @param width the amount of horizontal space left on both sides of the figure. - * @param height the amount of vertical space left on both sides of the figure. - */ - public void setBorderPixels(int width, int height); -} diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java b/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java deleted file mode 100644 index 121dc0e0..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java +++ /dev/null @@ -1,397 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Rectangle; -import java.awt.RenderingHints; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.util.EventObject; - -import javax.swing.BorderFactory; -import javax.swing.JComponent; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JViewport; -import javax.swing.ScrollPaneConstants; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -import net.sf.openrocket.gui.adaptors.DoubleModel; -import net.sf.openrocket.gui.components.UnitSelector; -import net.sf.openrocket.unit.Tick; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.StateChangeListener; - - - -/** - * A scroll pane that holds a {@link ScaleFigure} and includes rulers that show - * natural units. The figure can be moved by dragging on the figure. - * <p> - * This class implements both <code>MouseListener</code> and - * <code>MouseMotionListener</code>. If subclasses require extra functionality - * (e.g. checking for clicks) then these methods may be overridden, and only unhandled - * events passed to this class. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ScaleScrollPane extends JScrollPane - implements MouseListener, MouseMotionListener { - - public static final int RULER_SIZE = 20; - public static final int MINOR_TICKS = 3; - public static final int MAJOR_TICKS = 30; - - - private JComponent component; - private ScaleFigure figure; - private JViewport viewport; - - private DoubleModel rulerUnit; - private Ruler horizontalRuler; - private Ruler verticalRuler; - - private final boolean allowFit; - - private boolean fit = false; - - - /** - * Create a scale scroll pane that allows fitting. - * - * @param component the component to contain (must implement ScaleFigure) - */ - public ScaleScrollPane(JComponent component) { - this(component, true); - } - - /** - * Create a scale scroll pane. - * - * @param component the component to contain (must implement ScaleFigure) - * @param allowFit whether automatic fitting of the figure is allowed - */ - public ScaleScrollPane(JComponent component, boolean allowFit) { - super(component); - - if (!(component instanceof ScaleFigure)) { - throw new IllegalArgumentException("component must implement ScaleFigure"); - } - - this.component = component; - this.figure = (ScaleFigure) component; - this.allowFit = allowFit; - - - rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH); - rulerUnit.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - ScaleScrollPane.this.component.repaint(); - } - }); - horizontalRuler = new Ruler(Ruler.HORIZONTAL); - verticalRuler = new Ruler(Ruler.VERTICAL); - this.setColumnHeaderView(horizontalRuler); - this.setRowHeaderView(verticalRuler); - - UnitSelector selector = new UnitSelector(rulerUnit); - selector.setFont(new Font("SansSerif", Font.PLAIN, 8)); - this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector); - this.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new JPanel()); - this.setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel()); - this.setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel()); - - this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY)); - - - viewport = this.getViewport(); - viewport.addMouseListener(this); - viewport.addMouseMotionListener(this); - - figure.addChangeListener(new StateChangeListener() { - @Override - public void stateChanged(EventObject e) { - horizontalRuler.updateSize(); - verticalRuler.updateSize(); - if (fit) { - setFitting(true); - } - } - }); - - viewport.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - if (fit) { - setFitting(true); - } - } - }); - - } - - public ScaleFigure getFigure() { - return figure; - } - - - /** - * Return whether automatic fitting of the figure is allowed. - */ - public boolean isFittingAllowed() { - return allowFit; - } - - /** - * Return whether the figure is currently automatically fitted within the component bounds. - */ - public boolean isFitting() { - return fit; - } - - /** - * Set whether the figure is automatically fitted within the component bounds. - * - * @throws BugException if automatic fitting is disallowed and <code>fit</code> is <code>true</code> - */ - public void setFitting(boolean fit) { - if (fit && !allowFit) { - throw new BugException("Attempting to fit figure not allowing fit."); - } - this.fit = fit; - if (fit) { - setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); - setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER); - validate(); - Dimension view = viewport.getExtentSize(); - figure.setScaling(view); - } else { - setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED); - } - } - - - - public double getScaling() { - return figure.getScaling(); - } - - public double getScale() { - return figure.getAbsoluteScale(); - } - - public void setScaling(double scale) { - if (fit) { - setFitting(false); - } - figure.setScaling(scale); - horizontalRuler.repaint(); - verticalRuler.repaint(); - } - - - public Unit getCurrentUnit() { - return rulerUnit.getCurrentUnit(); - } - - - //////////////// Mouse handlers //////////////// - - - private int dragStartX = 0; - private int dragStartY = 0; - private Rectangle dragRectangle = null; - - @Override - public void mousePressed(MouseEvent e) { - dragStartX = e.getX(); - dragStartY = e.getY(); - dragRectangle = viewport.getViewRect(); - } - - @Override - public void mouseReleased(MouseEvent e) { - dragRectangle = null; - } - - @Override - public void mouseDragged(MouseEvent e) { - if (dragRectangle == null) { - return; - } - - dragRectangle.setLocation(dragStartX - e.getX(), dragStartY - e.getY()); - - dragStartX = e.getX(); - dragStartY = e.getY(); - - viewport.scrollRectToVisible(dragRectangle); - } - - @Override - public void mouseClicked(MouseEvent e) { - } - - @Override - public void mouseEntered(MouseEvent e) { - } - - @Override - public void mouseExited(MouseEvent e) { - } - - @Override - public void mouseMoved(MouseEvent e) { - } - - - - //////////////// The view port rulers //////////////// - - - private class Ruler extends JComponent { - public static final int HORIZONTAL = 0; - public static final int VERTICAL = 1; - - private final int orientation; - - public Ruler(int orientation) { - this.orientation = orientation; - updateSize(); - - rulerUnit.addChangeListener(new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - Ruler.this.repaint(); - } - }); - } - - - public void updateSize() { - Dimension d = component.getPreferredSize(); - if (orientation == HORIZONTAL) { - setPreferredSize(new Dimension(d.width + 10, RULER_SIZE)); - } else { - setPreferredSize(new Dimension(RULER_SIZE, d.height + 10)); - } - revalidate(); - repaint(); - } - - private double fromPx(int px) { - Dimension origin = figure.getOrigin(); - if (orientation == HORIZONTAL) { - px -= origin.width; - } else { - // px = -(px - origin.height); - px -= origin.height; - } - return px / figure.getAbsoluteScale(); - } - - private int toPx(double l) { - Dimension origin = figure.getOrigin(); - int px = (int) (l * figure.getAbsoluteScale() + 0.5); - if (orientation == HORIZONTAL) { - px += origin.width; - } else { - px = px + origin.height; - // px += origin.height; - } - return px; - } - - - @Override - protected void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D) g; - - Rectangle area = g2.getClipBounds(); - - // Fill area with background color - g2.setColor(getBackground()); - g2.fillRect(area.x, area.y, area.width, area.height + 100); - - - int startpx, endpx; - if (orientation == HORIZONTAL) { - startpx = area.x; - endpx = area.x + area.width; - } else { - startpx = area.y; - endpx = area.y + area.height; - } - - Unit unit = rulerUnit.getCurrentUnit(); - double start, end, minor, major; - start = fromPx(startpx); - end = fromPx(endpx); - minor = MINOR_TICKS / figure.getAbsoluteScale(); - major = MAJOR_TICKS / figure.getAbsoluteScale(); - - Tick[] ticks = unit.getTicks(start, end, minor, major); - - - // Set color & hints - g2.setColor(Color.BLACK); - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, - RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, - RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, - RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - - for (Tick t : ticks) { - int position = toPx(t.value); - drawTick(g2, position, t); - } - } - - private void drawTick(Graphics g, int position, Tick t) { - int length; - String str = null; - if (t.major) { - length = RULER_SIZE / 2; - } else { - if (t.notable) - length = RULER_SIZE / 3; - else - length = RULER_SIZE / 6; - } - - // Set font - if (t.major) { - str = rulerUnit.getCurrentUnit().toString(t.value); - if (t.notable) - g.setFont(new Font("SansSerif", Font.BOLD, 9)); - else - g.setFont(new Font("SansSerif", Font.PLAIN, 9)); - } - - // Draw tick & text - if (orientation == HORIZONTAL) { - g.drawLine(position, RULER_SIZE - length, position, RULER_SIZE); - if (str != null) - g.drawString(str, position, RULER_SIZE - length - 1); - } else { - g.drawLine(RULER_SIZE - length, position, RULER_SIZE, position); - if (str != null) - g.drawString(str, 1, position - 1); - } - } - } -} diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java b/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java deleted file mode 100644 index 1e966a05..00000000 --- a/src/net/sf/openrocket/gui/scalefigure/ScaleSelector.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.sf.openrocket.gui.scalefigure; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.text.DecimalFormat; -import java.util.Arrays; -import java.util.EventObject; - -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.gui.util.Icons; -import net.sf.openrocket.util.StateChangeListener; - -public class ScaleSelector extends JPanel { - - // Ready zoom settings - private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%"); - - private static final double[] ZOOM_LEVELS = { 0.15, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0 }; - private static final String ZOOM_FIT = "Fit"; - private static final String[] ZOOM_SETTINGS; - static { - ZOOM_SETTINGS = new String[ZOOM_LEVELS.length+1]; - for (int i=0; i<ZOOM_LEVELS.length; i++) - ZOOM_SETTINGS[i] = PERCENT_FORMAT.format(ZOOM_LEVELS[i]); - ZOOM_SETTINGS[ZOOM_SETTINGS.length-1] = ZOOM_FIT; - } - - - private final ScaleScrollPane scrollPane; - private JComboBox zoomSelector; - - - public ScaleSelector(ScaleScrollPane scroll) { - super(new MigLayout()); - - this.scrollPane = scroll; - - // Zoom out button - JButton button = new JButton(Icons.ZOOM_OUT); - button.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - double scale = scrollPane.getScaling(); - scale = getPreviousScale(scale); - scrollPane.setScaling(scale); - } - }); - add(button, "gap"); - - // Zoom level selector - String[] settings = ZOOM_SETTINGS; - if (!scrollPane.isFittingAllowed()) { - settings = Arrays.copyOf(settings, settings.length-1); - } - - zoomSelector = new JComboBox(settings); - zoomSelector.setEditable(true); - setZoomText(); - zoomSelector.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - try { - String text = (String)zoomSelector.getSelectedItem(); - text = text.replaceAll("%", "").trim(); - - if (text.toLowerCase().startsWith(ZOOM_FIT.toLowerCase()) && - scrollPane.isFittingAllowed()) { - scrollPane.setFitting(true); - setZoomText(); - return; - } - - double n = Double.parseDouble(text); - n /= 100; - if (n <= 0.005) - n = 0.005; - - scrollPane.setScaling(n); - setZoomText(); - } catch (NumberFormatException ignore) { - } finally { - setZoomText(); - } - } - }); - scrollPane.getFigure().addChangeListener(new StateChangeListener() { - @Override - public void stateChanged(EventObject e) { - setZoomText(); - } - }); - add(zoomSelector,"gap rel"); - - - // Zoom in button - button = new JButton(Icons.ZOOM_IN); - button.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent e) { - double scale = scrollPane.getScaling(); - scale = getNextScale(scale); - scrollPane.setScaling(scale); - } - }); - add(button,"gapleft rel"); - - } - - - - private void setZoomText() { - String text; - double zoom = scrollPane.getScaling(); - text = PERCENT_FORMAT.format(zoom); - if (scrollPane.isFitting()) { - text = "Fit ("+text+")"; - } - if (!text.equals(zoomSelector.getSelectedItem())) - zoomSelector.setSelectedItem(text); - } - - - - private double getPreviousScale(double scale) { - int i; - for (i=0; i<ZOOM_LEVELS.length-1; i++) { - if (scale > ZOOM_LEVELS[i]+0.05 && scale < ZOOM_LEVELS[i+1]+0.05) - return ZOOM_LEVELS[i]; - } - if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) { - // scale is large, drop to next lowest full 100% - scale = Math.ceil(scale-1.05); - return Math.max(scale, ZOOM_LEVELS[i]); - } - // scale is small - return scale/1.5; - } - - - private double getNextScale(double scale) { - int i; - for (i=0; i<ZOOM_LEVELS.length-1; i++) { - if (scale > ZOOM_LEVELS[i]-0.05 && scale < ZOOM_LEVELS[i+1]-0.05) - return ZOOM_LEVELS[i+1]; - } - if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) { - // scale is large, give next full 100% - scale = Math.floor(scale+1.05); - return scale; - } - return scale*1.5; - } - -} diff --git a/src/net/sf/openrocket/gui/util/ColorConversion.java b/src/net/sf/openrocket/gui/util/ColorConversion.java deleted file mode 100644 index e9a2eae4..00000000 --- a/src/net/sf/openrocket/gui/util/ColorConversion.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.sf.openrocket.gui.util; - -public class ColorConversion { - - public static java.awt.Color toAwtColor( net.sf.openrocket.util.Color c ) { - if ( c == null ) { - return null; - } - return new java.awt.Color(c.getRed(),c.getGreen(),c.getBlue(),c.getAlpha()); - } - - public static net.sf.openrocket.util.Color fromAwtColor( java.awt.Color c ) { - if ( c == null ) { - return null; - } - return new net.sf.openrocket.util.Color( c.getRed(), c.getGreen(), c.getBlue(), c.getAlpha()); - } -} diff --git a/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitor.java b/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitor.java deleted file mode 100644 index b6aa6ea3..00000000 --- a/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitor.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.awt.Component; - -import javax.swing.ProgressMonitor; -import javax.swing.SwingUtilities; - - -/** - * A thread-safe <code>ProgressMonitor</code>. This class may be instantiated - * and the method {@link #setProgress(int)} called safely from any thread. - * <p> - * Why the FSCK&!#&% isn't the default API version thread-safe?!?! - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ConcurrentProgressMonitor extends ProgressMonitor { - - public ConcurrentProgressMonitor(Component parentComponent, Object message, - String note, int min, int max) { - super(parentComponent, message, note, min, max); - } - - @Override - public void setProgress(final int nv) { - - if (SwingUtilities.isEventDispatchThread()) { - super.setProgress(nv); - } else { - - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - ConcurrentProgressMonitor.super.setProgress(nv); - } - - }); - } - } - - - @Override - public void close() { - if (SwingUtilities.isEventDispatchThread()) { - super.close(); - } else { - - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - ConcurrentProgressMonitor.super.close(); - } - - }); - } - } - - -} diff --git a/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitorInputStream.java b/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitorInputStream.java deleted file mode 100644 index ccc0d799..00000000 --- a/src/net/sf/openrocket/gui/util/ConcurrentProgressMonitorInputStream.java +++ /dev/null @@ -1,144 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.awt.Component; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; - - - -/** - * A functional equivalent of <code>ProgressMonitorInputStream</code> which - * uses {@link ConcurrentProgressMonitor} and leaves the progress dialog open - * to be manually closed later on. - */ - -public class ConcurrentProgressMonitorInputStream extends FilterInputStream { - private ConcurrentProgressMonitor monitor; - private int nread = 0; - private int size = 0; - - - /** - * Constructs an object to monitor the progress of an input stream. - * - * @param message Descriptive text to be placed in the dialog box - * if one is popped up. - * @param parentComponent The component triggering the operation - * being monitored. - * @param in The input stream to be monitored. - */ - public ConcurrentProgressMonitorInputStream(Component parentComponent, - Object message, InputStream in) { - super(in); - try { - size = in.available(); - } catch (IOException ioe) { - size = 0; - } - monitor = new ConcurrentProgressMonitor(parentComponent, message, null, 0, - size + 1); - } - - - /** - * Get the ProgressMonitor object being used by this stream. Normally - * this isn't needed unless you want to do something like change the - * descriptive text partway through reading the file. - * @return the ProgressMonitor object used by this object - */ - public ConcurrentProgressMonitor getProgressMonitor() { - return monitor; - } - - - /** - * Overrides <code>FilterInputStream.read</code> - * to update the progress monitor after the read. - */ - @Override - public int read() throws IOException { - int c = in.read(); - if (c >= 0) - monitor.setProgress(++nread); - if (monitor.isCanceled()) { - InterruptedIOException exc = new InterruptedIOException("progress"); - exc.bytesTransferred = nread; - throw exc; - } - return c; - } - - - /** - * Overrides <code>FilterInputStream.read</code> - * to update the progress monitor after the read. - */ - @Override - public int read(byte b[]) throws IOException { - int nr = in.read(b); - if (nr > 0) - monitor.setProgress(nread += nr); - if (monitor.isCanceled()) { - InterruptedIOException exc = new InterruptedIOException("progress"); - exc.bytesTransferred = nread; - throw exc; - } - return nr; - } - - - /** - * Overrides <code>FilterInputStream.read</code> - * to update the progress monitor after the read. - */ - @Override - public int read(byte b[], int off, int len) throws IOException { - int nr = in.read(b, off, len); - if (nr > 0) - monitor.setProgress(nread += nr); - if (monitor.isCanceled()) { - InterruptedIOException exc = new InterruptedIOException("progress"); - exc.bytesTransferred = nread; - throw exc; - } - return nr; - } - - - /** - * Overrides <code>FilterInputStream.skip</code> - * to update the progress monitor after the skip. - */ - @Override - public long skip(long n) throws IOException { - long nr = in.skip(n); - if (nr > 0) - monitor.setProgress(nread += nr); - return nr; - } - - - /** - * Overrides <code>FilterInputStream.close</code> - * to close the progress monitor as well as the stream. - */ - @Override - public void close() throws IOException { - in.close(); - monitor.close(); - } - - - /** - * Overrides <code>FilterInputStream.reset</code> - * to reset the progress monitor as well as the stream. - */ - @Override - public synchronized void reset() throws IOException { - in.reset(); - nread = size - in.available(); - monitor.setProgress(nread); - } -} diff --git a/src/net/sf/openrocket/gui/util/FileHelper.java b/src/net/sf/openrocket/gui/util/FileHelper.java deleted file mode 100644 index 8c496393..00000000 --- a/src/net/sf/openrocket/gui/util/FileHelper.java +++ /dev/null @@ -1,120 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.awt.Component; -import java.io.File; -import java.io.IOException; - -import javax.swing.JOptionPane; -import javax.swing.filechooser.FileFilter; - -import net.sf.openrocket.l10n.L10N; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * Helper methods related to user-initiated file manipulation. - * <p> - * These methods log the necessary information to the debug log. -* - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class FileHelper { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - // TODO: HIGH: Rename translation keys - - /** File filter for any rocket designs (*.ork, *.rkt) */ - public static final FileFilter ALL_DESIGNS_FILTER = - new SimpleFileFilter(trans.get("BasicFrame.SimpleFileFilter1"), - ".ork", ".ork.gz", ".rkt", ".rkt.gz"); - - /** File filter for OpenRocket designs (*.ork) */ - public static final FileFilter OPENROCKET_DESIGN_FILTER = - new SimpleFileFilter(trans.get("BasicFrame.SimpleFileFilter2"), ".ork", ".ork.gz"); - - /** File filter for RockSim designs (*.rkt) */ - public static final FileFilter ROCKSIM_DESIGN_FILTER = - new SimpleFileFilter(trans.get("BasicFrame.SimpleFileFilter3"), ".rkt", ".rkt.gz"); - - /** File filter for PDF files (*.pdf) */ - public static final FileFilter PDF_FILTER = - new SimpleFileFilter(trans.get("filetypes.pdf"), ".pdf"); - - /** File filter for CSV files (*.csv) */ - public static final FileFilter CSV_FILE_FILTER = - new SimpleFileFilter(trans.get("SimExpPan.desc"), ".csv"); - - - - - - private FileHelper() { - // Prevent instantiation - } - - /** - * Ensure that the provided file has a file extension. If the file does not have - * any extension, append the provided extension to it. - * - * @param original the original file - * @param extension the extension to append if none exists (without preceding dot) - * @return the resulting filen - */ - public static File ensureExtension(File original, String extension) { - - if (original.getName().indexOf('.') < 0) { - log.debug(1, "File name does not contain extension, adding '" + extension + "'"); - String name = original.getAbsolutePath(); - name = name + "." + extension; - return new File(name); - } - - return original; - } - - - /** - * Confirm that it is allowed to write to a file. If the file exists, - * a confirmation dialog will be presented to the user to ensure overwriting is ok. - * - * @param file the file that is going to be written. - * @param parent the parent component for the dialog. - * @return <code>true</code> to write, <code>false</code> to abort. - */ - public static boolean confirmWrite(File file, Component parent) { - if (file.exists()) { - log.info(1, "File " + file + " exists, confirming overwrite from user"); - int result = JOptionPane.showConfirmDialog(parent, - L10N.replace(trans.get("error.fileExists.desc"), "{filename}", file.getName()), - trans.get("error.fileExists.title"), JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE); - if (result != JOptionPane.YES_OPTION) { - log.user(1, "User decided not to overwrite the file"); - return false; - } - log.user(1, "User decided to overwrite the file"); - } - return true; - } - - - /** - * Display an error message to the user that writing a file failed. - * - * @param e the I/O exception that caused the error. - * @param parent the parent component for the dialog. - */ - public static void errorWriting(IOException e, Component parent) { - - log.warn(1, "Error writing to file", e); - JOptionPane.showMessageDialog(parent, - new Object[] { - trans.get("error.writing.desc"), - e.getLocalizedMessage() - }, trans.get("error.writing.title"), JOptionPane.ERROR_MESSAGE); - - } - -} diff --git a/src/net/sf/openrocket/gui/util/GUIUtil.java b/src/net/sf/openrocket/gui/util/GUIUtil.java deleted file mode 100644 index 4ee74ba4..00000000 --- a/src/net/sf/openrocket/gui/util/GUIUtil.java +++ /dev/null @@ -1,588 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.awt.Component; -import java.awt.Container; -import java.awt.Dimension; -import java.awt.Font; -import java.awt.Image; -import java.awt.KeyboardFocusManager; -import java.awt.Point; -import java.awt.Toolkit; -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.awt.event.FocusListener; -import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.WindowAdapter; -import java.awt.event.WindowEvent; -import java.beans.PropertyChangeListener; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.imageio.ImageIO; -import javax.swing.AbstractAction; -import javax.swing.AbstractButton; -import javax.swing.Action; -import javax.swing.BoundedRangeModel; -import javax.swing.ComboBoxModel; -import javax.swing.DefaultBoundedRangeModel; -import javax.swing.DefaultComboBoxModel; -import javax.swing.DefaultListSelectionModel; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JComponent; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JLabel; -import javax.swing.JRootPane; -import javax.swing.JSlider; -import javax.swing.JSpinner; -import javax.swing.JTable; -import javax.swing.JTree; -import javax.swing.KeyStroke; -import javax.swing.ListSelectionModel; -import javax.swing.LookAndFeel; -import javax.swing.RootPaneContainer; -import javax.swing.SpinnerModel; -import javax.swing.SpinnerNumberModel; -import javax.swing.SwingUtilities; -import javax.swing.UIManager; -import javax.swing.border.TitledBorder; -import javax.swing.event.ChangeListener; -import javax.swing.table.AbstractTableModel; -import javax.swing.table.DefaultTableColumnModel; -import javax.swing.table.DefaultTableModel; -import javax.swing.table.TableColumnModel; -import javax.swing.table.TableModel; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.DefaultTreeSelectionModel; -import javax.swing.tree.TreeModel; -import javax.swing.tree.TreeSelectionModel; - -import net.sf.openrocket.gui.Resettable; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Invalidatable; -import net.sf.openrocket.util.MemoryManagement; - -public class GUIUtil { - private static final LogHelper log = Application.getLogger(); - - private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); - private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING"; - - private static final List<Image> images = new ArrayList<Image>(); - static { - loadImage("pix/icon/icon-256.png"); - loadImage("pix/icon/icon-064.png"); - loadImage("pix/icon/icon-048.png"); - loadImage("pix/icon/icon-032.png"); - loadImage("pix/icon/icon-016.png"); - } - - private static void loadImage(String file) { - InputStream is; - - is = ClassLoader.getSystemResourceAsStream(file); - if (is == null) - return; - - try { - Image image = ImageIO.read(is); - images.add(image); - } catch (IOException ignore) { - ignore.printStackTrace(); - } - } - - /** - * Return the DPI setting of the monitor. This is either the setting provided - * by the system or a user-specified DPI setting. - * - * @return the DPI setting to use. - */ - public static double getDPI() { - int dpi = Application.getPreferences().getInt("DPI", 0); // Tenths of a dpi - - if (dpi < 10) { - dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10; - } - if (dpi < 10) - dpi = 960; - - return (dpi) / 10.0; - } - - - - - /** - * Set suitable options for a single-use disposable dialog. This includes - * setting ESC to close the dialog, adding the appropriate window icons and - * setting the location based on the platform. If defaultButton is provided, - * it is set to the default button action. - * <p> - * The default button must be already attached to the dialog. - * - * @param dialog the dialog. - * @param defaultButton the default button of the dialog, or <code>null</code>. - */ - public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) { - installEscapeCloseOperation(dialog); - setWindowIcons(dialog); - addModelNullingListener(dialog); - dialog.setLocationByPlatform(true); - dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); - dialog.pack(); - if (defaultButton != null) { - setDefaultButton(defaultButton); - } - } - - - - /** - * Add the correct action to close a JDialog when the ESC key is pressed. - * The dialog is closed by sending is a WINDOW_CLOSING event. - * - * @param dialog the dialog for which to install the action. - */ - public static void installEscapeCloseOperation(final JDialog dialog) { - Action dispatchClosing = new AbstractAction() { - @Override - public void actionPerformed(ActionEvent event) { - log.user("Closing dialog " + dialog); - dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING)); - } - }; - JRootPane root = dialog.getRootPane(); - root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY); - root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing); - } - - - /** - * Set the given button as the default button of the frame/dialog it is in. The button - * must be first attached to the window component hierarchy. - * - * @param button the button to set as the default button. - */ - public static void setDefaultButton(JButton button) { - Window w = SwingUtilities.windowForComponent(button); - if (w == null) { - throw new IllegalArgumentException("Attach button to a window first."); - } - if (!(w instanceof RootPaneContainer)) { - throw new IllegalArgumentException("Button not attached to RootPaneContainer, w=" + w); - } - ((RootPaneContainer) w).getRootPane().setDefaultButton(button); - } - - - - /** - * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of - * the components. This is necessary for e.g. <code>JTextArea</code>. - * - * @param c the component to modify - */ - public static void setTabToFocusing(Component c) { - Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB"))); - c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes); - strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB"))); - c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes); - } - - - - /** - * Set the OpenRocket icons to the window icons. - * - * @param window the window to set. - */ - public static void setWindowIcons(Window window) { - window.setIconImages(images); - } - - /** - * Add a listener to the provided window that will call {@link #setNullModels(Component)} - * on the window once it is closed. This method may only be used on single-use - * windows and dialogs, that will never be shown again once closed! - * - * @param window the window to add the listener to. - */ - public static void addModelNullingListener(final Window window) { - window.addWindowListener(new WindowAdapter() { - @Override - public void windowClosed(WindowEvent e) { - log.debug("Clearing all models of window " + window); - setNullModels(window); - MemoryManagement.collectable(window); - } - }); - } - - - - /** - * Set the best available look-and-feel into use. - */ - public static void setBestLAF() { - /* - * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used - * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few - * other alternatives. - */ - try { - // Set system L&F - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - - // Check whether we have an ugly L&F - LookAndFeel laf = UIManager.getLookAndFeel(); - if (laf == null || - laf.getName().matches(".*[mM][oO][tT][iI][fF].*") || - laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) { - - // Search for better LAF - UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels(); - String lafNames[] = { - ".*[gG][tT][kK].*", - ".*[wW][iI][nN].*", - ".*[mM][aA][cC].*", - ".*[aA][qQ][uU][aA].*", - ".*[nN][iI][mM][bB].*" - }; - - lf: for (String lafName : lafNames) { - for (UIManager.LookAndFeelInfo l : info) { - if (l.getName().matches(lafName)) { - UIManager.setLookAndFeel(l.getClassName()); - break lf; - } - } - } - } - } catch (Exception e) { - log.warn("Error setting LAF: " + e); - } - } - - - /** - * Changes the size of the font of the specified component by the given amount. - * - * @param component the component for which to change the font - * @param size the change in the font size - */ - public static void changeFontSize(JComponent component, float size) { - Font font = component.getFont(); - font = font.deriveFont(font.getSize2D() + size); - component.setFont(font); - } - - - - /** - * Automatically remember the size of a window. This stores the window size in the user - * preferences when resizing/maximizing the window and sets the state on the first call. - */ - public static void rememberWindowSize(final Window window) { - window.addComponentListener(new ComponentAdapter() { - @Override - public void componentResized(ComponentEvent e) { - log.debug("Storing size of " + window.getClass().getName() + ": " + window.getSize()); - ((SwingPreferences) Application.getPreferences()).setWindowSize(window.getClass(), window.getSize()); - if (window instanceof JFrame) { - if ((((JFrame) window).getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) { - log.debug("Storing maximized state of " + window.getClass().getName()); - ((SwingPreferences) Application.getPreferences()).setWindowMaximized(window.getClass()); - } - } - } - }); - - if (((SwingPreferences) Application.getPreferences()).isWindowMaximized(window.getClass())) { - if (window instanceof JFrame) { - ((JFrame) window).setExtendedState(JFrame.MAXIMIZED_BOTH); - } - } else { - Dimension dim = ((SwingPreferences) Application.getPreferences()).getWindowSize(window.getClass()); - if (dim != null) { - window.setSize(dim); - } - } - } - - - /** - * Automatically remember the position of a window. The position is stored in the user preferences - * every time the window is moved and set from there when first calling this method. - */ - public static void rememberWindowPosition(final Window window) { - window.addComponentListener(new ComponentAdapter() { - @Override - public void componentMoved(ComponentEvent e) { - ((SwingPreferences) Application.getPreferences()).setWindowPosition(window.getClass(), window.getLocation()); - } - }); - - // Set window position according to preferences, and set prefs when moving - Point position = ((SwingPreferences) Application.getPreferences()).getWindowPosition(window.getClass()); - if (position != null) { - window.setLocationByPlatform(false); - window.setLocation(position); - } - } - - - /** - * Changes the style of the font of the specified border. - * - * @param border the component for which to change the font - * @param style the change in the font style - */ - public static void changeFontStyle(TitledBorder border, int style) { - /* - * The fix of JRE bug #4129681 causes a TitledBorder occasionally to - * return a null font. We try to work around the issue by detecting it - * and reverting to the font of a JLabel instead. - */ - Font font = border.getTitleFont(); - if (font == null) { - log.error("Border font is null, reverting to JLabel font"); - font = new JLabel().getFont(); - if (font == null) { - log.error("JLabel font is null, not modifying font"); - return; - } - } - font = font.deriveFont(style); - if (font == null) { - throw new BugException("Derived font is null"); - } - border.setTitleFont(font); - } - - - - /** - * Traverses recursively the component tree, and sets all applicable component - * models to null, so as to remove the listener connections. After calling this - * method the component hierarchy should no longed be used. - * <p> - * All components that use custom models should be added to this method, as - * there exists no standard way of removing the model from a component. - * - * @param c the component (<code>null</code> is ok) - */ - public static void setNullModels(Component c) { - if (c == null) - return; - - // Remove various listeners - for (ComponentListener l : c.getComponentListeners()) { - c.removeComponentListener(l); - } - for (FocusListener l : c.getFocusListeners()) { - c.removeFocusListener(l); - } - for (MouseListener l : c.getMouseListeners()) { - c.removeMouseListener(l); - } - for (PropertyChangeListener l : c.getPropertyChangeListeners()) { - c.removePropertyChangeListener(l); - } - for (PropertyChangeListener l : c.getPropertyChangeListeners("model")) { - c.removePropertyChangeListener("model", l); - } - for (PropertyChangeListener l : c.getPropertyChangeListeners("action")) { - c.removePropertyChangeListener("action", l); - } - - // Remove models for known components - // Why the FSCK must this be so hard?!?!? - - if (c instanceof JSpinner) { - - JSpinner spinner = (JSpinner) c; - for (ChangeListener l : spinner.getChangeListeners()) { - spinner.removeChangeListener(l); - } - SpinnerModel model = spinner.getModel(); - spinner.setModel(new SpinnerNumberModel()); - if (model instanceof Invalidatable) { - ((Invalidatable) model).invalidate(); - } - - } else if (c instanceof JSlider) { - - JSlider slider = (JSlider) c; - for (ChangeListener l : slider.getChangeListeners()) { - slider.removeChangeListener(l); - } - BoundedRangeModel model = slider.getModel(); - slider.setModel(new DefaultBoundedRangeModel()); - if (model instanceof Invalidatable) { - ((Invalidatable) model).invalidate(); - } - - } else if (c instanceof JComboBox) { - - JComboBox combo = (JComboBox) c; - for (ActionListener l : combo.getActionListeners()) { - combo.removeActionListener(l); - } - ComboBoxModel model = combo.getModel(); - combo.setModel(new DefaultComboBoxModel()); - if (model instanceof Invalidatable) { - ((Invalidatable) model).invalidate(); - } - - } else if (c instanceof AbstractButton) { - - AbstractButton button = (AbstractButton) c; - for (ActionListener l : button.getActionListeners()) { - button.removeActionListener(l); - } - Action model = button.getAction(); - button.setAction(new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - } - }); - if (model instanceof Invalidatable) { - ((Invalidatable) model).invalidate(); - } - - } else if (c instanceof JTable) { - - JTable table = (JTable) c; - TableModel model1 = table.getModel(); - table.setModel(new DefaultTableModel()); - if (model1 instanceof Invalidatable) { - ((Invalidatable) model1).invalidate(); - } - - TableColumnModel model2 = table.getColumnModel(); - table.setColumnModel(new DefaultTableColumnModel()); - if (model2 instanceof Invalidatable) { - ((Invalidatable) model2).invalidate(); - } - - ListSelectionModel model3 = table.getSelectionModel(); - table.setSelectionModel(new DefaultListSelectionModel()); - if (model3 instanceof Invalidatable) { - ((Invalidatable) model3).invalidate(); - } - - } else if (c instanceof JTree) { - - JTree tree = (JTree) c; - TreeModel model1 = tree.getModel(); - tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode())); - if (model1 instanceof Invalidatable) { - ((Invalidatable) model1).invalidate(); - } - - TreeSelectionModel model2 = tree.getSelectionModel(); - tree.setSelectionModel(new DefaultTreeSelectionModel()); - if (model2 instanceof Invalidatable) { - ((Invalidatable) model2).invalidate(); - } - - } else if (c instanceof Resettable) { - - ((Resettable) c).resetModel(); - - } - - // Recurse the component - if (c instanceof Container) { - Component[] cs = ((Container) c).getComponents(); - for (Component sub : cs) - setNullModels(sub); - } - - } - - - - /** - * A mouse listener that toggles the state of a boolean value in a table model - * when clicked on another column of the table. - * <p> - * NOTE: If the table model does not extend AbstractTableModel, the model must - * fire a change event (which in normal table usage is not necessary). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public static class BooleanTableClickListener extends MouseAdapter { - - private final JTable table; - private final int clickColumn; - private final int booleanColumn; - - - public BooleanTableClickListener(JTable table) { - this(table, 1, 0); - } - - - public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) { - this.table = table; - this.clickColumn = clickColumn; - this.booleanColumn = booleanColumn; - } - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getButton() != MouseEvent.BUTTON1) - return; - - Point p = e.getPoint(); - int col = table.columnAtPoint(p); - if (col < 0) - return; - col = table.convertColumnIndexToModel(col); - if (col != clickColumn) - return; - - int row = table.rowAtPoint(p); - if (row < 0) - return; - row = table.convertRowIndexToModel(row); - if (row < 0) - return; - - TableModel model = table.getModel(); - Object value = model.getValueAt(row, booleanColumn); - - if (!(value instanceof Boolean)) { - throw new IllegalStateException("Table value at row=" + row + " col=" + - booleanColumn + " is not a Boolean, value=" + value); - } - - Boolean b = (Boolean) value; - b = !b; - model.setValueAt(b, row, booleanColumn); - if (model instanceof AbstractTableModel) { - ((AbstractTableModel) model).fireTableCellUpdated(row, booleanColumn); - } - } - - } - -} diff --git a/src/net/sf/openrocket/gui/util/Icons.java b/src/net/sf/openrocket/gui/util/Icons.java deleted file mode 100644 index 5c76bf24..00000000 --- a/src/net/sf/openrocket/gui/util/Icons.java +++ /dev/null @@ -1,101 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.net.URL; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import javax.swing.Icon; -import javax.swing.ImageIcon; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - - -public class Icons { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - static { - log.debug("Starting to load icons"); - } - - /** - * Icons used for showing the status of a simulation (up to date, out of date, etc). - */ - public static final Map<Simulation.Status, Icon> SIMULATION_STATUS_ICON_MAP; - static { - HashMap<Simulation.Status, Icon> map = new HashMap<Simulation.Status, Icon>(); - map.put(Simulation.Status.NOT_SIMULATED, loadImageIcon("pix/spheres/gray-16x16.png", "Not simulated")); - map.put(Simulation.Status.UPTODATE, loadImageIcon("pix/spheres/green-16x16.png", "Up to date")); - map.put(Simulation.Status.LOADED, loadImageIcon("pix/spheres/yellow-16x16.png", "Loaded from file")); - map.put(Simulation.Status.OUTDATED, loadImageIcon("pix/spheres/red-16x16.png", "Out-of-date")); - map.put(Simulation.Status.EXTERNAL, loadImageIcon("pix/spheres/blue-16x16.png", "Imported data")); - SIMULATION_STATUS_ICON_MAP = Collections.unmodifiableMap(map); - } - - public static final Icon SIMULATION_LISTENER_OK; - public static final Icon SIMULATION_LISTENER_ERROR; - static { - SIMULATION_LISTENER_OK = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.UPTODATE); - SIMULATION_LISTENER_ERROR = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.OUTDATED); - } - - - public static final Icon FILE_NEW = loadImageIcon("pix/icons/document-new.png", "New document"); - public static final Icon FILE_OPEN = loadImageIcon("pix/icons/document-open.png", "Open document"); - public static final Icon FILE_OPEN_EXAMPLE = loadImageIcon("pix/icons/document-open-example.png", "Open example document"); - public static final Icon FILE_SAVE = loadImageIcon("pix/icons/document-save.png", "Save document"); - public static final Icon FILE_SAVE_AS = loadImageIcon("pix/icons/document-save-as.png", "Save document as"); - public static final Icon FILE_PRINT = loadImageIcon("pix/icons/document-print.png", "Print document"); - public static final Icon FILE_CLOSE = loadImageIcon("pix/icons/document-close.png", "Close document"); - public static final Icon FILE_QUIT = loadImageIcon("pix/icons/application-exit.png", "Quit OpenRocket"); - - public static final Icon EDIT_UNDO = loadImageIcon("pix/icons/edit-undo.png", trans.get("Icons.Undo")); - public static final Icon EDIT_REDO = loadImageIcon("pix/icons/edit-redo.png", trans.get("Icons.Redo")); - public static final Icon EDIT_CUT = loadImageIcon("pix/icons/edit-cut.png", "Cut"); - public static final Icon EDIT_COPY = loadImageIcon("pix/icons/edit-copy.png", "Copy"); - public static final Icon EDIT_PASTE = loadImageIcon("pix/icons/edit-paste.png", "Paste"); - public static final Icon EDIT_DELETE = loadImageIcon("pix/icons/edit-delete.png", "Delete"); - public static final Icon EDIT_SCALE = loadImageIcon("pix/icons/edit-scale.png", "Scale"); - - public static final Icon HELP_ABOUT = loadImageIcon("pix/icons/help-about.png", "About"); - public static final Icon HELP_BUG_REPORT = loadImageIcon("pix/icons/help-bug.png", "Bug report"); - public static final Icon HELP_DEBUG_LOG = loadImageIcon("pix/icons/help-log.png", "Debug log"); - public static final Icon HELP_LICENSE = loadImageIcon("pix/icons/help-license.png", "License"); - - public static final Icon ZOOM_IN = loadImageIcon("pix/icons/zoom-in.png", "Zoom in"); - public static final Icon ZOOM_OUT = loadImageIcon("pix/icons/zoom-out.png", "Zoom out"); - - public static final Icon PREFERENCES = loadImageIcon("pix/icons/preferences.png", "Preferences"); - - public static final Icon DELETE = loadImageIcon("pix/icons/delete.png", "Delete"); - - static { - log.debug("Icons loaded"); - } - - /** - * Load an ImageIcon from the specified file. The file is obtained as a system - * resource from the normal classpath. If the file cannot be loaded a bug dialog - * is opened and <code>null</code> is returned. - * - * @param file the file to load. - * @param name the description of the icon. - * @return the ImageIcon, or null if could not be loaded (after the user closes the dialog) - */ - public static ImageIcon loadImageIcon(String file, String name) { - if (System.getProperty("openrocket.unittest") != null) { - return new ImageIcon(); - } - - URL url = ClassLoader.getSystemResource(file); - if (url == null) { - Application.getExceptionHandler().handleErrorCondition("Image file " + file + " not found, ignoring."); - return null; - } - return new ImageIcon(url, name); - } -} diff --git a/src/net/sf/openrocket/gui/util/OpenFileWorker.java b/src/net/sf/openrocket/gui/util/OpenFileWorker.java deleted file mode 100644 index 1b186408..00000000 --- a/src/net/sf/openrocket/gui/util/OpenFileWorker.java +++ /dev/null @@ -1,174 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; - -import javax.swing.SwingWorker; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.file.RocketLoader; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; - - -/** - * A SwingWorker thread that opens a rocket design file. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OpenFileWorker extends SwingWorker<OpenRocketDocument, Void> { - private static final LogHelper log = Application.getLogger(); - - private final File file; - private final InputStream stream; - private final RocketLoader loader; - - public OpenFileWorker(File file, RocketLoader loader) { - this.file = file; - this.stream = null; - this.loader = loader; - } - - - public OpenFileWorker(InputStream stream, RocketLoader loader) { - this.stream = stream; - this.file = null; - this.loader = loader; - } - - public RocketLoader getRocketLoader() { - return loader; - } - - @Override - protected OpenRocketDocument doInBackground() throws Exception { - InputStream is; - - // Get the correct input stream - if (file != null) { - is = new FileInputStream(file); - } else { - is = stream; - } - - // Buffer stream unless already buffered - if (!(is instanceof BufferedInputStream)) { - is = new BufferedInputStream(is); - } - - // Encapsulate in a ProgressInputStream - is = new ProgressInputStream(is); - - try { - return loader.load(is); - } finally { - try { - is.close(); - } catch (Exception e) { - Application.getExceptionHandler().handleErrorCondition("Error closing file", e); - } - } - } - - - - - private class ProgressInputStream extends FilterInputStream { - - private final int size; - private int readBytes = 0; - private int progress = -1; - - protected ProgressInputStream(InputStream in) { - super(in); - int s; - try { - s = in.available(); - } catch (IOException e) { - log.info("Exception while estimating available bytes!", e); - s = 0; - } - size = Math.max(s, 1); - } - - - - @Override - public int read() throws IOException { - int c = in.read(); - if (c >= 0) { - readBytes++; - setProgress(); - } - if (isCancelled()) { - throw new InterruptedIOException("OpenFileWorker was cancelled"); - } - return c; - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - int n = in.read(b, off, len); - if (n > 0) { - readBytes += n; - setProgress(); - } - if (isCancelled()) { - throw new InterruptedIOException("OpenFileWorker was cancelled"); - } - return n; - } - - @Override - public int read(byte[] b) throws IOException { - int n = in.read(b); - if (n > 0) { - readBytes += n; - setProgress(); - } - if (isCancelled()) { - throw new InterruptedIOException("OpenFileWorker was cancelled"); - } - return n; - } - - @Override - public long skip(long n) throws IOException { - long nr = in.skip(n); - if (nr > 0) { - readBytes += nr; - setProgress(); - } - if (isCancelled()) { - throw new InterruptedIOException("OpenFileWorker was cancelled"); - } - return nr; - } - - @Override - public synchronized void reset() throws IOException { - in.reset(); - readBytes = size - in.available(); - setProgress(); - if (isCancelled()) { - throw new InterruptedIOException("OpenFileWorker was cancelled"); - } - } - - - - private void setProgress() { - int p = MathUtil.clamp(readBytes * 100 / size, 0, 100); - if (progress != p) { - progress = p; - OpenFileWorker.this.setProgress(progress); - } - } - } -} diff --git a/src/net/sf/openrocket/gui/util/ProgressOutputStream.java b/src/net/sf/openrocket/gui/util/ProgressOutputStream.java deleted file mode 100644 index 22935736..00000000 --- a/src/net/sf/openrocket/gui/util/ProgressOutputStream.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InterruptedIOException; -import java.io.OutputStream; - -import javax.swing.SwingWorker; - -import net.sf.openrocket.util.MathUtil; - - -public abstract class ProgressOutputStream extends FilterOutputStream { - - private final int totalBytes; - private final SwingWorker<?,?> worker; - private int writtenBytes = 0; - private int progress = -1; - - public ProgressOutputStream(OutputStream out, int estimate, SwingWorker<?,?> worker) { - super(out); - this.totalBytes = estimate; - this.worker = worker; - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - out.write(b, off, len); - writtenBytes += len; - setProgress(); - if (worker.isCancelled()) { - throw new InterruptedIOException("SaveFileWorker was cancelled"); - } - } - - @Override - public void write(byte[] b) throws IOException { - out.write(b); - writtenBytes += b.length; - setProgress(); - if (worker.isCancelled()) { - throw new InterruptedIOException("SaveFileWorker was cancelled"); - } - } - - @Override - public void write(int b) throws IOException { - out.write(b); - writtenBytes++; - setProgress(); - if (worker.isCancelled()) { - throw new InterruptedIOException("SaveFileWorker was cancelled"); - } - } - - - private void setProgress() { - int p = MathUtil.clamp(writtenBytes * 100 / totalBytes, 0, 100); - if (progress != p) { - progress = p; - setProgress(progress); - } - } - - /** - * Set the current progress. The value of <code>progress</code> is guaranteed - * to be between 0 and 100, inclusive. - * - * @param progress the current progress in the range 0-100. - */ - protected abstract void setProgress(int progress); - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/gui/util/SaveCSVWorker.java b/src/net/sf/openrocket/gui/util/SaveCSVWorker.java deleted file mode 100644 index efa7d13d..00000000 --- a/src/net/sf/openrocket/gui/util/SaveCSVWorker.java +++ /dev/null @@ -1,131 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.awt.Window; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.concurrent.ExecutionException; - -import javax.swing.JOptionPane; -import javax.swing.SwingWorker; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.file.CSVExport; -import net.sf.openrocket.gui.dialogs.SwingWorkerDialog; -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.util.BugException; - - -public class SaveCSVWorker extends SwingWorker<Void, Void> { - - private static final int BYTES_PER_FIELD_PER_POINT = 7; - - private final File file; - private final Simulation simulation; - private final FlightDataBranch branch; - private final FlightDataType[] fields; - private final Unit[] units; - private final String fieldSeparator; - private final String commentStarter; - private final boolean simulationComments; - private final boolean fieldComments; - private final boolean eventComments; - - - public SaveCSVWorker(File file, Simulation simulation, FlightDataBranch branch, - FlightDataType[] fields, Unit[] units, String fieldSeparator, String commentStarter, - boolean simulationComments, boolean fieldComments, boolean eventComments) { - this.file = file; - this.simulation = simulation; - this.branch = branch; - this.fields = fields; - this.units = units; - this.fieldSeparator = fieldSeparator; - this.commentStarter = commentStarter; - this.simulationComments = simulationComments; - this.fieldComments = fieldComments; - this.eventComments = eventComments; - } - - - @Override - protected Void doInBackground() throws Exception { - - int estimate = BYTES_PER_FIELD_PER_POINT * fields.length * branch.getLength(); - estimate = Math.max(estimate, 1000); - - // Create the ProgressOutputStream that provides progress estimates - ProgressOutputStream os = new ProgressOutputStream( - new BufferedOutputStream(new FileOutputStream(file)), - estimate, this) { - - @Override - protected void setProgress(int progress) { - SaveCSVWorker.this.setProgress(progress); - } - - }; - - try { - CSVExport.exportCSV(os, simulation, branch, fields, units, fieldSeparator, - commentStarter, simulationComments, fieldComments, eventComments); - } finally { - try { - os.close(); - } catch (Exception e) { - Application.getExceptionHandler().handleErrorCondition("Error closing file", e); - } - } - return null; - } - - - - /** - * Exports a CSV file using a progress dialog if necessary. - * - * @return <code>true</code> if the save was successful, <code>false</code> otherwise. - */ - public static boolean export(File file, Simulation simulation, FlightDataBranch branch, - FlightDataType[] fields, Unit[] units, String fieldSeparator, String commentStarter, - boolean simulationComments, boolean fieldComments, boolean eventComments, - Window parent) { - - - SaveCSVWorker worker = new SaveCSVWorker(file, simulation, branch, fields, units, - fieldSeparator, commentStarter, simulationComments, fieldComments, - eventComments); - - if (!SwingWorkerDialog.runWorker(parent, "Exporting flight data", - "Writing " + file.getName() + "...", worker)) { - - // User cancelled the save - file.delete(); - return false; - } - - try { - worker.get(); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - - if (cause instanceof IOException) { - JOptionPane.showMessageDialog(parent, new String[] { - "An I/O error occurred while saving:", - e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE); - return false; - } else { - throw new BugException("Unknown error when saving file", e); - } - - } catch (InterruptedException e) { - throw new BugException("EDT was interrupted", e); - } - - return true; - } -} diff --git a/src/net/sf/openrocket/gui/util/SaveFileWorker.java b/src/net/sf/openrocket/gui/util/SaveFileWorker.java deleted file mode 100644 index 1ccc3145..00000000 --- a/src/net/sf/openrocket/gui/util/SaveFileWorker.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileOutputStream; - -import javax.swing.SwingWorker; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.startup.Application; - -public class SaveFileWorker extends SwingWorker<Void, Void> { - - private final OpenRocketDocument document; - private final File file; - private final RocketSaver saver; - - public SaveFileWorker(OpenRocketDocument document, File file, RocketSaver saver) { - this.document = document; - this.file = file; - this.saver = saver; - } - - - @Override - protected Void doInBackground() throws Exception { - - int estimate = (int)saver.estimateFileSize(document, - document.getDefaultStorageOptions()); - - // Create the ProgressOutputStream that provides progress estimates - ProgressOutputStream os = new ProgressOutputStream( - new BufferedOutputStream(new FileOutputStream(file)), - estimate, this) { - - @Override - protected void setProgress(int progress) { - SaveFileWorker.this.setProgress(progress); - } - - }; - - try { - saver.save(os, document); - } finally { - try { - os.close(); - } catch (Exception e) { - Application.getExceptionHandler().handleErrorCondition("Error closing file", e); - } - } - return null; - } - -} diff --git a/src/net/sf/openrocket/gui/util/SimpleFileFilter.java b/src/net/sf/openrocket/gui/util/SimpleFileFilter.java deleted file mode 100644 index 39ded917..00000000 --- a/src/net/sf/openrocket/gui/util/SimpleFileFilter.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.io.File; - -import javax.swing.filechooser.FileFilter; - -/** - * A FileFilter similar to FileNameExtensionFilter except that - * it allows multipart extensions (.ork.gz), and also implements - * the java.io.FileFilter interface. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimpleFileFilter extends FileFilter implements java.io.FileFilter { - - private final String description; - private final boolean acceptDir; - private final String[] extensions; - - - /** - * Create filter that accepts files with the provided extensions that - * accepts directories as well. - * - * @param description the description of this file filter. - * @param extensions an array of extensions that match this filter. - */ - public SimpleFileFilter(String description, String ... extensions) { - this(description, true, extensions); - } - - - /** - * Create filter that accepts files with the provided extensions. - * - * @param description the description of this file filter. - * @param acceptDir whether to accept directories - * @param extensions an array of extensions that match this filter. - */ - public SimpleFileFilter(String description, boolean acceptDir, String ... extensions) { - this.description = description; - this.acceptDir = acceptDir; - this.extensions = new String[extensions.length]; - for (int i=0; i<extensions.length; i++) { - String ext = extensions[i].toLowerCase(); - if (ext.charAt(0) == '.') { - this.extensions[i] = ext; - } else { - this.extensions[i] = '.' + ext; - } - } - } - - - @Override - public boolean accept(File file) { - if (file == null) - return false; - if (file.isDirectory()) - return acceptDir; - - String filename = file.getName(); - filename = filename.toLowerCase(); - for (String ext: extensions) { - if (filename.endsWith(ext)) - return true; - } - - return false; - } - - @Override - public String getDescription() { - return description; - } - -} diff --git a/src/net/sf/openrocket/gui/util/SwingPreferences.java b/src/net/sf/openrocket/gui/util/SwingPreferences.java deleted file mode 100644 index 69e841e4..00000000 --- a/src/net/sf/openrocket/gui/util/SwingPreferences.java +++ /dev/null @@ -1,559 +0,0 @@ -package net.sf.openrocket.gui.util; - -import java.awt.Color; -import java.awt.Dimension; -import java.awt.Point; -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.prefs.BackingStoreException; -import java.util.prefs.Preferences; - -import net.sf.openrocket.arch.SystemInfo; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.RK4SimulationStepper; -import net.sf.openrocket.simulation.SimulationOptions; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.BuildProperties; - - -public class SwingPreferences extends net.sf.openrocket.startup.Preferences { - private static final LogHelper log = Application.getLogger(); - - private static final String SPLIT_CHARACTER = "|"; - - - private static final List<Locale> SUPPORTED_LOCALES; - static { - List<Locale> list = new ArrayList<Locale>(); - for (String lang : new String[] { "en", "de", "es", "fr" }) { - list.add(new Locale(lang)); - } - SUPPORTED_LOCALES = Collections.unmodifiableList(list); - } - - - /** - * Whether to use the debug-node instead of the normal node. - */ - private static final boolean DEBUG; - static { - DEBUG = (System.getProperty("openrocket.debug.prefs") != null); - } - - /** - * Whether to clear all preferences at application startup. This has an effect only - * if DEBUG is true. - */ - private static final boolean CLEARPREFS = true; - - /** - * The node name to use in the Java preferences storage. - */ - private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket"); - - private final Preferences PREFNODE; - - - public SwingPreferences() { - Preferences root = Preferences.userRoot(); - if (DEBUG && CLEARPREFS) { - try { - if (root.nodeExists(NODENAME)) { - root.node(NODENAME).removeNode(); - } - } catch (BackingStoreException e) { - throw new BugException("Unable to clear preference node", e); - } - } - PREFNODE = root.node(NODENAME); - } - - - - - ////////////////////// - - - - /** - * Store the current OpenRocket version into the preferences to allow for preferences migration. - */ - private void storeVersion() { - PREFNODE.put("OpenRocketVersion", BuildProperties.getVersion()); - } - - /** - * Return a string preference. - * - * @param key the preference key. - * @param def the default if no preference is stored - * @return the preference value - */ - @Override - public String getString(String key, String def) { - return PREFNODE.get(key, def); - } - - @Override - public String getString( String directory, String key, String defaultValue ) { - Preferences p = PREFNODE.node(directory); - return p.get(key,defaultValue); - } - - /** - * Set a string preference. - * - * @param key the preference key - * @param value the value to set, or <code>null</code> to remove the key - */ - @Override - public void putString(String key, String value) { - if (value == null) { - PREFNODE.remove(key); - } else { - PREFNODE.put(key, value); - } - storeVersion(); - } - - @Override - public void putString(String directory, String key, String value ) { - Preferences p = PREFNODE.node(directory); - if ( value == null ) { - p.remove(key); - } else { - p.put(key,value); - } - storeVersion(); - } - - /** - * Return a boolean preference. - * - * @param key the preference key - * @param def the default if no preference is stored - * @return the preference value - */ - @Override - public boolean getBoolean(String key, boolean def) { - return PREFNODE.getBoolean(key, def); - } - - /** - * Set a boolean preference. - * - * @param key the preference key - * @param value the value to set - */ - @Override - public void putBoolean(String key, boolean value) { - PREFNODE.putBoolean(key, value); - storeVersion(); - } - - @Override - public int getInt( String key, int defaultValue ) { - return PREFNODE.getInt(key, defaultValue); - } - - @Override - public void putInt( String key , int value ) { - PREFNODE.putInt(key, value ); - storeVersion(); - } - - @Override - public double getDouble(String key, double defaultValue) { - return PREFNODE.getDouble(key, defaultValue ); - } - - @Override - public void putDouble(String key, double value) { - PREFNODE.putDouble(key,value); - storeVersion(); - } - - - - /** - * Return a preferences object for the specified node name. - * - * @param nodeName the node name - * @return the preferences object for that node - */ - public Preferences getNode(String nodeName) { - return PREFNODE.node(nodeName); - } - - - ////////////////// - - - public static List<Locale> getSupportedLocales() { - return SUPPORTED_LOCALES; - } - - public File getDefaultDirectory() { - String file = getString("defaultDirectory", null); - if (file == null) - return null; - return new File(file); - } - - public void setDefaultDirectory(File dir) { - String d; - if (dir == null) { - d = null; - } else { - d = dir.getAbsolutePath(); - } - putString("defaultDirectory", d); - storeVersion(); - } - - - /** - * Return a list of files/directories to be loaded as custom thrust curves. - * <p> - * If this property has not been set, the directory "ThrustCurves" in the user - * application directory will be used. The directory will be created if it does not - * exist. - * - * @return a list of files to load as thrust curves. - */ - public List<File> getUserThrustCurveFiles() { - List<File> list = new ArrayList<File>(); - - String files = getString(USER_THRUST_CURVES_KEY, null); - if (files == null) { - // Default to application directory - File tcdir = getDefaultUserThrustCurveFile(); - if (!tcdir.isDirectory()) { - tcdir.mkdirs(); - } - list.add(tcdir); - } else { - for (String file : files.split("\\" + SPLIT_CHARACTER)) { - file = file.trim(); - if (file.length() > 0) { - list.add(new File(file)); - } - } - } - - return list; - } - - public File getDefaultUserThrustCurveFile() { - File appdir = SystemInfo.getUserApplicationDirectory(); - File tcdir = new File(appdir, "ThrustCurves"); - return tcdir; - } - - - /** - * Set the list of files/directories to be loaded as custom thrust curves. - * - * @param files the files to load, or <code>null</code> to reset to default value. - */ - public void setUserThrustCurveFiles(List<File> files) { - if (files == null) { - putString(USER_THRUST_CURVES_KEY, null); - return; - } - - String str = ""; - - for (File file : files) { - if (str.length() > 0) { - str += SPLIT_CHARACTER; - } - str += file.getAbsolutePath(); - } - putString(USER_THRUST_CURVES_KEY, str); - } - - public Color getMotorBorderColor() { - // TODO: MEDIUM: Motor color (settable?) - return new Color(0, 0, 0, 200); - } - - - public Color getMotorFillColor() { - // TODO: MEDIUM: Motor fill color (settable?) - return new Color(0, 0, 0, 100); - } - - - - public static int getMaxThreadCount() { - return Runtime.getRuntime().availableProcessors(); - } - - - - public Point getWindowPosition(Class<?> c) { - int x, y; - String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null); - - if (pref == null) - return null; - - if (pref.indexOf(',') < 0) - return null; - - try { - x = Integer.parseInt(pref.substring(0, pref.indexOf(','))); - y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1)); - } catch (NumberFormatException e) { - return null; - } - return new Point(x, y); - } - - public void setWindowPosition(Class<?> c, Point p) { - PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y); - storeVersion(); - } - - - - - public Dimension getWindowSize(Class<?> c) { - int x, y; - String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null); - - if (pref == null) - return null; - - if (pref.indexOf(',') < 0) - return null; - - try { - x = Integer.parseInt(pref.substring(0, pref.indexOf(','))); - y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1)); - } catch (NumberFormatException e) { - return null; - } - return new Dimension(x, y); - } - - - public boolean isWindowMaximized(Class<?> c) { - String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null); - return "max".equals(pref); - } - - public void setWindowSize(Class<?> c, Dimension d) { - PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height); - storeVersion(); - } - - public void setWindowMaximized(Class<?> c) { - PREFNODE.node("windows").put("size." + c.getCanonicalName(), "max"); - storeVersion(); - } - - /** - * this class returns a java.awt.Color object for the specified key. - * you can pass (java.awt.Color) null to the second argument to - * disambiguate - */ - public Color getColor( String key, Color defaultValue ) { - net.sf.openrocket.util.Color c = super.getColor(key, (net.sf.openrocket.util.Color) null); - if ( c == null ) { - return defaultValue; - } - return ColorConversion.toAwtColor(c); - } - - /** - * - */ - public void putColor( String key, Color value ) { - net.sf.openrocket.util.Color c = ColorConversion.fromAwtColor(value); - super.putColor(key, c); - } - - //// Printing - - - //// Background flight data computation - - public boolean computeFlightInBackground() { - return PREFNODE.getBoolean("backgroundFlight", true); - } - - public Simulation getBackgroundSimulation(Rocket rocket) { - Simulation s = new Simulation(rocket); - SimulationOptions cond = s.getOptions(); - - cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2); - cond.setWindSpeedAverage(1.0); - cond.setWindSpeedDeviation(0.1); - cond.setLaunchRodLength(5); - return s; - } - - - - ///////// Export variables - - public boolean isExportSelected(FlightDataType type) { - Preferences prefs = PREFNODE.node("exports"); - return prefs.getBoolean(type.getName(), false); - } - - public void setExportSelected(FlightDataType type, boolean selected) { - Preferences prefs = PREFNODE.node("exports"); - prefs.putBoolean(type.getName(), selected); - } - - - - ///////// Default unit storage - - public void loadDefaultUnits() { - Preferences prefs = PREFNODE.node("units"); - try { - - for (String key : prefs.keys()) { - UnitGroup group = UnitGroup.UNITS.get(key); - if (group == null) - continue; - - try { - group.setDefaultUnit(prefs.get(key, null)); - } catch (IllegalArgumentException ignore) { - } - } - - } catch (BackingStoreException e) { - Application.getExceptionHandler().handleErrorCondition(e); - } - } - - public void storeDefaultUnits() { - Preferences prefs = PREFNODE.node("units"); - - for (String key : UnitGroup.UNITS.keySet()) { - UnitGroup group = UnitGroup.UNITS.get(key); - if (group == null || group.getUnitCount() < 2) - continue; - - prefs.put(key, group.getDefaultUnit().getUnit()); - } - } - - - - //// Material storage - - - /** - * Add a user-defined material to the preferences. The preferences are - * first checked for an existing material matching the provided one using - * {@link Material#equals(Object)}. - * - * @param m the material to add. - */ - public void addUserMaterial(Material m) { - Preferences prefs = PREFNODE.node("userMaterials"); - - - // Check whether material already exists - if (getUserMaterials().contains(m)) { - return; - } - - // Add material using next free key (key is not used when loading) - String mat = m.toStorableString(); - for (int i = 0;; i++) { - String key = "material" + i; - if (prefs.get(key, null) == null) { - prefs.put(key, mat); - return; - } - } - } - - - /** - * Remove a user-defined material from the preferences. The matching is performed - * using {@link Material#equals(Object)}. - * - * @param m the material to remove. - */ - public void removeUserMaterial(Material m) { - Preferences prefs = PREFNODE.node("userMaterials"); - - try { - - // Iterate through materials and remove all keys with a matching material - for (String key : prefs.keys()) { - String value = prefs.get(key, null); - try { - - Material existing = Material.fromStorableString(value, true); - if (existing.equals(m)) { - prefs.remove(key); - } - - } catch (IllegalArgumentException ignore) { - } - - } - - } catch (BackingStoreException e) { - throw new IllegalStateException("Cannot read preferences!", e); - } - } - - - /** - * Return a set of all user-defined materials in the preferences. The materials - * are created marked as user-defined. - * - * @return a set of all user-defined materials. - */ - public Set<Material> getUserMaterials() { - Preferences prefs = PREFNODE.node("userMaterials"); - - HashSet<Material> materials = new HashSet<Material>(); - try { - - for (String key : prefs.keys()) { - String value = prefs.get(key, null); - try { - - Material m = Material.fromStorableString(value, true); - materials.add(m); - - } catch (IllegalArgumentException e) { - log.warn("Illegal material string " + value); - } - - } - - } catch (BackingStoreException e) { - throw new IllegalStateException("Cannot read preferences!", e); - } - - return materials; - } - - - //// Helper methods - -} diff --git a/src/net/sf/openrocket/l10n/ClassBasedTranslator.java b/src/net/sf/openrocket/l10n/ClassBasedTranslator.java deleted file mode 100644 index b4212dc9..00000000 --- a/src/net/sf/openrocket/l10n/ClassBasedTranslator.java +++ /dev/null @@ -1,92 +0,0 @@ -package net.sf.openrocket.l10n; - -import java.util.MissingResourceException; - -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.util.BugException; - -/** - * A translator that prepends a pre-defined class name in front of a translation key - * and retrieves the translator for that key, and only if that is missing reverts to - * the base key name. The base class name can either be provided to the constructor - * or retrieved from the stack. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ClassBasedTranslator implements Translator { - - - private final Translator translator; - private final String className; - - /** - * Construct a translator using a specified class name. - * - * @param translator the translator from which to obtain the translations. - * @param className the base class name to prepend. - */ - public ClassBasedTranslator(Translator translator, String className) { - this.translator = translator; - this.className = className; - } - - /** - * Construct a translator by obtaining the base class name from the stack. - * - * @param translator the translator from which to obtain the translations. - * @param levels the number of levels to move upwards in the stack from the point where this method is called. - */ - public ClassBasedTranslator(Translator translator, int levels) { - this(translator, getStackClass(levels)); - } - - - - @Override - public String get(String key) { - String classKey = className + "." + key; - - try { - return translator.get(classKey); - } catch (MissingResourceException e) { - // Ignore - } - - try { - return translator.get(key); - } catch (MissingResourceException e) { - MissingResourceException mre = new MissingResourceException( - "Neither key '" + classKey + "' nor '" + key + "' could be found", e.getClassName(), key); - mre.initCause(e); - throw mre; - } - } - - - - private static String getStackClass(int levels) { - TraceException trace = new TraceException(); - StackTraceElement stack[] = trace.getStackTrace(); - final int index = levels + 2; - if (stack.length <= index) { - throw new BugException("Stack trace is too short, length=" + stack.length + ", expected=" + index, trace); - } - - StackTraceElement element = stack[index]; - String cn = element.getClassName(); - int pos = cn.lastIndexOf('.'); - if (pos >= 0) { - cn = cn.substring(pos + 1); - } - return cn; - } - - - - - // For unit testing purposes - String getClassName() { - return className; - } - -} diff --git a/src/net/sf/openrocket/l10n/DebugTranslator.java b/src/net/sf/openrocket/l10n/DebugTranslator.java deleted file mode 100644 index 5a2bf592..00000000 --- a/src/net/sf/openrocket/l10n/DebugTranslator.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.l10n; - -/** - * A translator implementation that returns the logical key in brackets instead - * of an actual translation. The class optionally verifies that the translation - * is actually obtainable from some other translator. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DebugTranslator implements Translator { - - private final Translator translator; - - - /** - * Sole constructor. - * - * @param translator the translator to verify the translation exists, or <code>null</code> not to verify. - */ - public DebugTranslator(Translator translator) { - this.translator = translator; - } - - - - @Override - public String get(String key) { - if (translator != null) { - translator.get(key); - } - return "[" + key + "]"; - } - -} diff --git a/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java b/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java deleted file mode 100644 index dd916b6c..00000000 --- a/src/net/sf/openrocket/l10n/ExceptionSuppressingTranslator.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.sf.openrocket.l10n; - -import java.util.Locale; -import java.util.MissingResourceException; - -import net.sf.openrocket.startup.Application; - -/** - * A translator that suppresses MissingResourceExceptions and handles them gracefully. - * For the first missing key this class calls the exception handler, and afterwards - * always returns the key for missing translations. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ExceptionSuppressingTranslator implements Translator { - - static boolean errorReported = false; - - private final Translator translator; - - - /** - * Sole constructor. - * - * @param translator the translator to use - */ - public ExceptionSuppressingTranslator(Translator translator) { - this.translator = translator; - } - - - - @Override - public String get(String key) { - try { - return translator.get(key); - } catch (MissingResourceException e) { - handleError(key, e); - } - - return key; - } - - - - private static synchronized void handleError(String key, MissingResourceException e) { - if (!errorReported) { - errorReported = true; - Application.getExceptionHandler().handleErrorCondition("Can not find translation for '" + key + "' locale=" + Locale.getDefault(), e); - } - } - -} diff --git a/src/net/sf/openrocket/l10n/L10N.java b/src/net/sf/openrocket/l10n/L10N.java deleted file mode 100644 index 878b3c41..00000000 --- a/src/net/sf/openrocket/l10n/L10N.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.sf.openrocket.l10n; - -import java.util.Locale; -import java.util.regex.Pattern; - -/** - * Helper methods for localization needs. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class L10N { - - private L10N() { - // Prevent instantiation - } - - - /** - * Replace a text token by a replacement value. - * <p> - * A text token is a string portion that should be surrounded by - * braces, "{text}". - * - * @param original the original string. - * @param token the text token to replace. - * @param replacement the replacement text. - * @return the modified string. - */ - public static String replace(String original, String token, String replacement) { - return Pattern.compile(token, Pattern.LITERAL).matcher(original).replaceAll(replacement); - } - - - /** - * Convert a language code into a Locale. - * - * @param langcode the language code (<code>null</code> ok). - * @return the corresponding locale (or <code>null</code> if the input was <code>null</code>) - */ - public static Locale toLocale(String langcode) { - if (langcode == null) { - return null; - } - - Locale l; - String[] split = langcode.split("[_-]", 3); - if (split.length == 1) { - l = new Locale(split[0]); - } else if (split.length == 2) { - l = new Locale(split[0], split[1]); - } else { - l = new Locale(split[0], split[1], split[2]); - } - return l; - } - -} diff --git a/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java b/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java deleted file mode 100644 index 241ecefc..00000000 --- a/src/net/sf/openrocket/l10n/ResourceBundleTranslator.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.sf.openrocket.l10n; - -import java.util.Locale; -import java.util.ResourceBundle; - -/** - * A translator that obtains translated strings from a resource bundle. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ResourceBundleTranslator implements Translator { - - private final ResourceBundle bundle; - - /** - * Create a ResourceBundleTranslator using the default Locale. - * - * @param baseName the base name of the resource bundle - */ - public ResourceBundleTranslator(String baseName) { - this(baseName, Locale.getDefault()); - } - - /** - * Create a ResourceBundleTranslator using the specified Locale. - * - * @param baseName the base name of the resource bundle - * @param locale the locale to use - */ - public ResourceBundleTranslator(String baseName, Locale locale) { - this.bundle = ResourceBundle.getBundle(baseName, locale); - } - - - /* - * NOTE: This method must be thread-safe! - */ - @Override - public synchronized String get(String key) { - return bundle.getString(key); - } - -} diff --git a/src/net/sf/openrocket/l10n/Translator.java b/src/net/sf/openrocket/l10n/Translator.java deleted file mode 100644 index 9eed2cdf..00000000 --- a/src/net/sf/openrocket/l10n/Translator.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.l10n; - -import java.util.MissingResourceException; - -/** - * An interface for obtaining translations from logical keys. - * <p> - * Translator implementations must be thread-safe. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface Translator { - - /** - * Retrieve a translated string based on a logical key. This always returns - * some string, potentially falling back to the key itself. - * - * @param key the logical string key. - * @return the translated string. - * @throws MissingResourceException if the translation corresponding to the key is not found. - * @throws NullPointerException if key is null. - */ - public String get(String key); - -} diff --git a/src/net/sf/openrocket/logging/BufferLogger.java b/src/net/sf/openrocket/logging/BufferLogger.java deleted file mode 100644 index eab392b6..00000000 --- a/src/net/sf/openrocket/logging/BufferLogger.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.sf.openrocket.logging; - -import java.util.EnumMap; -import java.util.List; - -/** - * A logger implementation that buffers specific levels of log lines. - * The levels that are logged are set using the method - * {@link #setStoreLevel(LogLevel, boolean)}. The stored LogLines can - * be obtained using {@link #getLogs()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class BufferLogger extends LogHelper { - - private final CyclicBuffer<LogLine> buffer; - private final EnumMap<LogLevel, Boolean> storeLevels = - new EnumMap<LogLevel, Boolean>(LogLevel.class); - - - /** - * Create a buffered logger with that logs the specified number of log - * lines. By default all log levels are buffered. - * - * @param length the length of the buffer. - */ - public BufferLogger(int length) { - for (LogLevel l: LogLevel.values()) { - storeLevels.put(l, true); - } - buffer = new CyclicBuffer<LogLine>(length); - } - - - @Override - public void log(LogLine line) { - if (storeLevels.get(line.getLevel())) { - buffer.add(line); - } - } - - /** - * Set whether the specified log level is buffered. - * - * @param level the log level. - * @param store whether to store the level. - */ - public void setStoreLevel(LogLevel level, boolean store) { - storeLevels.put(level, store); - } - - /** - * Get whether the specified log level is buffered. - * - * @param level the log level. - * @return whether the log level is stored. - */ - public boolean getStoreLevel(LogLevel level) { - return storeLevels.get(level); - } - - - /** - * Return all the buffered log lines. - * - * @return a list of all buffered log lines. - */ - public List<LogLine> getLogs() { - return buffer.asList(); - } - - /** - * Return the number of log lines that has been overwritten. - * - * @return the number of log lines missed. - */ - public int getOverwriteCount() { - return buffer.getOverwriteCount(); - } -} diff --git a/src/net/sf/openrocket/logging/CyclicBuffer.java b/src/net/sf/openrocket/logging/CyclicBuffer.java deleted file mode 100644 index 31328ba2..00000000 --- a/src/net/sf/openrocket/logging/CyclicBuffer.java +++ /dev/null @@ -1,176 +0,0 @@ -package net.sf.openrocket.logging; - -import java.util.AbstractQueue; -import java.util.ArrayList; -import java.util.ConcurrentModificationException; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -/** - * A cyclic buffer with a fixed size. When more data is inserted, the newest - * data will overwrite the oldest data. - * <p> - * Though this class implements the Queue interface, it specifically breaks the - * contract by overwriting (removing) data without specific removal. It also - * currently does not support removing arbitrary elements from the set. - * <p> - * The methods in this class are synchronized for concurrent modification. - * However, iterating over the set is not thread-safe. To obtain a snapshot - * of the state of the buffer, use {@link #asList()}. - * - * @param <E> the object type that is stored. - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class CyclicBuffer<E> extends AbstractQueue<E> { - - private final ArrayList<E> buffer; - private final int maxSize; - - private int startPosition = 0; - private int size = 0; - private int overwriteCount = 0; - - private int modCount = 0; - - - /** - * Create a cyclic buffer of the specified size. - * - * @param size the size of the cyclic buffer. - */ - public CyclicBuffer(int size) { - this.buffer = new ArrayList<E>(size); - for (int i=0; i<size; i++) { - this.buffer.add(null); - } - this.maxSize = size; - } - - - - @Override - public synchronized boolean offer(E e) { - buffer.set((startPosition + size) % maxSize, e); - if (size < maxSize) { - size++; - } else { - startPosition = next(startPosition); - overwriteCount++; - } - - modCount++; - return true; - } - - - @Override - public synchronized E peek() { - if (size == 0) - return null; - return buffer.get(startPosition); - } - - - @Override - public synchronized E poll() { - if (size == 0) - return null; - - E element = buffer.get(startPosition); - startPosition = next(startPosition); - size--; - - modCount++; - return element; - } - - - @Override - public synchronized int size() { - return size; - } - - - @Override - public synchronized Iterator<E> iterator() { - return new CyclicBufferIterator(); - } - - - /** - * Return a snapshot of the current buffered objects in the order they - * were placed in the buffer. The list is independent of the buffer. - * - * @return a list of the buffered objects. - */ - public synchronized List<E> asList() { - ArrayList<E> list = new ArrayList<E>(size); - if (startPosition + size > maxSize) { - list.addAll(buffer.subList(startPosition, maxSize)); - list.addAll(buffer.subList(0, startPosition + size - maxSize)); - } else { - list.addAll(buffer.subList(startPosition, startPosition+size)); - } - return list; - } - - - /** - * Return the number of elements that have been overwritten in the buffer. - * The overwritten elements are the elements that have been added to the - * buffer, have not been explicitly removed but are not present in the list. - * - * @return the number of overwritten elements this far. - */ - public synchronized int getOverwriteCount() { - return overwriteCount; - } - - - private int next(int n) { - return (n+1) % maxSize; - } - - - private class CyclicBufferIterator implements Iterator<E> { - - private int expectedModCount; - private int n = 0; - - public CyclicBufferIterator() { - this.expectedModCount = modCount; - } - - @Override - public boolean hasNext() { - synchronized (CyclicBuffer.this) { - if (expectedModCount != modCount) { - throw new ConcurrentModificationException("expectedModCount="+ - expectedModCount+" modCount=" + modCount); - } - return (n < size); - } - } - - @Override - public E next() { - synchronized (CyclicBuffer.this) { - if (expectedModCount != modCount) { - throw new ConcurrentModificationException("expectedModCount="+ - expectedModCount+" modCount=" + modCount); - } - if (n >= size) { - throw new NoSuchElementException("n="+n+" size="+size); - } - n++; - return buffer.get((startPosition + n-1) % maxSize); - } - } - - @Override - public void remove() { - throw new UnsupportedOperationException("random remove not supported"); - } - } -} diff --git a/src/net/sf/openrocket/logging/DelegatorLogger.java b/src/net/sf/openrocket/logging/DelegatorLogger.java deleted file mode 100644 index 8c3a22df..00000000 --- a/src/net/sf/openrocket/logging/DelegatorLogger.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.sf.openrocket.logging; - -import java.util.List; - -import net.sf.openrocket.util.ArrayList; - -/** - * A logger implementation that delegates logging to other logger implementations. - * Multiple loggers can be added to the delegator, all of which will receive - * all of the log lines. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DelegatorLogger extends LogHelper { - - /** - * List of loggers. This list must not be modified, instead it should be - * replaced every time the list is changed. - */ - private volatile ArrayList<LogHelper> loggers = new ArrayList<LogHelper>(); - - @Override - public void log(LogLine line) { - // Must create local reference for thread safety - List<LogHelper> list = loggers; - for (LogHelper l : list) { - l.log(line); - } - } - - - /** - * Add a logger from the delegation list. - * @param logger the logger to add. - */ - public synchronized void addLogger(LogHelper logger) { - ArrayList<LogHelper> newList = loggers.clone(); - newList.add(logger); - this.loggers = newList; - } - - /** - * Remove a logger from the delegation list. - * @param logger the logger to be removed. - */ - public synchronized void removeLogger(LogHelper logger) { - ArrayList<LogHelper> newList = loggers.clone(); - newList.remove(logger); - this.loggers = newList; - } - -} diff --git a/src/net/sf/openrocket/logging/LogHelper.java b/src/net/sf/openrocket/logging/LogHelper.java deleted file mode 100644 index d6b52104..00000000 --- a/src/net/sf/openrocket/logging/LogHelper.java +++ /dev/null @@ -1,517 +0,0 @@ -package net.sf.openrocket.logging; - -import net.sf.openrocket.util.BugException; - - -/** - * Base class for all loggers used in OpenRocket. - * <p> - * This class contains methods for logging at various log levels, and methods - * which take the logging level as a parameter. All methods may take three types - * of parameters: - * <ul> - * <li><code>levels</code> number of additional levels of the stack trace to print - * on the log line. This is useful to determine from where - * the current method has been called. Zero if not provided. - * <li><code>message</code> the String message (may be null). - * <li><code>cause</code> the exception that caused this log (may be null). - * </ul> - * <p> - * The logging methods are guaranteed never to throw an exception, and can thus be safely - * used in finally blocks. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class LogHelper { - /** - * Level from which upward a TraceException is added to the log lines. - */ - private static final LogLevel TRACING_LOG_LEVEL = - LogLevel.fromString(System.getProperty("openrocket.log.tracelevel"), LogLevel.INFO); - - private static final DelegatorLogger delegator = new DelegatorLogger(); - - - - /** - * Get the logger to be used in logging. - * - * @return the logger to be used in all logging. - */ - public static LogHelper getInstance() { - return delegator; - } - - - - /** - * Log a LogLine object. This method needs to be able to cope with multiple threads - * accessing it concurrently (for example by being synchronized). - * - * @param line the LogLine to log. - */ - public abstract void log(LogLine line); - - - /** - * Log using VBOSE level. - * - * @param message the logged message (may be null). - */ - public void verbose(String message) { - try { - log(createLogLine(0, LogLevel.VBOSE, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using VBOSE level. - * - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void verbose(String message, Throwable cause) { - try { - log(createLogLine(0, LogLevel.VBOSE, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using VBOSE level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - */ - public void verbose(int levels, String message) { - try { - log(createLogLine(levels, LogLevel.VBOSE, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using VBOSE level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void verbose(int levels, String message, Throwable cause) { - try { - log(createLogLine(levels, LogLevel.VBOSE, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - /** - * Log using DEBUG level. - * - * @param message the logged message (may be null). - */ - public void debug(String message) { - try { - log(createLogLine(0, LogLevel.DEBUG, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using DEBUG level. - * - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void debug(String message, Throwable cause) { - try { - log(createLogLine(0, LogLevel.DEBUG, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using DEBUG level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - */ - public void debug(int levels, String message) { - try { - log(createLogLine(levels, LogLevel.DEBUG, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using DEBUG level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void debug(int levels, String message, Throwable cause) { - try { - log(createLogLine(levels, LogLevel.DEBUG, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - /** - * Log using INFO level. - * - * @param message the logged message (may be null). - */ - public void info(String message) { - try { - log(createLogLine(0, LogLevel.INFO, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using INFO level. - * - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void info(String message, Throwable cause) { - try { - log(createLogLine(0, LogLevel.INFO, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using INFO level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - */ - public void info(int levels, String message) { - try { - log(createLogLine(levels, LogLevel.INFO, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using INFO level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void info(int levels, String message, Throwable cause) { - try { - log(createLogLine(levels, LogLevel.INFO, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - /** - * Log using USER level. - * - * @param message the logged message (may be null). - */ - public void user(String message) { - try { - log(createLogLine(0, LogLevel.USER, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using USER level. - * - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void user(String message, Throwable cause) { - try { - log(createLogLine(0, LogLevel.USER, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using USER level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - */ - public void user(int levels, String message) { - try { - log(createLogLine(levels, LogLevel.USER, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using USER level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void user(int levels, String message, Throwable cause) { - try { - log(createLogLine(levels, LogLevel.USER, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - /** - * Log using WARN level. - * - * @param message the logged message (may be null). - */ - public void warn(String message) { - try { - log(createLogLine(0, LogLevel.WARN, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using WARN level. - * - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void warn(String message, Throwable cause) { - try { - log(createLogLine(0, LogLevel.WARN, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using WARN level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - */ - public void warn(int levels, String message) { - try { - log(createLogLine(levels, LogLevel.WARN, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using WARN level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void warn(int levels, String message, Throwable cause) { - try { - log(createLogLine(levels, LogLevel.WARN, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - /** - * Log using ERROR level. - * - * @param message the logged message (may be null). - */ - public void error(String message) { - try { - log(createLogLine(0, LogLevel.ERROR, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using ERROR level. - * - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void error(String message, Throwable cause) { - try { - log(createLogLine(0, LogLevel.ERROR, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using ERROR level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - */ - public void error(int levels, String message) { - try { - log(createLogLine(levels, LogLevel.ERROR, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using ERROR level. - * - * @param levels number of additional levels of stack trace to include. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void error(int levels, String message, Throwable cause) { - try { - log(createLogLine(levels, LogLevel.ERROR, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - - /** - * Log using the provided log level. - * - * @param level the logging level. - * @param message the logged message (may be null). - */ - public void log(LogLevel level, String message) { - try { - log(createLogLine(0, level, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using the provided log level. - * - * @param level the logging level. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void log(LogLevel level, String message, Throwable cause) { - try { - log(createLogLine(0, level, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using the provided log level. - * - * @param levels number of additional levels of stack trace to include. - * @param level the logging level. - * @param message the logged message (may be null). - */ - public void log(int levels, LogLevel level, String message) { - try { - log(createLogLine(levels, level, message, null)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Log using the provided log level. - * - * @param levels number of additional levels of stack trace to include. - * @param level the logging level. - * @param message the logged message (may be null). - * @param cause the causing exception (may be null). - */ - public void log(int levels, LogLevel level, String message, Throwable cause) { - try { - log(createLogLine(levels, level, message, cause)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - - - /** - * Instantiates, logs and throws a BugException. The message is logged at - * ERROR level. - * <p> - * This method never returns normally. - * - * @param message the message for the log and exception. - * @throws BugException always. - */ - public void throwBugException(String message) throws BugException { - BugException e = new BugException(message); - log(createLogLine(0, LogLevel.ERROR, message, e)); - throw e; - } - - /** - * Instantiates, logs and throws a BugException. The message is logged at - * ERROR level with the specified cause. - * <p> - * This method never returns normally. - * - * @param message the message for the log and exception. - * @param cause the causing exception (may be null). - * @throws BugException always. - */ - public void throwBugException(String message, Throwable cause) throws BugException { - BugException e = new BugException(message, cause); - log(createLogLine(0, LogLevel.ERROR, message, cause)); - throw e; - } - - - - - /** - * Create a LogLine object from the provided information. This method must be - * called directly from the called method in order for the trace position - * to be correct! - * - * @param additionalLevels how many additional stack trace levels to include on the line. - * @param level the log level. - * @param message the log message (null ok). - * @param cause the log exception (null ok). - * - * @return a LogLine populated with all necessary fields. - */ - private LogLine createLogLine(int additionalLevels, LogLevel level, String message, - Throwable cause) { - TraceException trace; - if (level.atLeast(TRACING_LOG_LEVEL)) { - trace = new TraceException(2, 2 + additionalLevels); - } else { - trace = null; - } - return new LogLine(level, trace, message, cause); - } -} diff --git a/src/net/sf/openrocket/logging/LogLevel.java b/src/net/sf/openrocket/logging/LogLevel.java deleted file mode 100644 index b52d1453..00000000 --- a/src/net/sf/openrocket/logging/LogLevel.java +++ /dev/null @@ -1,123 +0,0 @@ -package net.sf.openrocket.logging; - -/** - * The logging level. The natural order of the LogLevel orders the levels - * from highest priority to lowest priority. Comparisons of the relative levels - * should be performed using the methods {@link #atLeast(LogLevel)}, - * {@link #moreThan(LogLevel)} and {@link #compareTo(LogLevel)}. - * <p> - * A description of the level can be obtained using {@link #toString()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public enum LogLevel { - /** - * Level for indicating a bug or error condition noticed in the software or JRE. - * No ERROR level events _should_ occur while running the program. - */ - ERROR, - - /** - * Level for indicating error conditions or atypical events that can occur during - * normal operation (errors while loading files, weird computation results etc). - */ - WARN, - - /** - * Level for logging user actions (adding and modifying components, running - * simulations etc). A user action should be logged as soon as possible on this - * level. The level is separate so that additional INFO messages won't purge - * user actions from a bounded log buffer. - */ - USER, - - /** - * Level for indicating general level actions the software is performing and - * other notable events during execution (dialogs shown, simulations run etc). - */ - INFO, - - /** - * Level for indicating mid-results, outcomes of methods and other debugging - * information. The data logged should be of value when analyzing error - * conditions and what has caused them. Places that are called repeatedly - * during e.g. flight simulation should use the VBOSE level instead. - */ - DEBUG, - - /** - * Level of verbose debug logging to be used in areas which are called repeatedly, - * such as computational methods used in simulations. This level is separated to - * allow filtering out the verbose logs generated during simulations, DnD etc. - * from the normal debug logs. - */ - VBOSE; - - /** The log level with highest priority */ - public static final LogLevel HIGHEST; - /** The log level with lowest priority */ - public static final LogLevel LOWEST; - /** The maximum length of a level textual description */ - public static final int LENGTH; - - static { - int length = 0; - for (LogLevel l : LogLevel.values()) { - length = Math.max(length, l.toString().length()); - } - LENGTH = length; - - LogLevel[] values = LogLevel.values(); - HIGHEST = values[0]; - LOWEST = values[values.length - 1]; - } - - /** - * Return true if this log level is of a priority at least that of - * <code>level</code>. - */ - public boolean atLeast(LogLevel level) { - return this.compareTo(level) <= 0; - } - - /** - * Return true if this log level is of a priority greater than that of - * <code>level</code>. - */ - public boolean moreThan(LogLevel level) { - return this.compareTo(level) < 0; - } - - - /** - * Return a log level corresponding to a string. The string is case-insensitive. If the - * string is case-insensitively equal to "all", then the lowest logging level is returned. - * - * @param value the string name of a log level, or "all" - * @param defaultLevel the value to return if the string doesn't correspond to any log level or is null - * @return the corresponding log level, of defaultLevel. - */ - public static LogLevel fromString(String value, LogLevel defaultLevel) { - - // Normalize the string - if (value == null) { - return defaultLevel; - } - value = value.toUpperCase().trim(); - - // Find the correct level - LogLevel level = defaultLevel; - if (value.equals("ALL")) { - LogLevel[] values = LogLevel.values(); - level = values[values.length - 1]; - } else { - try { - level = LogLevel.valueOf(value); - } catch (Exception e) { - // Ignore - } - } - return level; - } - -} diff --git a/src/net/sf/openrocket/logging/LogLevelBufferLogger.java b/src/net/sf/openrocket/logging/LogLevelBufferLogger.java deleted file mode 100644 index d9021065..00000000 --- a/src/net/sf/openrocket/logging/LogLevelBufferLogger.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.sf.openrocket.logging; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.List; - -/** - * A logger that buffers a specific number of log lines from every log - * level. This prevents a multitude of lower-level lines from purging - * away important higher-level messages. The method {@link #getLogs()} - * combines these logs into their original (natural) order. A log line - * is also inserted stating the number of log lines of the particular - * level that have been purged from the buffer. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class LogLevelBufferLogger extends LogHelper { - - private final EnumMap<LogLevel, BufferLogger> loggers = - new EnumMap<LogLevel, BufferLogger>(LogLevel.class); - - - /** - * Sole constructor. - * @param count the number of log lines from each level to buffer. - */ - public LogLevelBufferLogger(int count) { - for (LogLevel level : LogLevel.values()) { - loggers.put(level, new BufferLogger(count)); - } - } - - @Override - public void log(LogLine line) { - // Delegate to the buffered logger of this level - loggers.get(line.getLevel()).log(line); - } - - - /** - * Retrieve all buffered log lines in order. A log line is also added to indicate the number of - * log lines that have been purged for that log level. - * - * @return a list of log lines in order. - */ - public List<LogLine> getLogs() { - List<LogLine> result = new ArrayList<LogLine>(); - - for (LogLevel level : LogLevel.values()) { - BufferLogger logger = loggers.get(level); - List<LogLine> logs = logger.getLogs(); - int misses = logger.getOverwriteCount(); - - if (misses > 0) { - if (logs.isEmpty()) { - result.add(new LogLine(level, 0, 0, null, - "===== " + misses + " " + level + " lines removed but log is empty! =====", - null)); - } else { - result.add(new LogLine(level, logs.get(0).getLogCount(), 0, null, - "===== " + misses + " " + level + " lines removed =====", null)); - } - } - result.addAll(logs); - } - - Collections.sort(result); - return result; - } - - -} diff --git a/src/net/sf/openrocket/logging/LogLine.java b/src/net/sf/openrocket/logging/LogLine.java deleted file mode 100644 index c2dc4fa6..00000000 --- a/src/net/sf/openrocket/logging/LogLine.java +++ /dev/null @@ -1,155 +0,0 @@ -package net.sf.openrocket.logging; - -import java.io.PrintWriter; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Container object for a log line. A log line consists of the following elements: - * <ul> - * <li>a LogLevel - * <li>a TraceException - * <li>a message - * <li>a cause Throwable - * <li>an incremental log line counter (provided by LogLine) - * <li>a millisecond timestamp (provided by LogLine) - * </ul> - * Any one of the provided input values may be null. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class LogLine implements Comparable<LogLine> { - - private static final AtomicInteger logCount = new AtomicInteger(1); - private static final long startTime = System.currentTimeMillis(); - - private final LogLevel level; - private final int count; - private final long timestamp; - private final TraceException trace; - private final String message; - private final Throwable cause; - - private volatile String formattedMessage = null; - - - /** - * Construct a LogLine at the current moment. The next log line count number is selected - * and the current run time set to the timestamp. - * - * @param level the logging level - * @param trace the trace exception for the log line, <code>null</code> permitted - * @param message the log message - * @param cause the causing throwable, <code>null</code> permitted - */ - public LogLine(LogLevel level, TraceException trace, String message, Throwable cause) { - this(level, logCount.getAndIncrement(), System.currentTimeMillis() - startTime, trace, message, cause); - } - - /** - * Construct a LogLine with all parameters. This should only be used in special conditions, - * for example to insert a log line at a specific point within normal logs. - * - * @param level the logging level - * @param count the log line count number - * @param timestamp the log line timestamp - * @param trace the trace exception for the log line, <code>null</code> permitted - * @param message the log message - * @param cause the causing throwable, <code>null</code> permitted - */ - public LogLine(LogLevel level, int count, long timestamp, - TraceException trace, String message, Throwable cause) { - this.level = level; - this.count = count; - this.timestamp = timestamp; - this.trace = trace; - this.message = message; - this.cause = cause; - } - - - - /** - * @return the level - */ - public LogLevel getLevel() { - return level; - } - - - /** - * @return the count - */ - public int getLogCount() { - return count; - } - - - /** - * @return the timestamp - */ - public long getTimestamp() { - return timestamp; - } - - - /** - * @return the trace - */ - public TraceException getTrace() { - return trace; - } - - - /** - * @return the message - */ - public String getMessage() { - return message; - } - - - /** - * @return the error - */ - public Throwable getCause() { - return cause; - } - - - - - /** - * Return a formatted string of the log line. The line contains the log - * line count, the time stamp, the log level, the trace position, the log - * message and, if provided, the stack trace of the error throwable. - */ - @Override - public String toString() { - if (formattedMessage == null) { - String str; - str = String.format("%4d %10.3f %-" + LogLevel.LENGTH + "s %s %s", - count, timestamp / 1000.0, (level != null) ? level.toString() : "NULL", - (trace != null) ? trace.getMessage() : "(-)", - message); - if (cause != null) { - StackTraceWriter stw = new StackTraceWriter(); - PrintWriter pw = new PrintWriter(stw); - cause.printStackTrace(pw); - pw.flush(); - str = str + "\n" + stw.toString(); - } - formattedMessage = str; - } - return formattedMessage; - } - - - /** - * Compare against another log line based on the log line count number. - */ - @Override - public int compareTo(LogLine o) { - return this.count - o.count; - } - -} diff --git a/src/net/sf/openrocket/logging/PrintStreamLogger.java b/src/net/sf/openrocket/logging/PrintStreamLogger.java deleted file mode 100644 index d598783f..00000000 --- a/src/net/sf/openrocket/logging/PrintStreamLogger.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.sf.openrocket.logging; - -import java.io.PrintStream; -import java.util.EnumMap; - -/** - * A logger that output log lines to various print streams depending on the log level. - * By default output is logged nowhere. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class PrintStreamLogger extends LogHelper { - - private final EnumMap<LogLevel, PrintStream> output = new EnumMap<LogLevel, PrintStream>(LogLevel.class); - - - @Override - public void log(LogLine line) { - PrintStream stream = output.get(line.getLevel()); - if (stream != null) { - stream.println(line.toString()); - } - } - - public PrintStream getOutput(LogLevel level) { - return output.get(level); - } - - public void setOutput(LogLevel level, PrintStream stream) { - if (level == null) { - throw new IllegalArgumentException("level=" + level + " stream=" + stream); - } - output.put(level, stream); - } - -} diff --git a/src/net/sf/openrocket/logging/StackTraceWriter.java b/src/net/sf/openrocket/logging/StackTraceWriter.java deleted file mode 100644 index dbe2c5a9..00000000 --- a/src/net/sf/openrocket/logging/StackTraceWriter.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.sf.openrocket.logging; - -import java.io.IOException; -import java.io.Writer; - -public class StackTraceWriter extends Writer { - - public static final String PREFIX = " > "; - - private final StringBuilder buffer = new StringBuilder(); - private boolean addPrefix = true; - - @Override - public void write(char[] cbuf, int off, int len) throws IOException { - for (int i=0; i<len; i++) { - if (addPrefix) { - buffer.append(PREFIX); - addPrefix = false; - } - char c = cbuf[off+i]; - buffer.append(c); - if (c == '\n') - addPrefix = true; - } - } - - - @Override - public String toString() { - if (addPrefix && buffer.length() > 0) { - return buffer.substring(0, buffer.length()-1); - } else { - return buffer.toString(); - } - } - - - @Override - public void close() throws IOException { - // no-op - } - - @Override - public void flush() throws IOException { - // no-op - } - -} diff --git a/src/net/sf/openrocket/logging/TraceException.java b/src/net/sf/openrocket/logging/TraceException.java deleted file mode 100644 index f3fc1714..00000000 --- a/src/net/sf/openrocket/logging/TraceException.java +++ /dev/null @@ -1,139 +0,0 @@ -package net.sf.openrocket.logging; - - - -/** - * An exception that is used to store a stack trace. On modern computers - * instantiation of an exception takes on the order of one microsecond, while - * examining the trace typically takes several times longer. Therefore the - * exception should be stored and the stack trace examined only when necessary. - * <p> - * The {@link #getMessage()} method returns a description of the position - * where this exception has been instantiated. The position is provided - * as many levels upwards from the instantiation position as provided to the - * constructor. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class TraceException extends Exception { - - private static final String STANDARD_PACKAGE_PREFIX = "net.sf.openrocket."; - - private final int minLevel; - private final int maxLevel; - private volatile String message = null; - - - /** - * Instantiate exception that provides the line of instantiation as a message. - */ - public TraceException() { - this(0, 0); - } - - /** - * Instantiate exception that provides the provided number of levels upward - * from the instantiation location as a message. The level provided - * is how many levels upward should be examined to find the stack trace - * position for the exception message. - * - * @param level how many levels upward to examine the stack trace to find - * the correct message. - */ - public TraceException(int level) { - this(level, level); - } - - - /** - * Instantiate exception that provides a range of levels upward from the - * instantiation location as a message. This is useful to identify the - * next level of callers upward. - * - * @param minLevel the first level which to include. - * @param maxLevel the last level which to include. - */ - public TraceException(int minLevel, int maxLevel) { - if (minLevel > maxLevel || minLevel < 0) { - throw new IllegalArgumentException("minLevel=" + minLevel + " maxLevel=" + maxLevel); - } - this.minLevel = minLevel; - this.maxLevel = maxLevel; - } - - - /** - * Construct an exception with the specified message. - * - * @param message the message for the exception. - */ - public TraceException(String message) { - this(0, 0); - this.message = message; - } - - - /** - * Construct an exception with the specified message and cause. - * - * @param message the message for the exception. - * @param cause the cause for this exception. - */ - public TraceException(String message, Throwable cause) { - this(0, 0); - this.message = message; - this.initCause(cause); - } - - - /** - * Get the description of the code position as provided in the constructor. - */ - @Override - public String getMessage() { - if (message == null) { - StackTraceElement[] elements = this.getStackTrace(); - - StringBuilder sb = new StringBuilder(); - sb.append('('); - - if (elements == null || elements.length == 0) { - sb.append("no stack trace"); - } else { - - int levelCount = 0; - int position = minLevel; - while (levelCount <= (maxLevel - minLevel) && position < elements.length) { - - // Ignore synthetic "access$0" methods generated by the JRE - if (elements[position].getMethodName().contains("$")) { - position++; - continue; - } - - if (levelCount > 0) { - sb.append(' '); - } - sb.append(toString(elements[position])); - levelCount++; - position++; - } - - } - sb.append(')'); - - message = sb.toString(); - } - return message; - } - - - private static String toString(StackTraceElement element) { - if (element.getClassName().startsWith(STANDARD_PACKAGE_PREFIX)) { - return element.getFileName() + ":" + element.getLineNumber(); - } else { - return element.toString(); - } - } - -} diff --git a/src/net/sf/openrocket/masscalc/AbstractMassCalculator.java b/src/net/sf/openrocket/masscalc/AbstractMassCalculator.java deleted file mode 100644 index 30b53de6..00000000 --- a/src/net/sf/openrocket/masscalc/AbstractMassCalculator.java +++ /dev/null @@ -1,53 +0,0 @@ -package net.sf.openrocket.masscalc; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.startup.Application; - -/** - * Abstract base for mass calculators. Provides functionality for cacheing mass data. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class AbstractMassCalculator implements MassCalculator { - private static final LogHelper log = Application.getLogger(); - - private int rocketMassModID = -1; - private int rocketTreeModID = -1; - - - /** - * Check the current cache consistency. This method must be called by all - * methods that may use any cached data before any other operations are - * performed. If the rocket has changed since the previous call to - * <code>checkCache()</code>, then {@link #voidMassCache()} is called. - * <p> - * This method performs the checking based on the rocket's modification IDs, - * so that these method may be called from listeners of the rocket itself. - * - * @param configuration the configuration of the current call - */ - protected final void checkCache(Configuration configuration) { - if (rocketMassModID != configuration.getRocket().getMassModID() || - rocketTreeModID != configuration.getRocket().getTreeModID()) { - rocketMassModID = configuration.getRocket().getMassModID(); - rocketTreeModID = configuration.getRocket().getTreeModID(); - log.debug("Voiding the mass cache"); - voidMassCache(); - } - } - - - - /** - * Void cached mass data. This method is called whenever a change occurs in - * the rocket structure that affects the mass of the rocket and when a new - * Rocket is used. This method must be overridden to void any cached data - * necessary. The method must call <code>super.voidMassCache()</code> during - * its execution. - */ - protected void voidMassCache() { - // No-op - } - -} diff --git a/src/net/sf/openrocket/masscalc/BasicMassCalculator.java b/src/net/sf/openrocket/masscalc/BasicMassCalculator.java deleted file mode 100644 index 4b5d8276..00000000 --- a/src/net/sf/openrocket/masscalc/BasicMassCalculator.java +++ /dev/null @@ -1,350 +0,0 @@ -package net.sf.openrocket.masscalc; - -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorId; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.motor.MotorInstanceConfiguration; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -public class BasicMassCalculator extends AbstractMassCalculator { - - private static final double MIN_MASS = 0.001 * MathUtil.EPSILON; - - - /* - * Cached data. All CG data is in absolute coordinates. All moments of inertia - * are relative to their respective CG. - */ - private Coordinate[] cgCache = null; - private double longitudinalInertiaCache[] = null; - private double rotationalInertiaCache[] = null; - - - - ////////////////// Mass property calculations /////////////////// - - - /** - * Return the CG of the rocket with the specified motor status (no motors, - * ignition, burnout). - */ - public Coordinate getCG(Configuration configuration, MassCalcType type) { - checkCache(configuration); - calculateStageCache(configuration); - - Coordinate totalCG = null; - - // Stage contribution - for (int stage : configuration.getActiveStages()) { - totalCG = cgCache[stage].average(totalCG); - } - - if (totalCG == null) - totalCG = Coordinate.NUL; - - // Add motor CGs - String motorId = configuration.getMotorConfigurationID(); - if (type != MassCalcType.NO_MOTORS && motorId != null) { - Iterator<MotorMount> iterator = configuration.motorIterator(); - while (iterator.hasNext()) { - MotorMount mount = iterator.next(); - RocketComponent comp = (RocketComponent) mount; - Motor motor = mount.getMotor(motorId); - if (motor == null) - continue; - - Coordinate motorCG = type.getCG(motor).add(mount.getMotorPosition(motorId)); - Coordinate[] cgs = comp.toAbsolute(motorCG); - for (Coordinate cg : cgs) { - totalCG = totalCG.average(cg); - } - } - } - - return totalCG; - } - - - - - /** - * Return the CG of the rocket with the provided motor configuration. - */ - public Coordinate getCG(Configuration configuration, MotorInstanceConfiguration motors) { - checkCache(configuration); - calculateStageCache(configuration); - - Coordinate totalCG = getCG(configuration, MassCalcType.NO_MOTORS); - - // Add motor CGs - if (motors != null) { - for (MotorId id : motors.getMotorIDs()) { - int stage = ((RocketComponent) motors.getMotorMount(id)).getStageNumber(); - if (configuration.isStageActive(stage)) { - - MotorInstance motor = motors.getMotorInstance(id); - Coordinate position = motors.getMotorPosition(id); - Coordinate cg = motor.getCG().add(position); - totalCG = totalCG.average(cg); - - } - } - } - return totalCG; - } - - - /** - * Return the longitudinal inertia of the rocket with the specified motor instance - * configuration. - * - * @param configuration the current motor instance configuration - * @return the longitudinal inertia of the rocket - */ - public double getLongitudinalInertia(Configuration configuration, MotorInstanceConfiguration motors) { - checkCache(configuration); - calculateStageCache(configuration); - - final Coordinate totalCG = getCG(configuration, motors); - double totalInertia = 0; - - // Stages - for (int stage : configuration.getActiveStages()) { - Coordinate stageCG = cgCache[stage]; - - totalInertia += (longitudinalInertiaCache[stage] + - stageCG.weight * MathUtil.pow2(stageCG.x - totalCG.x)); - } - - - // Motors - if (motors != null) { - for (MotorId id : motors.getMotorIDs()) { - int stage = ((RocketComponent) motors.getMotorMount(id)).getStageNumber(); - if (configuration.isStageActive(stage)) { - MotorInstance motor = motors.getMotorInstance(id); - Coordinate position = motors.getMotorPosition(id); - Coordinate cg = motor.getCG().add(position); - - double inertia = motor.getLongitudinalInertia(); - totalInertia += inertia + cg.weight * MathUtil.pow2(cg.x - totalCG.x); - } - } - } - - return totalInertia; - } - - - - /** - * Return the rotational inertia of the rocket with the specified motor instance - * configuration. - * - * @param configuration the current motor instance configuration - * @return the rotational inertia of the rocket - */ - public double getRotationalInertia(Configuration configuration, MotorInstanceConfiguration motors) { - checkCache(configuration); - calculateStageCache(configuration); - - final Coordinate totalCG = getCG(configuration, motors); - double totalInertia = 0; - - // Stages - for (int stage : configuration.getActiveStages()) { - Coordinate stageCG = cgCache[stage]; - - totalInertia += (rotationalInertiaCache[stage] + - stageCG.weight * (MathUtil.pow2(stageCG.y - totalCG.y) + - MathUtil.pow2(stageCG.z - totalCG.z))); - } - - - // Motors - if (motors != null) { - for (MotorId id : motors.getMotorIDs()) { - int stage = ((RocketComponent) motors.getMotorMount(id)).getStageNumber(); - if (configuration.isStageActive(stage)) { - MotorInstance motor = motors.getMotorInstance(id); - Coordinate position = motors.getMotorPosition(id); - Coordinate cg = motor.getCG().add(position); - - double inertia = motor.getRotationalInertia(); - totalInertia += inertia + cg.weight * (MathUtil.pow2(cg.y - totalCG.y) + - MathUtil.pow2(cg.z - totalCG.z)); - } - } - } - - return totalInertia; - } - - - - @Override - public Map<RocketComponent, Coordinate> getCGAnalysis(Configuration configuration, MassCalcType type) { - checkCache(configuration); - calculateStageCache(configuration); - - Map<RocketComponent, Coordinate> map = new HashMap<RocketComponent, Coordinate>(); - - for (RocketComponent c : configuration) { - Coordinate[] cgs = c.toAbsolute(c.getCG()); - Coordinate totalCG = Coordinate.NUL; - for (Coordinate cg : cgs) { - totalCG = totalCG.average(cg); - } - map.put(c, totalCG); - } - - map.put(configuration.getRocket(), getCG(configuration, type)); - - return map; - } - - //////// Cache computations //////// - - private void calculateStageCache(Configuration config) { - if (cgCache == null) { - - int stages = config.getRocket().getStageCount(); - - cgCache = new Coordinate[stages]; - longitudinalInertiaCache = new double[stages]; - rotationalInertiaCache = new double[stages]; - - for (int i = 0; i < stages; i++) { - RocketComponent stage = config.getRocket().getChild(i); - MassData data = calculateAssemblyMassData(stage); - cgCache[i] = stage.toAbsolute(data.cg)[0]; - longitudinalInertiaCache[i] = data.longitudinalInertia; - rotationalInertiaCache[i] = data.rotationalInetria; - } - - } - } - - - - /** - * Returns the mass and inertia data for this component and all subcomponents. - * The inertia is returned relative to the CG, and the CG is in the coordinates - * of the specified component, not global coordinates. - */ - private MassData calculateAssemblyMassData(RocketComponent parent) { - MassData parentData = new MassData(); - - // Calculate data for this component - parentData.cg = parent.getComponentCG(); - if (parentData.cg.weight < MIN_MASS) - parentData.cg = parentData.cg.setWeight(MIN_MASS); - - - // Override only this component's data - if (!parent.getOverrideSubcomponents()) { - if (parent.isMassOverridden()) - parentData.cg = parentData.cg.setWeight(MathUtil.max(parent.getOverrideMass(), MIN_MASS)); - if (parent.isCGOverridden()) - parentData.cg = parentData.cg.setXYZ(parent.getOverrideCG()); - } - - parentData.longitudinalInertia = parent.getLongitudinalUnitInertia() * parentData.cg.weight; - parentData.rotationalInetria = parent.getRotationalUnitInertia() * parentData.cg.weight; - - - // Combine data for subcomponents - for (RocketComponent sibling : parent.getChildren()) { - Coordinate combinedCG; - double dx2, dr2; - - // Compute data of sibling - MassData siblingData = calculateAssemblyMassData(sibling); - Coordinate[] siblingCGs = sibling.toRelative(siblingData.cg, parent); - - for (Coordinate siblingCG : siblingCGs) { - - // Compute CG of this + sibling - combinedCG = parentData.cg.average(siblingCG); - - // Add effect of this CG change to parent inertia - dx2 = pow2(parentData.cg.x - combinedCG.x); - parentData.longitudinalInertia += parentData.cg.weight * dx2; - - dr2 = pow2(parentData.cg.y - combinedCG.y) + pow2(parentData.cg.z - combinedCG.z); - parentData.rotationalInetria += parentData.cg.weight * dr2; - - - // Add inertia of sibling - parentData.longitudinalInertia += siblingData.longitudinalInertia; - parentData.rotationalInetria += siblingData.rotationalInetria; - - // Add effect of sibling CG change - dx2 = pow2(siblingData.cg.x - combinedCG.x); - parentData.longitudinalInertia += siblingData.cg.weight * dx2; - - dr2 = pow2(siblingData.cg.y - combinedCG.y) + pow2(siblingData.cg.z - combinedCG.z); - parentData.rotationalInetria += siblingData.cg.weight * dr2; - - // Set combined CG - parentData.cg = combinedCG; - } - } - - // Override total data - if (parent.getOverrideSubcomponents()) { - if (parent.isMassOverridden()) { - double oldMass = parentData.cg.weight; - double newMass = MathUtil.max(parent.getOverrideMass(), MIN_MASS); - parentData.longitudinalInertia = parentData.longitudinalInertia * newMass / oldMass; - parentData.rotationalInetria = parentData.rotationalInetria * newMass / oldMass; - parentData.cg = parentData.cg.setWeight(newMass); - } - if (parent.isCGOverridden()) { - double oldx = parentData.cg.x; - double newx = parent.getOverrideCGX(); - parentData.longitudinalInertia += parentData.cg.weight * pow2(oldx - newx); - parentData.cg = parentData.cg.setX(newx); - } - } - - return parentData; - } - - - private static class MassData { - public Coordinate cg = Coordinate.NUL; - public double longitudinalInertia = 0; - public double rotationalInetria = 0; - } - - - @Override - protected void voidMassCache() { - super.voidMassCache(); - this.cgCache = null; - this.longitudinalInertiaCache = null; - this.rotationalInertiaCache = null; - } - - - - - @Override - public int getModID() { - return 0; - } - - - -} diff --git a/src/net/sf/openrocket/masscalc/MassCalculator.java b/src/net/sf/openrocket/masscalc/MassCalculator.java deleted file mode 100644 index 003650e1..00000000 --- a/src/net/sf/openrocket/masscalc/MassCalculator.java +++ /dev/null @@ -1,88 +0,0 @@ -package net.sf.openrocket.masscalc; - -import java.util.Map; - -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorInstanceConfiguration; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Monitorable; - -public interface MassCalculator extends Monitorable { - - public static enum MassCalcType { - NO_MOTORS { - @Override - public Coordinate getCG(Motor motor) { - return Coordinate.NUL; - } - }, - LAUNCH_MASS { - @Override - public Coordinate getCG(Motor motor) { - return motor.getLaunchCG(); - } - }, - BURNOUT_MASS { - @Override - public Coordinate getCG(Motor motor) { - return motor.getEmptyCG(); - } - }; - - public abstract Coordinate getCG(Motor motor); - } - - /** - * Compute the CG of the provided configuration. - * - * @param configuration the rocket configuration - * @param type the state of the motors (none, launch mass, burnout mass) - * @return the CG of the configuration - */ - public Coordinate getCG(Configuration configuration, MassCalcType type); - - /** - * Compute the CG of the provided configuration with specified motors. - * - * @param configuration the rocket configuration - * @param motors the motor configuration - * @return the CG of the configuration - */ - public Coordinate getCG(Configuration configuration, MotorInstanceConfiguration motors); - - /** - * Compute the longitudinal inertia of the provided configuration with specified motors. - * - * @param configuration the rocket configuration - * @param motors the motor configuration - * @return the longitudinal inertia of the configuration - */ - public double getLongitudinalInertia(Configuration configuration, MotorInstanceConfiguration motors); - - /** - * Compute the rotational inertia of the provided configuration with specified motors. - * - * @param configuration the rocket configuration - * @param motors the motor configuration - * @return the rotational inertia of the configuration - */ - public double getRotationalInertia(Configuration configuration, MotorInstanceConfiguration motors); - - - /** - * Compute an analysis of the per-component CG's of the provided configuration. - * The returned map will contain an entry for each physical rocket component (not stages) - * with its corresponding (best-effort) CG. Overriding of subcomponents is ignored. - * The CG of the entire configuration with motors is stored in the entry with the corresponding - * Rocket as the key. - * - * @param configuration the rocket configuration - * @param type the state of the motors (none, launch mass, burnout mass) - * @return a map from each rocket component to its corresponding CG. - */ - public Map<RocketComponent, Coordinate> getCGAnalysis(Configuration configuration, MassCalcType type); - - -} diff --git a/src/net/sf/openrocket/material/Material.java b/src/net/sf/openrocket/material/Material.java deleted file mode 100644 index 00be68b1..00000000 --- a/src/net/sf/openrocket/material/Material.java +++ /dev/null @@ -1,236 +0,0 @@ -package net.sf.openrocket.material; - -import net.sf.openrocket.unit.Unit; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.MathUtil; - -/** - * A class for different material types. Each material has a name and density. - * The interpretation of the density depends on the material type. For - * {@link Type#BULK} it is kg/m^3, for {@link Type#SURFACE} km/m^2. - * <p> - * Objects of this type are immutable. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public abstract class Material implements Comparable<Material> { - - public enum Type { - LINE("Line", UnitGroup.UNITS_DENSITY_LINE), - SURFACE("Surface", UnitGroup.UNITS_DENSITY_SURFACE), - BULK("Bulk", UnitGroup.UNITS_DENSITY_BULK); - - private final String name; - private final UnitGroup units; - - private Type(String name, UnitGroup units) { - this.name = name; - this.units = units; - } - - public UnitGroup getUnitGroup() { - return units; - } - - @Override - public String toString() { - return name; - } - } - - - ///// Definitions of different material types ///// - - public static class Line extends Material { - public Line(String name, double density, boolean userDefined) { - super(name, density, userDefined); - } - - @Override - public Type getType() { - return Type.LINE; - } - } - - public static class Surface extends Material { - - public Surface(String name, double density, boolean userDefined) { - super(name, density, userDefined); - } - - @Override - public Type getType() { - return Type.SURFACE; - } - - @Override - public String toStorableString() { - return super.toStorableString(); - } - } - - public static class Bulk extends Material { - public Bulk(String name, double density, boolean userDefined) { - super(name, density, userDefined); - } - - @Override - public Type getType() { - return Type.BULK; - } - } - - - - private final String name; - private final double density; - private final boolean userDefined; - - - public Material(String name, double density, boolean userDefined) { - this.name = name; - this.density = density; - this.userDefined = userDefined; - } - - - - public double getDensity() { - return density; - } - - public String getName() { - return name; - } - - public String getName(Unit u) { - return name + " (" + u.toStringUnit(density) + ")"; - } - - public boolean isUserDefined() { - return userDefined; - } - - public abstract Type getType(); - - @Override - public String toString() { - return this.getName(this.getType().getUnitGroup().getDefaultUnit()); - } - - - /** - * Compares this object to another object. Material objects are equal if and only if - * their types, names and densities are identical. - */ - @Override - public boolean equals(Object o) { - if (o == null) - return false; - if (this.getClass() != o.getClass()) - return false; - Material m = (Material) o; - return ((m.name.equals(this.name)) && MathUtil.equals(m.density, this.density)); - } - - - /** - * A hashCode() method giving a hash code compatible with the equals() method. - */ - @Override - public int hashCode() { - return name.hashCode() + (int) (density * 1000); - } - - - /** - * Order the materials according to their name, secondarily according to density. - */ - @Override - public int compareTo(Material o) { - int c = this.name.compareTo(o.name); - if (c != 0) { - return c; - } else { - return (int) ((this.density - o.density) * 1000); - } - } - - - /** - * Return a new material of the specified type. - */ - public static Material newMaterial(Type type, String name, double density, - boolean userDefined) { - switch (type) { - case LINE: - return new Material.Line(name, density, userDefined); - - case SURFACE: - return new Material.Surface(name, density, userDefined); - - case BULK: - return new Material.Bulk(name, density, userDefined); - - default: - throw new IllegalArgumentException("Unknown material type: " + type); - } - } - - - public String toStorableString() { - return getType().name() + "|" + name.replace('|', ' ') + '|' + density; - } - - - /** - * Return a material defined by the provided string. - * - * @param str the material storage string. - * @param userDefined whether the created material is user-defined. - * @return a new <code>Material</code> object. - * @throws IllegalArgumentException if <code>str</code> is invalid or null. - */ - public static Material fromStorableString(String str, boolean userDefined) { - if (str == null) - throw new IllegalArgumentException("Material string is null"); - - String[] split = str.split("\\|", 3); - if (split.length < 3) - throw new IllegalArgumentException("Illegal material string: " + str); - - Type type = null; - String name; - double density; - - try { - type = Type.valueOf(split[0]); - } catch (Exception e) { - throw new IllegalArgumentException("Illegal material string: " + str, e); - } - - name = split[1]; - - try { - density = Double.parseDouble(split[2]); - } catch (NumberFormatException e) { - throw new IllegalArgumentException("Illegal material string: " + str, e); - } - - switch (type) { - case BULK: - return new Material.Bulk(name, density, userDefined); - - case SURFACE: - return new Material.Surface(name, density, userDefined); - - case LINE: - return new Material.Line(name, density, userDefined); - - default: - throw new IllegalArgumentException("Illegal material string: " + str); - } - } - -} diff --git a/src/net/sf/openrocket/material/MaterialStorage.java b/src/net/sf/openrocket/material/MaterialStorage.java deleted file mode 100644 index 0d7f11b4..00000000 --- a/src/net/sf/openrocket/material/MaterialStorage.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.material; - -import net.sf.openrocket.database.Database; -import net.sf.openrocket.database.DatabaseListener; -import net.sf.openrocket.startup.Application; - -/** - * Class for storing changes to user-added materials. The materials are stored to - * the OpenRocket preferences. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MaterialStorage implements DatabaseListener<Material> { - - @Override - public void elementAdded(Material material, Database<Material> source) { - if (material.isUserDefined()) { - Application.getPreferences().addUserMaterial(material); - } - } - - @Override - public void elementRemoved(Material material, Database<Material> source) { - Application.getPreferences().removeUserMaterial(material); - } - -} diff --git a/src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java b/src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java deleted file mode 100644 index a988ac8e..00000000 --- a/src/net/sf/openrocket/models/atmosphere/AtmosphericConditions.java +++ /dev/null @@ -1,151 +0,0 @@ -package net.sf.openrocket.models.atmosphere; - -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.UniqueID; - -public class AtmosphericConditions implements Cloneable, Monitorable { - - /** Specific gas constant of dry air. */ - public static final double R = 287.053; - - /** Specific heat ratio of air. */ - public static final double GAMMA = 1.4; - - /** The standard air pressure (1.01325 bar). */ - public static final double STANDARD_PRESSURE = 101325.0; - - /** The standard air temperature (20 degrees Celcius). */ - public static final double STANDARD_TEMPERATURE = 293.15; - - - - /** Air pressure, in Pascals. */ - private double pressure; - - /** Air temperature, in Kelvins. */ - private double temperature; - - private int modID; - - - /** - * Construct standard atmospheric conditions. - */ - public AtmosphericConditions() { - this(STANDARD_TEMPERATURE, STANDARD_PRESSURE); - } - - /** - * Construct specified atmospheric conditions. - * - * @param temperature the temperature in Kelvins. - * @param pressure the pressure in Pascals. - */ - public AtmosphericConditions(double temperature, double pressure) { - this.setTemperature(temperature); - this.setPressure(pressure); - this.modID = UniqueID.next(); - } - - - - public double getPressure() { - return pressure; - } - - public void setPressure(double pressure) { - this.pressure = pressure; - this.modID = UniqueID.next(); - } - - public double getTemperature() { - return temperature; - } - - public void setTemperature(double temperature) { - this.temperature = temperature; - this.modID = UniqueID.next(); - } - - /** - * Return the current density of air for dry air. - * - * @return the current density of air. - */ - public double getDensity() { - return getPressure() / (R*getTemperature()); - } - - - /** - * Return the current speed of sound for dry air. - * <p> - * The speed of sound is calculated using the expansion around the temperature 0 C - * <code> c = 331.3 + 0.606*T </code> where T is in Celcius. The result is accurate - * to about 0.5 m/s for temperatures between -30 and 30 C, and within 2 m/s - * for temperatures between -55 and 30 C. - * - * @return the current speed of sound. - */ - public double getMachSpeed() { - return 165.77 + 0.606 * getTemperature(); - } - - - /** - * Return the current kinematic viscosity of the air. - * <p> - * The effect of temperature on the viscosity of a gas can be computed using - * Sutherland's formula. In the region of -40 ... 40 degrees Celcius the effect - * is highly linear, and thus a linear approximation is used in its stead. - * This is divided by the result of {@link #getDensity()} to achieve the - * kinematic viscosity. - * - * @return the current kinematic viscosity. - */ - public double getKinematicViscosity() { - double v = 3.7291e-06 + 4.9944e-08 * getTemperature(); - return v / getDensity(); - } - - - /** - * Return a copy of the atmospheric conditions. - */ - @Override - public AtmosphericConditions clone() { - try { - return (AtmosphericConditions) super.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException encountered!"); - } - } - - @Override - public boolean equals(Object other) { - if (this == other) - return true; - if (!(other instanceof AtmosphericConditions)) - return false; - AtmosphericConditions o = (AtmosphericConditions) other; - return MathUtil.equals(this.pressure, o.pressure) && MathUtil.equals(this.temperature, o.temperature); - } - - @Override - public int hashCode() { - return (int) (this.pressure + this.temperature*1000); - } - - @Override - public int getModID() { - return modID; - } - - @Override - public String toString() { - return String.format("AtmosphericConditions[T=%.2f,P=%.2f]", getTemperature(), getPressure()); - } - -} diff --git a/src/net/sf/openrocket/models/atmosphere/AtmosphericModel.java b/src/net/sf/openrocket/models/atmosphere/AtmosphericModel.java deleted file mode 100644 index 6a4eec84..00000000 --- a/src/net/sf/openrocket/models/atmosphere/AtmosphericModel.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.sf.openrocket.models.atmosphere; - -import net.sf.openrocket.util.Monitorable; - -public interface AtmosphericModel extends Monitorable { - - public AtmosphericConditions getConditions(double altitude); - -} diff --git a/src/net/sf/openrocket/models/atmosphere/ExtendedISAModel.java b/src/net/sf/openrocket/models/atmosphere/ExtendedISAModel.java deleted file mode 100644 index 6b7eb2b7..00000000 --- a/src/net/sf/openrocket/models/atmosphere/ExtendedISAModel.java +++ /dev/null @@ -1,128 +0,0 @@ -package net.sf.openrocket.models.atmosphere; - -import static net.sf.openrocket.models.atmosphere.AtmosphericConditions.R; -import net.sf.openrocket.util.MathUtil; - - -/** - * An atmospheric temperature/pressure model based on the International Standard Atmosphere - * (ISA). The no-argument constructor creates an {@link AtmosphericModel} that corresponds - * to the ISA model. It is extended by the other constructors to allow defining a custom - * first layer. The base temperature and pressure are as given, and all other values - * are calculated based on these. - * <p> - * TODO: LOW: Values at altitudes over 32km differ from standard results by ~5%. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ExtendedISAModel extends InterpolatingAtmosphericModel { - - public static final double STANDARD_TEMPERATURE = 288.15; - public static final double STANDARD_PRESSURE = 101325; - - private static final double G = 9.80665; - - private final double[] layer = { 0, 11000, 20000, 32000, 47000, 51000, 71000, 84852 }; - private final double[] baseTemperature = { - 288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.95 - }; - private final double[] basePressure = new double[layer.length]; - - - /** - * Construct the standard ISA model. - */ - public ExtendedISAModel() { - this(STANDARD_TEMPERATURE, STANDARD_PRESSURE); - } - - /** - * Construct an extended model with the given temperature and pressure at MSL. - * - * @param temperature the temperature at MSL. - * @param pressure the pressure at MSL. - */ - public ExtendedISAModel(double temperature, double pressure) { - this(0, temperature, pressure); - } - - - /** - * Construct an extended model with the given temperature and pressure at the - * specified altitude. Conditions below the given altitude cannot be calculated, - * and the values at the specified altitude will be returned instead. The altitude - * must be lower than the altitude of the next ISA standard layer (below 11km). - * - * @param altitude the altitude of the measurements. - * @param temperature the temperature. - * @param pressure the pressure. - * @throws IllegalArgumentException if the altitude exceeds the second layer boundary - * of the ISA model (over 11km). - */ - public ExtendedISAModel(double altitude, double temperature, double pressure) { - if (altitude >= layer[1]) { - throw new IllegalArgumentException("Too high first altitude: " + altitude); - } - - layer[0] = altitude; - baseTemperature[0] = temperature; - basePressure[0] = pressure; - - for (int i = 1; i < basePressure.length; i++) { - basePressure[i] = getExactConditions(layer[i] - 1).getPressure(); - } - } - - - @Override - protected AtmosphericConditions getExactConditions(double altitude) { - altitude = MathUtil.clamp(altitude, layer[0], layer[layer.length - 1]); - int n; - for (n = 0; n < layer.length - 1; n++) { - if (layer[n + 1] > altitude) - break; - } - - double rate = (baseTemperature[n + 1] - baseTemperature[n]) / (layer[n + 1] - layer[n]); - - double t = baseTemperature[n] + (altitude - layer[n]) * rate; - double p; - if (Math.abs(rate) > 0.001) { - p = basePressure[n] * - Math.pow(1 + (altitude - layer[n]) * rate / baseTemperature[n], -G / (rate * R)); - } else { - p = basePressure[n] * - Math.exp(-(altitude - layer[n]) * G / (R * baseTemperature[n])); - } - - return new AtmosphericConditions(t, p); - } - - @Override - protected double getMaxAltitude() { - return layer[layer.length - 1]; - } - - - public static void main(String foo[]) { - ExtendedISAModel model1 = new ExtendedISAModel(); - ExtendedISAModel model2 = new ExtendedISAModel(278.15, 100000); - - for (double alt = 0; alt < 80000; alt += 500) { - AtmosphericConditions cond1 = model1.getConditions(alt); - AtmosphericConditions cond2 = model2.getConditions(alt); - - AtmosphericConditions diff = new AtmosphericConditions(); - diff.setPressure((cond2.getPressure() - cond1.getPressure()) / cond1.getPressure() * 100); - diff.setTemperature((cond2.getTemperature() - cond1.getTemperature()) / cond1.getTemperature() * 100); - System.out.println("alt=" + alt + - ": std:" + cond1 + " mod:" + cond2 + " diff:" + diff); - } - } - - @Override - public int getModID() { - return 0; - } - -} diff --git a/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java b/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java deleted file mode 100644 index 6a416a9a..00000000 --- a/src/net/sf/openrocket/models/atmosphere/InterpolatingAtmosphericModel.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.sf.openrocket.models.atmosphere; - -/** - * An abstract atmospheric model that pre-computes the conditions on a number of layers - * and later linearly interpolates the values from between these layers. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class InterpolatingAtmosphericModel implements AtmosphericModel { - /** Layer thickness of interpolated altitude. */ - private static final double DELTA = 500; - - private AtmosphericConditions[] levels = null; - - - public AtmosphericConditions getConditions(double altitude) { - if (levels == null) - computeLayers(); - - if (altitude <= 0) - return levels[0]; - if (altitude >= DELTA * (levels.length - 1)) - return levels[levels.length - 1]; - - int n = (int) (altitude / DELTA); - double d = (altitude - n * DELTA) / DELTA; - AtmosphericConditions c = new AtmosphericConditions(); - c.setTemperature(levels[n].getTemperature() * (1 - d) + levels[n + 1].getTemperature() * d); - c.setPressure(levels[n].getPressure() * (1 - d) + levels[n + 1].getPressure() * d); - - return c; - } - - - private void computeLayers() { - double max = getMaxAltitude(); - int n = (int) (max / DELTA) + 1; - levels = new AtmosphericConditions[n]; - for (int i = 0; i < n; i++) { - levels[i] = getExactConditions(i * DELTA); - } - } - - - protected abstract double getMaxAltitude(); - - protected abstract AtmosphericConditions getExactConditions(double altitude); -} diff --git a/src/net/sf/openrocket/models/gravity/GravityModel.java b/src/net/sf/openrocket/models/gravity/GravityModel.java deleted file mode 100644 index 6bee52a7..00000000 --- a/src/net/sf/openrocket/models/gravity/GravityModel.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.sf.openrocket.models.gravity; - -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.WorldCoordinate; - -/** - * An interface for modeling gravitational acceleration. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface GravityModel extends Monitorable { - - /** - * Compute the gravitational acceleration at a given world coordinate - * - * @param wc the world coordinate location - * @return gravitational acceleration in m/s/s - */ - public double getGravity(WorldCoordinate wc); - -} diff --git a/src/net/sf/openrocket/models/gravity/WGSGravityModel.java b/src/net/sf/openrocket/models/gravity/WGSGravityModel.java deleted file mode 100644 index 41fb5c23..00000000 --- a/src/net/sf/openrocket/models/gravity/WGSGravityModel.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.sf.openrocket.models.gravity; - -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.WorldCoordinate; - -/** - * A gravity model based on the WGS84 ellipsoid. - * - * @author Richard Graham <richard@rdg.cc> - */ -public class WGSGravityModel implements GravityModel { - - // Cache the previously computed value - private WorldCoordinate lastWorldCoordinate; - private double lastg; - - - @Override - public double getGravity(WorldCoordinate wc) { - - // This is a proxy method to calcGravity, to avoid repeated calculation - if (wc != this.lastWorldCoordinate) { - this.lastg = calcGravity(wc); - this.lastWorldCoordinate = wc; - } - - return this.lastg; - - } - - - @Override - public int getModID() { - // The model is immutable, so it can return a constant mod ID - return 0; - } - - - private double calcGravity(WorldCoordinate wc) { - - double sin2lat = MathUtil.pow2(Math.sin(wc.getLatitudeRad())); - double g_0 = 9.7803267714 * ((1.0 + 0.00193185138639 * sin2lat) / Math.sqrt(1.0 - 0.00669437999013 * sin2lat)); - - // Apply correction due to altitude. Note this assumes a spherical earth, but it is a small correction - // so it probably doesn't really matter. Also does not take into account gravity of the atmosphere, again - // correction could be done but not really necessary. - double g_alt = g_0 * MathUtil.pow2(WorldCoordinate.REARTH / (WorldCoordinate.REARTH + wc.getAltitude())); - - return g_alt; - } - -} diff --git a/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java b/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java deleted file mode 100644 index 960325d5..00000000 --- a/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java +++ /dev/null @@ -1,169 +0,0 @@ -package net.sf.openrocket.models.wind; - -import java.util.Random; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.PinkNoise; - -/** - * A wind simulator that generates wind speed as pink noise from a specified average wind speed - * and standard deviance. Currently the wind is always directed in the direction of the negative - * X-axis. The simulated wind is unaffected by the altitude. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class PinkNoiseWindModel implements WindModel { - - /** Random value with which to XOR the random seed value */ - private static final int SEED_RANDOMIZATION = 0x7343AA03; - - - - /** Pink noise alpha parameter. */ - private static final double ALPHA = 5.0 / 3.0; - - /** Number of poles to use in the pink noise IIR filter. */ - private static final int POLES = 2; - - /** The standard deviation of the generated pink noise with the specified number of poles. */ - private static final double STDDEV = 2.252; - - /** Time difference between random samples. */ - private static final double DELTA_T = 0.05; - - - private double average = 0; - private double standardDeviation = 0; - - private final int seed; - - private PinkNoise randomSource = null; - private double time1; - private double value1, value2; - - - /** - * Construct a new wind simulation with a specific seed value. - * @param seed the seed value. - */ - public PinkNoiseWindModel(int seed) { - this.seed = seed ^ SEED_RANDOMIZATION; - } - - - - /** - * Return the average wind speed. - * - * @return the average wind speed. - */ - public double getAverage() { - return average; - } - - /** - * Set the average wind speed. This method will also modify the - * standard deviation such that the turbulence intensity remains constant. - * - * @param average the average wind speed to set - */ - public void setAverage(double average) { - double intensity = getTurbulenceIntensity(); - this.average = Math.max(average, 0); - setTurbulenceIntensity(intensity); - } - - - - /** - * Return the standard deviation from the average wind speed. - * - * @return the standard deviation of the wind speed - */ - public double getStandardDeviation() { - return standardDeviation; - } - - /** - * Set the standard deviation of the average wind speed. - * - * @param standardDeviation the standardDeviation to set - */ - public void setStandardDeviation(double standardDeviation) { - this.standardDeviation = Math.max(standardDeviation, 0); - } - - - /** - * Return the turbulence intensity (standard deviation / average). - * - * @return the turbulence intensity - */ - public double getTurbulenceIntensity() { - if (MathUtil.equals(average, 0)) { - if (MathUtil.equals(standardDeviation, 0)) - return 0; - else - return 1000; - } - return standardDeviation / average; - } - - /** - * Set the standard deviation to match the turbulence intensity. - * - * @param intensity the turbulence intensity - */ - public void setTurbulenceIntensity(double intensity) { - setStandardDeviation(intensity * average); - } - - - - - - @Override - public Coordinate getWindVelocity(double time, double altitude) { - if (time < 0) { - throw new IllegalArgumentException("Requesting wind speed at t=" + time); - } - - if (randomSource == null) { - randomSource = new PinkNoise(ALPHA, POLES, new Random(seed)); - time1 = 0; - value1 = randomSource.nextValue(); - value2 = randomSource.nextValue(); - } - - if (time < time1) { - reset(); - return getWindVelocity(time, altitude); - } - - while (time1 + DELTA_T < time) { - value1 = value2; - value2 = randomSource.nextValue(); - time1 += DELTA_T; - } - - double a = (time - time1) / DELTA_T; - - double speed = average + (value1 * (1 - a) + value2 * a) * standardDeviation / STDDEV; - // TODO: MEDIUM: Make wind direction configurable - return new Coordinate(speed, 0, 0); - } - - - private void reset() { - randomSource = null; - } - - - - @Override - public int getModID() { - return (int) (average * 1000 + standardDeviation); - } - -} diff --git a/src/net/sf/openrocket/models/wind/WindModel.java b/src/net/sf/openrocket/models/wind/WindModel.java deleted file mode 100644 index 3f78f102..00000000 --- a/src/net/sf/openrocket/models/wind/WindModel.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.sf.openrocket.models.wind; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Monitorable; - -public interface WindModel extends Monitorable { - - public Coordinate getWindVelocity(double time, double altitude); - -} diff --git a/src/net/sf/openrocket/motor/DesignationComparator.java b/src/net/sf/openrocket/motor/DesignationComparator.java deleted file mode 100644 index 21489cb7..00000000 --- a/src/net/sf/openrocket/motor/DesignationComparator.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.openrocket.motor; - -import java.text.Collator; -import java.util.Comparator; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Compares two motors designations. The motors are ordered first - * by their motor class, second by their average thrust and lastly by any - * extra modifiers at the end of the designation. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DesignationComparator implements Comparator<String> { - private static final Collator COLLATOR; - static { - COLLATOR = Collator.getInstance(Locale.US); - COLLATOR.setStrength(Collator.PRIMARY); - } - - private Pattern pattern = - Pattern.compile("^([0-9][0-9]+|1/([1-8]))?([a-zA-Z])([0-9]+)(.*?)$"); - - @Override - public int compare(String o1, String o2) { - int value; - Matcher m1, m2; - - m1 = pattern.matcher(o1); - m2 = pattern.matcher(o2); - - if (m1.find() && m2.find()) { - - String o1Class = m1.group(3); - int o1Thrust = Integer.parseInt(m1.group(4)); - String o1Extra = m1.group(5); - - String o2Class = m2.group(3); - int o2Thrust = Integer.parseInt(m2.group(4)); - String o2Extra = m2.group(5); - - // 1. Motor class - if (o1Class.equalsIgnoreCase("A") && o2Class.equalsIgnoreCase("A")) { - // 1/2A and 1/4A comparison - String sub1 = m1.group(2); - String sub2 = m2.group(2); - - if (sub1 != null || sub2 != null) { - if (sub1 == null) - sub1 = "1"; - if (sub2 == null) - sub2 = "1"; - value = -COLLATOR.compare(sub1,sub2); - if (value != 0) - return value; - } - } - value = COLLATOR.compare(o1Class,o2Class); - if (value != 0) - return value; - - // 2. Average thrust - if (o1Thrust != o2Thrust) - return o1Thrust - o2Thrust; - - // 3. Extra modifier - return COLLATOR.compare(o1Extra, o2Extra); - - } else { - - // Not understandable designation, simply compare strings - return COLLATOR.compare(o1, o2); - } - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/motor/Manufacturer.java b/src/net/sf/openrocket/motor/Manufacturer.java deleted file mode 100644 index 831181bb..00000000 --- a/src/net/sf/openrocket/motor/Manufacturer.java +++ /dev/null @@ -1,249 +0,0 @@ -package net.sf.openrocket.motor; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Class containing information about motor manufacturers. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Manufacturer { - - private static Set<Manufacturer> manufacturers = new HashSet<Manufacturer>(); - static { - - // AeroTech has many name combinations... - List<String> names = new ArrayList<String>(); - for (String s : new String[] { "A", "AT", "AERO", "AEROT", "AEROTECH" }) { - names.add(s); - names.add(s + "-RMS"); - names.add(s + "-RCS"); - names.add("RCS-" + s); - names.add(s + "-APOGEE"); - } - names.add("ISP"); - - // Aerotech has single-use, reload and hybrid motors - manufacturers.add(new Manufacturer("AeroTech", "AeroTech", Motor.Type.UNKNOWN, - names.toArray(new String[0]))); - - manufacturers.add(new Manufacturer("Alpha Hybrid Rocketry LLC", "Alpha Hybrid Rocketry", Motor.Type.HYBRID, - "AHR", "ALPHA", "ALPHA HYBRID", "ALPHA HYBRIDS", "ALPHA HYBRIDS ROCKETRY")); - - // TODO: HIGH: AMW/ProX - how to classify? - - manufacturers.add(new Manufacturer("Animal Motor Works", "Animal Motor Works", Motor.Type.RELOAD, - "AMW", "AW", "ANIMAL")); - - manufacturers.add(new Manufacturer("Apogee", "Apogee", Motor.Type.SINGLE, - "AP", "APOG", "P")); - - manufacturers.add(new Manufacturer("Cesaroni Technology Inc.", "Cesaroni Technology", Motor.Type.RELOAD, - "CES", "CESARONI", "CESARONI TECHNOLOGY INCORPORATED", "CTI", - "CS", "CSR", "PRO38", "ABC")); - - manufacturers.add(new Manufacturer("Contrail Rockets", "Contrail Rockets", Motor.Type.HYBRID, - "CR", "CONTR", "CONTRAIL", "CONTRAIL ROCKET")); - - manufacturers.add(new Manufacturer("Estes", "Estes", Motor.Type.SINGLE, - "E", "ES")); - - // Ellis Mountain has both single-use and reload motors - manufacturers.add(new Manufacturer("Ellis Mountain", "Ellis Mountain", Motor.Type.UNKNOWN, - "EM", "ELLIS", "ELLIS MOUNTAIN ROCKET", "ELLIS MOUNTAIN ROCKETS")); - - manufacturers.add(new Manufacturer("Gorilla Rocket Motors", "Gorilla Rocket Motors", Motor.Type.RELOAD, - "GR", "GORILLA", "GORILLA ROCKET", "GORILLA ROCKETS", "GORILLA MOTOR", - "GORILLA MOTORS", "GORILLA ROCKET MOTOR")); - - manufacturers.add(new Manufacturer("HyperTEK", "HyperTEK", Motor.Type.HYBRID, - "H", "HT", "HYPER")); - - manufacturers.add(new Manufacturer("Kosdon by AeroTech", "Kosdon by AeroTech", Motor.Type.RELOAD, - "K", "KBA", "K-AT", "KOS", "KOSDON", "KOSDON/AT", "KOSDON/AEROTECH")); - - manufacturers.add(new Manufacturer("Loki Research", "Loki Research", Motor.Type.RELOAD, - "LOKI", "LR")); - - manufacturers.add(new Manufacturer("Public Missiles, Ltd.", "Public Missiles", Motor.Type.SINGLE, - "PM", "PML", "PUBLIC MISSILES LIMITED")); - - manufacturers.add(new Manufacturer("Propulsion Polymers", "Propulsion Polymers", Motor.Type.HYBRID, - "PP", "PROP", "PROPULSION")); - - manufacturers.add(new Manufacturer("Quest", "Quest", Motor.Type.SINGLE, - "Q", "QU")); - - manufacturers.add(new Manufacturer("RATT Works", "RATT Works", Motor.Type.HYBRID, - "RATT", "RT", "RTW")); - - manufacturers.add(new Manufacturer("Roadrunner Rocketry", "Roadrunner Rocketry", Motor.Type.SINGLE, - "RR", "ROADRUNNER")); - - manufacturers.add(new Manufacturer("Rocketvision", "Rocketvision", Motor.Type.SINGLE, - "RV", "ROCKET VISION")); - - manufacturers.add(new Manufacturer("Sky Ripper Systems", "Sky Ripper Systems", Motor.Type.HYBRID, - "SR", "SRS", "SKYR", "SKYRIPPER", "SKY RIPPER", "SKYRIPPER SYSTEMS")); - - manufacturers.add(new Manufacturer("West Coast Hybrids", "West Coast Hybrids", Motor.Type.HYBRID, - "WCH", "WCR", "WEST COAST", "WEST COAST HYBRID")); - - // German WECO Feuerwerk, previously Sachsen Feuerwerk - manufacturers.add(new Manufacturer("WECO Feuerwerk", "WECO Feuerwerk", Motor.Type.SINGLE, - "WECO", "WECO FEUERWERKS", "SF", "SACHSEN", "SACHSEN FEUERWERK", - "SACHSEN FEUERWERKS")); - - - // Check that no duplicates have appeared - for (Manufacturer m1 : manufacturers) { - for (Manufacturer m2 : manufacturers) { - if (m1 == m2) - continue; - for (String name : m1.getAllNames()) { - if (m2.matches(name)) { - throw new IllegalStateException("Manufacturer name clash between " + - "manufacturers " + m1 + " and " + m2 + " name " + name); - } - } - } - } - } - - - - private final String displayName; - private final String simpleName; - private final Set<String> allNames; - private final Set<String> searchNames; - private final Motor.Type motorType; - - - private Manufacturer(String displayName, String simpleName, Motor.Type motorType, String... alternateNames) { - this.displayName = displayName; - this.simpleName = simpleName; - this.motorType = motorType; - if (motorType == null) { - throw new IllegalArgumentException("motorType cannot be null"); - } - - Set<String> all = new HashSet<String>(); - Set<String> search = new HashSet<String>(); - - all.add(displayName); - all.add(simpleName); - search.add(generateSearchString(displayName)); - search.add(generateSearchString(simpleName)); - - for (String name : alternateNames) { - all.add(name); - search.add(generateSearchString(name)); - } - - this.allNames = Collections.unmodifiableSet(all); - this.searchNames = Collections.unmodifiableSet(search); - } - - - /** - * Returns the display name of the manufacturer. This is the value that - * should be presented in the UI to the user. - * - * @return the display name - */ - public String getDisplayName() { - return displayName; - } - - - /** - * Returns the simple name of the manufacturer. This should be used for example - * when saving the manufacturer for compatibility. - * - * @return the simple name. - */ - public String getSimpleName() { - return simpleName; - } - - - /** - * Return all names of the manufacturer. This includes all kinds of - * codes that correspond to the manufacturer (for example "A" for AeroTech). - * - * @return an unmodifiable set of the alternative names. - */ - public Set<String> getAllNames() { - return allNames; - } - - - /** - * Return the motor type that this manufacturer produces if it produces only one motor type. - * If the manufacturer manufactures multiple motor types or the type is unknown, - * type <code>UNKNOWN</code> is returned. - * - * @return the manufactured motor type, or <code>UNKNOWN</code>. - */ - public Motor.Type getMotorType() { - return motorType; - } - - - /** - * Check whether the display, simple or any of the alternative names matches the - * specified name. Matching is performed case insensitively and ignoring any - * non-letter and non-number characters. - * - * @param name the name to search for. - * @return whether this manufacturer matches the request. - */ - public boolean matches(String name) { - if (name == null) - return false; - return this.searchNames.contains(generateSearchString(name)); - } - - - /** - * Return the display name of the manufacturer. - */ - @Override - public String toString() { - return displayName; - } - - - /** - * Returns a manufacturer for the given name. The manufacturer is searched for - * within the manufacturers and if a match is found the corresponding - * object is returned. If not, a new manufacturer is returned with display and - * simple name the name specified. Subsequent requests for the same (or corresponding) - * manufacturer name will return the same object. - * - * @param name the manufacturer name to search for. - * @return the Manufacturer object corresponding the name. - */ - public static synchronized Manufacturer getManufacturer(String name) { - for (Manufacturer m : manufacturers) { - if (m.matches(name)) - return m; - } - - Manufacturer m = new Manufacturer(name.trim(), name.trim(), Motor.Type.UNKNOWN); - manufacturers.add(m); - return m; - } - - - - - private String generateSearchString(String str) { - return str.toLowerCase().replaceAll("[^a-zA-Z0-9]+", " ").trim(); - } - -} diff --git a/src/net/sf/openrocket/motor/Motor.java b/src/net/sf/openrocket/motor/Motor.java deleted file mode 100644 index fda0eebb..00000000 --- a/src/net/sf/openrocket/motor/Motor.java +++ /dev/null @@ -1,133 +0,0 @@ -package net.sf.openrocket.motor; - -import net.sf.openrocket.util.Coordinate; - -public interface Motor { - - /** - * Enum of rocket motor types. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public enum Type { - SINGLE("Single-use", "Single-use solid propellant motor"), - RELOAD("Reloadable", "Reloadable solid propellant motor"), - HYBRID("Hybrid", "Hybrid rocket motor engine"), - UNKNOWN("Unknown", "Unknown motor type"); - - private final String name; - private final String description; - - Type(String name, String description) { - this.name = name; - this.description = description; - } - - /** - * Return a short name of this motor type. - * @return a short name of the motor type. - */ - public String getName() { - return name; - } - - /** - * Return a long description of this motor type. - * @return a description of the motor type. - */ - public String getDescription() { - return description; - } - - @Override - public String toString() { - return name; - } - } - - - /** - * Ejection charge delay value signifying a "plugged" motor with no ejection charge. - * The value is that of <code>Double.POSITIVE_INFINITY</code>. - */ - public static final double PLUGGED = Double.POSITIVE_INFINITY; - - - /** - * Below what portion of maximum thrust is the motor chosen to be off when - * calculating average thrust and burn time. NFPA 1125 defines the "official" - * burn time to be the time which the motor produces over 5% of its maximum thrust. - */ - public static final double MARGINAL_THRUST = 0.05; - - - - /** - * Return the motor type. - * - * @return the motorType - */ - public Type getMotorType(); - - - /** - * Return the designation of the motor. - * - * @return the designation - */ - public String getDesignation(); - - /** - * Return the designation of the motor, including a delay. - * - * @param delay the delay of the motor. - * @return designation with delay. - */ - public String getDesignation(double delay); - - - /** - * Return extra description for the motor. This may include for example - * comments on the source of the thrust curve. The returned <code>String</code> - * may include new-lines. - * - * @return the description - */ - public String getDescription(); - - - /** - * Return the maximum diameter of the motor. - * - * @return the diameter - */ - public double getDiameter(); - - /** - * Return the length of the motor. This should be a "characteristic" length, - * and the exact definition may depend on the motor type. Typically this should - * be the length from the bottom of the motor to the end of the maximum diameter - * portion, ignoring any smaller ejection charge compartments. - * - * @return the length - */ - public double getLength(); - - - public MotorInstance getInstance(); - - - public Coordinate getLaunchCG(); - public Coordinate getEmptyCG(); - - - public double getBurnTimeEstimate(); - public double getAverageThrustEstimate(); - public double getMaxThrustEstimate(); - public double getTotalImpulseEstimate(); - - public double[] getTimePoints(); - public double[] getThrustPoints(); - public Coordinate[] getCGPoints(); - -} diff --git a/src/net/sf/openrocket/motor/MotorDigest.java b/src/net/sf/openrocket/motor/MotorDigest.java deleted file mode 100644 index e50cbf9f..00000000 --- a/src/net/sf/openrocket/motor/MotorDigest.java +++ /dev/null @@ -1,181 +0,0 @@ -package net.sf.openrocket.motor; - -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.TextUtil; - -/** - * A class that generated a "digest" of a motor. A digest is a string value that - * uniquely identifies a motor (like a hash code or checksum). Two motors that have - * the same digest behave similarly with a very high probability. The digest can - * therefore be used to identify motors that otherwise have the same specifications. - * <p> - * The digest only uses a limited amount of precision, so that rounding errors won't - * cause differing digest results. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MotorDigest { - - private static final double EPSILON = 0.00000000001; - - public enum DataType { - /** An array of time points at which data is available (in ms) */ - TIME_ARRAY(0, 1000), - /** Mass data for a few specific points (normally initial and empty mass) (in 0.1g) */ - MASS_SPECIFIC(1, 10000), - /** Mass per time (in 0.1g) */ - MASS_PER_TIME(2, 10000), - /** CG position for a few specific points (normally initial and final CG) (in mm) */ - CG_SPECIFIC(3, 1000), - /** CG position per time (in mm) */ - CG_PER_TIME(4, 1000), - /** Thrust force per time (in mN) */ - FORCE_PER_TIME(5, 1000); - - private final int order; - private final int multiplier; - - DataType(int order, int multiplier) { - this.order = order; - this.multiplier = multiplier; - } - - public int getOrder() { - return order; - } - - public int getMultiplier() { - return multiplier; - } - } - - - private final MessageDigest digest; - private boolean used = false; - private int lastOrder = -1; - - - public MotorDigest() { - try { - digest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("MD5 digest not supported by JRE", e); - } - } - - - public void update(DataType type, int... values) { - - // Check for correct order - if (lastOrder >= type.getOrder()) { - throw new IllegalArgumentException("Called with type=" + type + " order=" + type.getOrder() + - " while lastOrder=" + lastOrder); - } - lastOrder = type.getOrder(); - - // Digest the type - digest.update(bytes(type.getOrder())); - - // Digest the data length - digest.update(bytes(values.length)); - - // Digest the values - for (int v : values) { - digest.update(bytes(v)); - } - - } - - - private void update(DataType type, int multiplier, double... values) { - - int[] intValues = new int[values.length]; - for (int i = 0; i < values.length; i++) { - double v = values[i]; - v = next(v); - v *= multiplier; - v = next(v); - intValues[i] = (int) Math.round(v); - } - update(type, intValues); - } - - public void update(DataType type, double... values) { - update(type, type.getMultiplier(), values); - } - - private static double next(double v) { - return v + Math.signum(v) * EPSILON; - } - - - public String getDigest() { - if (used) { - throw new IllegalStateException("MotorDigest already used"); - } - used = true; - byte[] result = digest.digest(); - return TextUtil.hexString(result); - } - - - - private byte[] bytes(int value) { - return new byte[] { - (byte) ((value >>> 24) & 0xFF), (byte) ((value >>> 16) & 0xFF), - (byte) ((value >>> 8) & 0xFF), (byte) (value & 0xFF) }; - } - - - /** - * Digest the contents of a thrust curve motor. The result is a string uniquely - * defining the functional aspects of the motor. - * - * @param m the motor to digest - * @return the digest - */ - public static String digestMotor(Motor m) { - - // Create the motor digest from data available in RASP files - MotorDigest motorDigest = new MotorDigest(); - motorDigest.update(DataType.TIME_ARRAY, m.getTimePoints()); - - Coordinate[] cg = m.getCGPoints(); - double[] cgx = new double[cg.length]; - double[] mass = new double[cg.length]; - for (int i = 0; i < cg.length; i++) { - cgx[i] = cg[i].x; - mass[i] = cg[i].weight; - } - - motorDigest.update(DataType.MASS_PER_TIME, mass); - motorDigest.update(DataType.CG_PER_TIME, cgx); - motorDigest.update(DataType.FORCE_PER_TIME, m.getThrustPoints()); - return motorDigest.getDigest(); - - } - - public static String digestComment(String comment) { - comment = comment.replaceAll("\\s+", " ").trim(); - - MessageDigest digest; - try { - digest = MessageDigest.getInstance("MD5"); - } catch (NoSuchAlgorithmException e) { - throw new IllegalStateException("MD5 digest not supported by JRE", e); - } - - try { - digest.update(comment.getBytes("UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new IllegalStateException("UTF-8 encoding not supported by JRE", e); - } - - return TextUtil.hexString(digest.digest()); - } - -} diff --git a/src/net/sf/openrocket/motor/MotorId.java b/src/net/sf/openrocket/motor/MotorId.java deleted file mode 100644 index 126fff1c..00000000 --- a/src/net/sf/openrocket/motor/MotorId.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.sf.openrocket.motor; - -/** - * An immutable identifier for a motor instance in a MotorInstanceConfiguration. - * The motor is identified by the ID of its mounting component and a - * positive motor count number. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class MotorId { - - private final String componentId; - private final int number; - - - /** - * Sole constructor. - * - * @param componentId the component ID, must not be null - * @param number a positive motor doun5 number - */ - public MotorId(String componentId, int number) { - super(); - - if (componentId == null) { - throw new IllegalArgumentException("Component ID was null"); - } - if (number <= 0) { - throw new IllegalArgumentException("Number must be positive, n=" + number); - } - - // Use intern so comparison can be done using == instead of equals() - this.componentId = componentId.intern(); - this.number = number; - } - - - public String getComponentId() { - return componentId; - } - - public int getNumber() { - return number; - } - - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - - if (!(o instanceof MotorId)) - return false; - - MotorId other = (MotorId)o; - // Comparison with == ok since string is intern()'ed - return this.componentId == other.componentId && this.number == other.number; - } - - - @Override - public int hashCode() { - return componentId.hashCode() + (number << 12); - } - - // TODO: toString() -} diff --git a/src/net/sf/openrocket/motor/MotorInstance.java b/src/net/sf/openrocket/motor/MotorInstance.java deleted file mode 100644 index 6b48e3a4..00000000 --- a/src/net/sf/openrocket/motor/MotorInstance.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.sf.openrocket.motor; - -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Monitorable; - -public interface MotorInstance extends Cloneable, Monitorable { - - /** - * Step the motor instance forward in time. - * - * @param time the time to step to, from motor ignition. - * @param acceleration the average acceleration during the step. - * @param cond the average atmospheric conditions during the step. - */ - public void step(double time, double acceleration, AtmosphericConditions cond); - - - /** - * Return the time to which this motor has been stepped. - * @return the current step time. - */ - public double getTime(); - - /** - * Return the average thrust during the last step. - */ - public double getThrust(); - - /** - * Return the average CG location during the last step. - */ - public Coordinate getCG(); - - /** - * Return the average longitudinal moment of inertia during the last step. - * This is the actual inertia, not the unit inertia! - */ - public double getLongitudinalInertia(); - - /** - * Return the average rotational moment of inertia during the last step. - * This is the actual inertia, not the unit inertia! - */ - public double getRotationalInertia(); - - /** - * Return whether this motor still produces thrust. If this method returns false - * the motor has burnt out, and will not produce any significant thrust anymore. - */ - public boolean isActive(); - - - /** - * Create a new instance of this motor instance. The state of the motor is - * identical to this instance and can be used independently from this one. - */ - public MotorInstance clone(); - -} diff --git a/src/net/sf/openrocket/motor/MotorInstanceConfiguration.java b/src/net/sf/openrocket/motor/MotorInstanceConfiguration.java deleted file mode 100644 index a3142028..00000000 --- a/src/net/sf/openrocket/motor/MotorInstanceConfiguration.java +++ /dev/null @@ -1,141 +0,0 @@ -package net.sf.openrocket.motor; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Monitorable; - -/** - * A configuration of motor instances identified by a string id. Each motor instance has - * an individual position, ingition time etc. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class MotorInstanceConfiguration implements Monitorable, Cloneable { - - private final List<MotorId> ids = new ArrayList<MotorId>(); - private final List<MotorId> unmodifiableIds = Collections.unmodifiableList(ids); - private final List<MotorInstance> motors = new ArrayList<MotorInstance>(); - private final List<MotorMount> mounts = new ArrayList<MotorMount>(); - private final List<Coordinate> positions = new ArrayList<Coordinate>(); - private final List<Double> ignitionTimes = new ArrayList<Double>(); - - - private int modID = 0; - - - /** - * Add a motor instance to this configuration. The motor is placed at - * the specified position and with an infinite ignition time (never ignited). - * - * @param id the ID of this motor instance. - * @param motor the motor instance. - * @param mount the motor mount containing this motor - * @param position the position of the motor in absolute coordinates. - * @throws IllegalArgumentException if a motor with the specified ID already exists. - */ - public void addMotor(MotorId id, MotorInstance motor, MotorMount mount, Coordinate position) { - if (this.ids.contains(id)) { - throw new IllegalArgumentException("MotorInstanceConfiguration already " + - "contains a motor with id " + id); - } - this.ids.add(id); - this.motors.add(motor); - this.mounts.add(mount); - this.positions.add(position); - this.ignitionTimes.add(Double.POSITIVE_INFINITY); - modID++; - } - - /** - * Return a list of all motor IDs in this configuration (not only ones in active stages). - */ - public List<MotorId> getMotorIDs() { - return unmodifiableIds; - } - - public MotorInstance getMotorInstance(MotorId id) { - return motors.get(indexOf(id)); - } - - public MotorMount getMotorMount(MotorId id) { - return mounts.get(indexOf(id)); - } - - public Coordinate getMotorPosition(MotorId id) { - return positions.get(indexOf(id)); - } - - public void setMotorPosition(MotorId id, Coordinate position) { - positions.set(indexOf(id), position); - modID++; - } - - public double getMotorIgnitionTime(MotorId id) { - return ignitionTimes.get(indexOf(id)); - } - - public void setMotorIgnitionTime(MotorId id, double time) { - this.ignitionTimes.set(indexOf(id), time); - modID++; - } - - - - private int indexOf(MotorId id) { - int index = ids.indexOf(id); - if (index < 0) { - throw new IllegalArgumentException("MotorInstanceConfiguration does not " + - "contain a motor with id " + id); - } - return index; - } - - - - /** - * Step all of the motor instances to the specified time minus their ignition time. - * @param time the "global" time - */ - public void step(double time, double acceleration, AtmosphericConditions cond) { - for (int i = 0; i < motors.size(); i++) { - double t = time - ignitionTimes.get(i); - if (t >= 0) { - motors.get(i).step(t, acceleration, cond); - } - } - modID++; - } - - @Override - public int getModID() { - int id = modID; - for (MotorInstance motor : motors) { - id += motor.getModID(); - } - return id; - } - - /** - * Return a copy of this motor instance configuration with independent motor instances - * from this instance. - */ - @Override - public MotorInstanceConfiguration clone() { - MotorInstanceConfiguration clone = new MotorInstanceConfiguration(); - clone.ids.addAll(this.ids); - clone.mounts.addAll(this.mounts); - clone.positions.addAll(this.positions); - clone.ignitionTimes.addAll(this.ignitionTimes); - for (MotorInstance motor : this.motors) { - clone.motors.add(motor.clone()); - } - clone.modID = this.modID; - return clone; - } - -} diff --git a/src/net/sf/openrocket/motor/ThrustCurveMotor.java b/src/net/sf/openrocket/motor/ThrustCurveMotor.java deleted file mode 100644 index 1f8acd2a..00000000 --- a/src/net/sf/openrocket/motor/ThrustCurveMotor.java +++ /dev/null @@ -1,564 +0,0 @@ -package net.sf.openrocket.motor; - -import java.io.Serializable; -import java.text.Collator; -import java.util.Arrays; -import java.util.Locale; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Inertia; -import net.sf.openrocket.util.MathUtil; - - -public class ThrustCurveMotor implements Motor, Comparable<ThrustCurveMotor> { - private static final LogHelper log = Application.getLogger(); - - public static final double MAX_THRUST = 10e6; - - // Comparators: - private static final Collator COLLATOR = Collator.getInstance(Locale.US); - static { - COLLATOR.setStrength(Collator.PRIMARY); - } - private static final DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator(); - - - - private final Manufacturer manufacturer; - private final String designation; - private final String description; - private final Motor.Type type; - private final double[] delays; - private final double diameter; - private final double length; - private final double[] time; - private final double[] thrust; - private final Coordinate[] cg; - - private double maxThrust; - private double burnTime; - private double averageThrust; - private double totalImpulse; - - - /** - * Sole constructor. Sets all the properties of the motor. - * - * @param manufacturer the manufacturer of the motor. - * @param designation the designation of the motor. - * @param description extra description of the motor. - * @param type the motor type - * @param delays the delays defined for this thrust curve - * @param diameter diameter of the motor. - * @param length length of the motor. - * @param time the time points for the thrust curve. - * @param thrust thrust at the time points. - * @param cg cg at the time points. - */ - public ThrustCurveMotor(Manufacturer manufacturer, String designation, String description, - Motor.Type type, double[] delays, double diameter, double length, - double[] time, double[] thrust, Coordinate[] cg) { - - // Check argument validity - if ((time.length != thrust.length) || (time.length != cg.length)) { - throw new IllegalArgumentException("Array lengths do not match, " + - "time:" + time.length + " thrust:" + thrust.length + - " cg:" + cg.length); - } - if (time.length < 2) { - throw new IllegalArgumentException("Too short thrust-curve, length=" + - time.length); - } - for (int i = 0; i < time.length - 1; i++) { - if (time[i + 1] < time[i]) { - throw new IllegalArgumentException("Time goes backwards, " + - "time[" + i + "]=" + time[i] + " " + - "time[" + (i + 1) + "]=" + time[i + 1]); - } - } - if (!MathUtil.equals(time[0], 0)) { - throw new IllegalArgumentException("Curve starts at time " + time[0]); - } - if (!MathUtil.equals(thrust[0], 0)) { - throw new IllegalArgumentException("Curve starts at thrust " + thrust[0]); - } - if (!MathUtil.equals(thrust[thrust.length - 1], 0)) { - throw new IllegalArgumentException("Curve ends at thrust " + - thrust[thrust.length - 1]); - } - for (double t : thrust) { - if (t < 0) { - throw new IllegalArgumentException("Negative thrust."); - } - if (t > MAX_THRUST || Double.isNaN(t)) { - throw new IllegalArgumentException("Invalid thrust " + t); - } - } - for (Coordinate c : cg) { - if (c.isNaN()) { - throw new IllegalArgumentException("Invalid CG " + c); - } - if (c.x < 0 || c.x > length) { - throw new IllegalArgumentException("Invalid CG position " + c.x); - } - if (c.weight < 0) { - throw new IllegalArgumentException("Negative mass " + c.weight); - } - } - - if (type != Motor.Type.SINGLE && type != Motor.Type.RELOAD && - type != Motor.Type.HYBRID && type != Motor.Type.UNKNOWN) { - throw new IllegalArgumentException("Illegal motor type=" + type); - } - - - this.manufacturer = manufacturer; - this.designation = designation; - this.description = description; - this.type = type; - this.delays = delays.clone(); - this.diameter = diameter; - this.length = length; - this.time = time.clone(); - this.thrust = thrust.clone(); - this.cg = cg.clone(); - - computeStatistics(); - } - - - - /** - * Get the manufacturer of this motor. - * - * @return the manufacturer - */ - public Manufacturer getManufacturer() { - return manufacturer; - } - - - /** - * Return the array of time points for this thrust curve. - * @return an array of time points where the thrust is sampled - */ - public double[] getTimePoints() { - return time.clone(); - } - - /** - * Returns the array of thrust points for this thrust curve. - * @return an array of thrust samples - */ - public double[] getThrustPoints() { - return thrust.clone(); - } - - /** - * Returns the array of CG points for this thrust curve. - * @return an array of CG samples - */ - public Coordinate[] getCGPoints() { - return cg.clone(); - } - - /** - * Return a list of standard delays defined for this motor. - * @return a list of standard delays - */ - public double[] getStandardDelays() { - return delays.clone(); - } - - - /** - * {@inheritDoc} - * <p> - * NOTE: In most cases you want to examine the motor type of the ThrustCurveMotorSet, - * not the ThrustCurveMotor itself. - */ - @Override - public Type getMotorType() { - return type; - } - - - @Override - public String getDesignation() { - return designation; - } - - @Override - public String getDesignation(double delay) { - return designation + "-" + getDelayString(delay); - } - - - @Override - public String getDescription() { - return description; - } - - @Override - public double getDiameter() { - return diameter; - } - - @Override - public double getLength() { - return length; - } - - - @Override - public MotorInstance getInstance() { - return new ThrustCurveMotorInstance(); - } - - - @Override - public Coordinate getLaunchCG() { - return cg[0]; - } - - @Override - public Coordinate getEmptyCG() { - return cg[cg.length - 1]; - } - - - - - @Override - public double getBurnTimeEstimate() { - return burnTime; - } - - @Override - public double getAverageThrustEstimate() { - return averageThrust; - } - - @Override - public double getMaxThrustEstimate() { - return maxThrust; - } - - @Override - public double getTotalImpulseEstimate() { - return totalImpulse; - } - - - - /** - * Compute the general statistics of this motor. - */ - private void computeStatistics() { - - // Maximum thrust - maxThrust = 0; - for (double t : thrust) { - if (t > maxThrust) - maxThrust = t; - } - - - // Burn start time - double thrustLimit = maxThrust * MARGINAL_THRUST; - double burnStart, burnEnd; - - int pos; - for (pos = 1; pos < thrust.length; pos++) { - if (thrust[pos] >= thrustLimit) - break; - } - if (pos >= thrust.length) { - throw new BugException("Could not compute burn start time, maxThrust=" + maxThrust + - " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust)); - } - if (MathUtil.equals(thrust[pos - 1], thrust[pos])) { - // For safety - burnStart = (time[pos - 1] + time[pos]) / 2; - } else { - burnStart = MathUtil.map(thrustLimit, thrust[pos - 1], thrust[pos], time[pos - 1], time[pos]); - } - - - // Burn end time - for (pos = thrust.length - 2; pos >= 0; pos--) { - if (thrust[pos] >= thrustLimit) - break; - } - if (pos < 0) { - throw new BugException("Could not compute burn end time, maxThrust=" + maxThrust + - " limit=" + thrustLimit + " thrust=" + Arrays.toString(thrust)); - } - if (MathUtil.equals(thrust[pos], thrust[pos + 1])) { - // For safety - burnEnd = (time[pos] + time[pos + 1]) / 2; - } else { - burnEnd = MathUtil.map(thrustLimit, thrust[pos], thrust[pos + 1], - time[pos], time[pos + 1]); - } - - - // Burn time - burnTime = Math.max(burnEnd - burnStart, 0); - - - // Total impulse and average thrust - totalImpulse = 0; - averageThrust = 0; - - for (pos = 0; pos < time.length - 1; pos++) { - double t0 = time[pos]; - double t1 = time[pos + 1]; - double f0 = thrust[pos]; - double f1 = thrust[pos + 1]; - - totalImpulse += (t1 - t0) * (f0 + f1) / 2; - - if (t0 < burnStart && t1 > burnStart) { - double fStart = MathUtil.map(burnStart, t0, t1, f0, f1); - averageThrust += (fStart + f1) / 2 * (t1 - burnStart); - } else if (t0 >= burnStart && t1 <= burnEnd) { - averageThrust += (f0 + f1) / 2 * (t1 - t0); - } else if (t0 < burnEnd && t1 > burnEnd) { - double fEnd = MathUtil.map(burnEnd, t0, t1, f0, f1); - averageThrust += (f0 + fEnd) / 2 * (burnEnd - t0); - } - } - - if (burnTime > 0) { - averageThrust /= burnTime; - } else { - averageThrust = 0; - } - - } - - - ////////// Static methods - - /** - * Return a String representation of a delay time. If the delay is {@link #PLUGGED}, - * returns "P". - * - * @param delay the delay time. - * @return the <code>String</code> representation. - */ - public static String getDelayString(double delay) { - return getDelayString(delay, "P"); - } - - /** - * Return a String representation of a delay time. If the delay is {@link #PLUGGED}, - * <code>plugged</code> is returned. - * - * @param delay the delay time. - * @param plugged the return value if there is no ejection charge. - * @return the String representation. - */ - public static String getDelayString(double delay, String plugged) { - if (delay == PLUGGED) - return plugged; - delay = Math.rint(delay * 10) / 10; - if (MathUtil.equals(delay, Math.rint(delay))) - return "" + ((int) delay); - return "" + delay; - } - - - - //////// Motor instance implementation //////// - private class ThrustCurveMotorInstance implements MotorInstance { - - private int position; - - // Previous time step value - private double prevTime; - - // Average thrust during previous step - private double stepThrust; - // Instantaneous thrust at current time point - private double instThrust; - - // Average CG during previous step - private Coordinate stepCG; - // Instantaneous CG at current time point - private Coordinate instCG; - - private final double unitRotationalInertia; - private final double unitLongitudinalInertia; - - private int modID = 0; - - public ThrustCurveMotorInstance() { - log.debug("ThrustCurveMotor: Creating motor instance of " + ThrustCurveMotor.this); - position = 0; - prevTime = 0; - instThrust = 0; - stepThrust = 0; - instCG = cg[0]; - stepCG = cg[0]; - unitRotationalInertia = Inertia.filledCylinderRotational(getDiameter() / 2); - unitLongitudinalInertia = Inertia.filledCylinderLongitudinal(getDiameter() / 2, getLength()); - } - - @Override - public double getTime() { - return prevTime; - } - - @Override - public Coordinate getCG() { - return stepCG; - } - - @Override - public double getLongitudinalInertia() { - return unitLongitudinalInertia * stepCG.weight; - } - - @Override - public double getRotationalInertia() { - return unitRotationalInertia * stepCG.weight; - } - - @Override - public double getThrust() { - return stepThrust; - } - - @Override - public boolean isActive() { - return prevTime < time[time.length - 1]; - } - - @Override - public void step(double nextTime, double acceleration, AtmosphericConditions cond) { - - if (!(nextTime >= prevTime)) { - // Also catches NaN - throw new IllegalArgumentException("Stepping backwards in time, current=" + - prevTime + " new=" + nextTime); - } - if (MathUtil.equals(prevTime, nextTime)) { - return; - } - - modID++; - - if (position >= time.length - 1) { - // Thrust has ended - prevTime = nextTime; - stepThrust = 0; - instThrust = 0; - stepCG = cg[cg.length - 1]; - return; - } - - - // Compute average & instantaneous thrust - if (nextTime < time[position + 1]) { - - // Time step between time points - double nextF = MathUtil.map(nextTime, time[position], time[position + 1], - thrust[position], thrust[position + 1]); - stepThrust = (instThrust + nextF) / 2; - instThrust = nextF; - - } else { - - // Portion of previous step - stepThrust = (instThrust + thrust[position + 1]) / 2 * (time[position + 1] - prevTime); - - // Whole steps - position++; - while ((position < time.length - 1) && (nextTime >= time[position + 1])) { - stepThrust += (thrust[position] + thrust[position + 1]) / 2 * - (time[position + 1] - time[position]); - position++; - } - - // End step - if (position < time.length - 1) { - instThrust = MathUtil.map(nextTime, time[position], time[position + 1], - thrust[position], thrust[position + 1]); - stepThrust += (thrust[position] + instThrust) / 2 * - (nextTime - time[position]); - } else { - // Thrust ended during this step - instThrust = 0; - } - - stepThrust /= (nextTime - prevTime); - - } - - // Compute average and instantaneous CG (simple average between points) - Coordinate nextCG; - if (position < time.length - 1) { - nextCG = MathUtil.map(nextTime, time[position], time[position + 1], - cg[position], cg[position + 1]); - } else { - nextCG = cg[cg.length - 1]; - } - stepCG = instCG.add(nextCG).multiply(0.5); - instCG = nextCG; - - // Update time - prevTime = nextTime; - } - - @Override - public MotorInstance clone() { - try { - return (MotorInstance) super.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException", e); - } - } - - @Override - public int getModID() { - return modID; - } - } - - - - @Override - public int compareTo(ThrustCurveMotor other) { - - int value; - - // 1. Manufacturer - value = COLLATOR.compare(this.manufacturer.getDisplayName(), - ((ThrustCurveMotor) other).manufacturer.getDisplayName()); - if (value != 0) - return value; - - // 2. Designation - value = DESIGNATION_COMPARATOR.compare(this.getDesignation(), other.getDesignation()); - if (value != 0) - return value; - - // 3. Diameter - value = (int) ((this.getDiameter() - other.getDiameter()) * 1000000); - if (value != 0) - return value; - - // 4. Length - value = (int) ((this.getLength() - other.getLength()) * 1000000); - return value; - - } - - -} diff --git a/src/net/sf/openrocket/optimization/general/Function.java b/src/net/sf/openrocket/optimization/general/Function.java deleted file mode 100644 index a7be5668..00000000 --- a/src/net/sf/openrocket/optimization/general/Function.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.optimization.general; - -/** - * An interface defining an optimizable function. - * <p> - * Some function optimizers require that the function is thread-safe. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface Function { - - /** - * Evaluate the function at the specified point. - * <p> - * If the function evaluation is slow, then this method should abort the computation if - * the thread is interrupted. - * - * @param point the point at which to evaluate the function. - * @return the function value. - * @throws InterruptedException if the thread was interrupted before function evaluation was completed. - * @throws OptimizationException if an error occurs that prevents the optimization - */ - public double evaluate(Point point) throws InterruptedException, OptimizationException; - -} diff --git a/src/net/sf/openrocket/optimization/general/FunctionCache.java b/src/net/sf/openrocket/optimization/general/FunctionCache.java deleted file mode 100644 index 69c42d79..00000000 --- a/src/net/sf/openrocket/optimization/general/FunctionCache.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.sf.openrocket.optimization.general; - -/** - * A storage of cached values of a function. The purpose of this class is to - * cache function values between optimization runs. Subinterfaces may provide - * additional functionality. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface FunctionCache { - - /** - * Compute and return the value of the function at the specified point. - * - * @param point the point at which to evaluate. - * @return the value of the function at that point. - */ - public double getValue(Point point); - - /** - * Clear the cache. - */ - public void clearCache(); - - /** - * Return the function that is evaluated by this cache implementation. - * - * @return the function that is being evaluated. - */ - public Function getFunction(); - - /** - * Set the function that is evaluated by this cache implementation. - * - * @param function the function that is being evaluated. - */ - public void setFunction(Function function); - -} diff --git a/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java b/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java deleted file mode 100644 index 485843f9..00000000 --- a/src/net/sf/openrocket/optimization/general/FunctionOptimizer.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.sf.openrocket.optimization.general; - -/** - * An interface for a function optimization algorithm. The function is evaluated - * via a function cache. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface FunctionOptimizer { - - /** - * Perform optimization on the function. The optimization control is called to control - * when optimization is stopped. - * - * @param initial the initial start point of the optimization. - * @param control the optimization control. - * @throws OptimizationException if an error occurs that prevents optimization - */ - public void optimize(Point initial, OptimizationController control) throws OptimizationException; - - - /** - * Return the optimum point computed by {@link #optimize(Point, OptimizationController)}. - * - * @return the optimum point value. - * @throws IllegalStateException if {@link #optimize(Point, OptimizationController)} has not been called. - */ - public Point getOptimumPoint(); - - /** - * Return the function value at the optimum point. - * - * @return the value at the optimum point. - * @throws IllegalStateException if {@link #optimize(Point, OptimizationController)} has not been called. - */ - public double getOptimumValue(); - - - /** - * Return the function cache used by this optimization algorithm. - * - * @return the function cache. - */ - public FunctionCache getFunctionCache(); - - /** - * Set the function cache that provides the function values for this algorithm. - * Some algorithms may require the function cache to be an instance of - * ParallelFunctionCache. - * - * @param functionCache the function cache. - */ - public void setFunctionCache(FunctionCache functionCache); - - -} diff --git a/src/net/sf/openrocket/optimization/general/OptimizationController.java b/src/net/sf/openrocket/optimization/general/OptimizationController.java deleted file mode 100644 index c91689cb..00000000 --- a/src/net/sf/openrocket/optimization/general/OptimizationController.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.sf.openrocket.optimization.general; - -public interface OptimizationController { - - /** - * Control for whether to continue the optimization. This method is called after - * every full step taken by the optimization algorithm. - * - * @param oldPoint the old position. - * @param oldValue the value of the function at the old position. - * @param newPoint the new position. - * @param newValue the value of the function at the new position. - * @param stepSize the step length that is used to search for smaller function values when applicable, or NaN. - * @return <code>true</code> to continue optimization, <code>false</code> to stop. - */ - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, - double stepSize); - -} diff --git a/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java b/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java deleted file mode 100644 index a0a455e9..00000000 --- a/src/net/sf/openrocket/optimization/general/OptimizationControllerDelegator.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.sf.openrocket.optimization.general; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * An OptimizationController that delegates control actions to multiple other controllers. - * The optimization is stopped if any of the controllers stops it. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OptimizationControllerDelegator implements OptimizationController { - - private final List<OptimizationController> controllers = new ArrayList<OptimizationController>(); - - /** - * Construct the controlled based on an array of controllers. - * - * @param controllers the controllers to use. - */ - public OptimizationControllerDelegator(OptimizationController... controllers) { - for (OptimizationController c : controllers) { - this.controllers.add(c); - } - } - - /** - * Construct the controller based on a collection of controllers. - * - * @param controllers the controllers to use. - */ - public OptimizationControllerDelegator(Collection<OptimizationController> controllers) { - this.controllers.addAll(controllers); - } - - - /** - * Control whether to continue optimization. This method returns false if any of the - * used controllers returns false. However, all controllers will be called even if - * an earlier one stops the optimization. - */ - @Override - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - boolean ret = true; - - for (OptimizationController c : controllers) { - if (!c.stepTaken(oldPoint, oldValue, newPoint, newValue, stepSize)) { - ret = false; - } - } - - return ret; - } - -} diff --git a/src/net/sf/openrocket/optimization/general/OptimizationException.java b/src/net/sf/openrocket/optimization/general/OptimizationException.java deleted file mode 100644 index 7c0c0674..00000000 --- a/src/net/sf/openrocket/optimization/general/OptimizationException.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.openrocket.optimization.general; - -/** - * An exception that prevents optimization from continuing. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class OptimizationException extends Exception { - - public OptimizationException(String message) { - super(message); - } - - public OptimizationException(Throwable cause) { - super(cause); - } - - public OptimizationException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java b/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java deleted file mode 100644 index 67154da5..00000000 --- a/src/net/sf/openrocket/optimization/general/ParallelExecutorCache.java +++ /dev/null @@ -1,303 +0,0 @@ -package net.sf.openrocket.optimization.general; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import net.sf.openrocket.util.BugException; - -/** - * An implementation of a ParallelFunctionCache that evaluates function values - * in parallel and caches them. This allows pre-calculating possibly required - * function values beforehand. If values are not required after all, the - * computation can be aborted assuming the function evaluation supports it. - * <p> - * Note that while this class handles threads and abstracts background execution, - * the public methods themselves are NOT thread-safe and should be called from - * only one thread at a time. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ParallelExecutorCache implements ParallelFunctionCache { - - private final Map<Point, Double> functionCache = new HashMap<Point, Double>(); - private final Map<Point, Future<Double>> futureMap = new HashMap<Point, Future<Double>>(); - - private ExecutorService executor; - - private Function function; - - - /** - * Construct a cache that uses the same number of computational threads as there are - * processors available. - */ - public ParallelExecutorCache() { - this(Runtime.getRuntime().availableProcessors()); - } - - /** - * Construct a cache that uses the specified number of computational threads for background - * computation. The threads that are created are marked as daemon threads. - * - * @param threadCount the number of threads to use in the executor. - */ - public ParallelExecutorCache(int threadCount) { - this(new ThreadPoolExecutor(threadCount, threadCount, 60, TimeUnit.SECONDS, - new LinkedBlockingQueue<Runnable>(), - new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setDaemon(true); - return t; - } - })); - } - - /** - * Construct a cache that uses the specified ExecutorService for managing - * computational threads. - * - * @param executor the executor to use for function evaluations. - */ - public ParallelExecutorCache(ExecutorService executor) { - this.executor = executor; - } - - - - @Override - public void compute(Collection<Point> points) { - for (Point p : points) { - compute(p); - } - } - - - @Override - public void compute(Point point) { - - if (isOutsideRange(point)) { - // Point is outside of range - return; - } - - if (functionCache.containsKey(point)) { - // Function has already been evaluated at the point - return; - } - - if (futureMap.containsKey(point)) { - // Function is being evaluated at the point - return; - } - - // Submit point for evaluation - FunctionCallable callable = new FunctionCallable(function, point); - Future<Double> future = executor.submit(callable); - futureMap.put(point, future); - } - - - @Override - public void waitFor(Collection<Point> points) throws InterruptedException, OptimizationException { - for (Point p : points) { - waitFor(p); - } - } - - - @Override - public void waitFor(Point point) throws InterruptedException, OptimizationException { - if (isOutsideRange(point)) { - return; - } - - if (functionCache.containsKey(point)) { - return; - } - - Future<Double> future = futureMap.get(point); - if (future == null) { - throw new IllegalStateException("waitFor called for " + point + " but it is not being computed"); - } - - try { - double value = future.get(); - functionCache.put(point, value); - } catch (ExecutionException e) { - Throwable cause = e.getCause(); - if (cause instanceof InterruptedException) { - throw (InterruptedException) cause; - } - if (cause instanceof OptimizationException) { - throw (OptimizationException) cause; - } - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } - - throw new BugException("Function threw unknown exception while processing", e); - } - } - - - - @Override - public List<Point> abort(Collection<Point> points) { - List<Point> computed = new ArrayList<Point>(Math.min(points.size(), 10)); - - for (Point p : points) { - if (abort(p)) { - computed.add(p); - } - } - - return computed; - } - - - - @Override - public boolean abort(Point point) { - if (isOutsideRange(point)) { - return false; - } - - if (functionCache.containsKey(point)) { - return true; - } - - Future<Double> future = futureMap.remove(point); - if (future == null) { - throw new IllegalStateException("abort called for " + point + " but it is not being computed"); - } - - if (future.isDone()) { - // Evaluation has been completed, store value in cache - try { - double value = future.get(); - functionCache.put(point, value); - return true; - } catch (Exception e) { - return false; - } - } else { - // Cancel the evaluation - future.cancel(true); - return false; - } - } - - - @Override - public void abortAll() { - Iterator<Point> iterator = futureMap.keySet().iterator(); - while (iterator.hasNext()) { - Point point = iterator.next(); - Future<Double> future = futureMap.get(point); - iterator.remove(); - - if (future.isDone()) { - // Evaluation has been completed, store value in cache - try { - double value = future.get(); - functionCache.put(point, value); - } catch (Exception e) { - // Ignore - } - } else { - // Cancel the evaluation - future.cancel(true); - } - } - } - - - @Override - public double getValue(Point point) { - if (isOutsideRange(point)) { - return Double.MAX_VALUE; - } - - Double d = functionCache.get(point); - if (d == null) { - throw new IllegalStateException(point + " is not in function cache. " + - "functionCache=" + functionCache + " futureMap=" + futureMap); - } - return d; - } - - - @Override - public Function getFunction() { - return function; - } - - @Override - public void setFunction(Function function) { - this.function = function; - clearCache(); - } - - @Override - public void clearCache() { - List<Point> list = new ArrayList<Point>(futureMap.keySet()); - abort(list); - functionCache.clear(); - } - - - public ExecutorService getExecutor() { - return executor; - } - - - /** - * Check whether a point is outside of the valid optimization range. - */ - private boolean isOutsideRange(Point p) { - int n = p.dim(); - for (int i = 0; i < n; i++) { - double d = p.get(i); - // Include NaN in disallowed range - if (!(d >= 0.0 && d <= 1.0)) { - return true; - } - } - return false; - } - - - /** - * A Callable that evaluates a function at a specific point and returns the result. - */ - private class FunctionCallable implements Callable<Double> { - private final Function calledFunction; - private final Point point; - - public FunctionCallable(Function function, Point point) { - this.calledFunction = function; - this.point = point; - } - - @Override - public Double call() throws InterruptedException, OptimizationException { - return calledFunction.evaluate(point); - } - } - - -} diff --git a/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java b/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java deleted file mode 100644 index 1c8a35f6..00000000 --- a/src/net/sf/openrocket/optimization/general/ParallelFunctionCache.java +++ /dev/null @@ -1,77 +0,0 @@ -package net.sf.openrocket.optimization.general; - -import java.util.Collection; -import java.util.List; - -/** - * A FunctionCache that allows scheduling points to be computed in the background, - * waiting for specific points to become computed or aborting the computation of - * points. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface ParallelFunctionCache extends FunctionCache { - - /** - * Schedule a list of function evaluations at the specified points. - * The points are added to the end of the computation queue in the order - * they are returned by the iterator. - * - * @param points the points at which to evaluate the function. - */ - public void compute(Collection<Point> points); - - /** - * Schedule function evaluation for the specified point. The point is - * added to the end of the computation queue. - * - * @param point the point at which to evaluate the function. - */ - public void compute(Point point); - - /** - * Wait for a collection of points to be computed. After calling this method - * the function values are available by calling {@link #getValue(Point)}. - * - * @param points the points to wait for. - * @throws InterruptedException if this thread or the computing thread was interrupted while waiting. - * @throws OptimizationException if an error preventing continuing the optimization occurs. - */ - public void waitFor(Collection<Point> points) throws InterruptedException, OptimizationException; - - /** - * Wait for a point to be computed. After calling this method - * the function value is available by calling {@link #getValue(Point)}. - * - * @param point the point to wait for. - * @throws InterruptedException if this thread or the computing thread was interrupted while waiting. - * @throws OptimizationException if an error preventing continuing the optimization occurs. - */ - public void waitFor(Point point) throws InterruptedException, OptimizationException; - - - /** - * Abort the computation of the specified points. If computation has ended, - * the result is stored in the function cache anyway. - * - * @param points the points to abort. - * @return a list of the points that have been computed anyway - */ - public List<Point> abort(Collection<Point> points); - - - /** - * Abort the computation of the specified point. If computation has ended, - * the result is stored in the function cache anyway. - * - * @param point the point to abort. - * @return <code>true</code> if the point has been computed anyway, <code>false</code> if not. - */ - public boolean abort(Point point); - - - /** - * Abort the computation of all still unexecuted points. - */ - public void abortAll(); -} diff --git a/src/net/sf/openrocket/optimization/general/Point.java b/src/net/sf/openrocket/optimization/general/Point.java deleted file mode 100644 index 9b2bece8..00000000 --- a/src/net/sf/openrocket/optimization/general/Point.java +++ /dev/null @@ -1,207 +0,0 @@ -package net.sf.openrocket.optimization.general; - -import java.util.Arrays; - -import net.sf.openrocket.util.MathUtil; - -/** - * An immutable n-dimensional coordinate point. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class Point { - - private final double[] point; - private double length = -1; - private double length2 = -1; - - - /** - * Create a new point with all values zero. - * - * @param dim the dimensionality of the point - */ - public Point(int dim) { - if (dim <= 0) { - throw new IllegalArgumentException("Invalid dimensionality " + dim); - } - point = new double[dim]; - } - - - /** - * Create a new point filled with a specific value. - * - * @param dim the dimensionality of the point - * @param value the value for all dimensions - */ - public Point(int dim, double value) { - this(dim); - Arrays.fill(point, value); - } - - - /** - * Create a new point with specific values. - * - * @param value the values of the dimensions. - */ - public Point(double... value) { - if (value.length == 0) { - throw new IllegalArgumentException("Zero-dimensional point not allowed"); - } - point = value.clone(); - } - - - /** - * Create a copy of a point. Used locally to create copies. - * - * @param p the point to copy. - */ - private Point(Point p) { - point = p.point.clone(); - } - - - - /** - * Return the point dimensionality. - * - * @return the point dimensionality - */ - public int dim() { - return point.length; - } - - - - public double get(int i) { - return point[i]; - } - - public Point set(int i, double v) { - Point p = new Point(this); - p.point[i] = v; - return p; - } - - - /** - * Return a new point that is the sum of two points. - * - * @param other the point to add to this point. - * @return the sum of these points. - */ - public Point add(Point other) { - Point p = new Point(this); - for (int i = 0; i < point.length; i++) { - p.point[i] += other.point[i]; - } - return p; - } - - - /** - * Return a new point that is the subtraction of two points. - * - * @param other the point to subtract from this point. - * @return the value of this - other. - */ - public Point sub(Point other) { - Point p = new Point(this); - for (int i = 0; i < point.length; i++) { - p.point[i] -= other.point[i]; - } - return p; - } - - - /** - * Return this point multiplied by a scalar value. - * - * @param v the scalar to multiply with - * @return the scaled point - */ - public Point mul(double v) { - Point p = new Point(this); - for (int i = 0; i < point.length; i++) { - p.point[i] *= v; - } - return p; - } - - - /** - * Return the length of this coordinate. - * - * @return the length. - */ - public double length() { - if (length < 0) { - length = MathUtil.safeSqrt(length2()); - } - return length; - } - - - /** - * Return the squared length of this coordinate. - * - * @return the square of the length of thie coordinate. - */ - public double length2() { - if (length2 < 0) { - length2 = 0; - for (double p : point) { - length2 += p * p; - } - } - return length2; - } - - - - /** - * Return the point as an array. - * - * @return the point as an array. - */ - public double[] asArray() { - return point.clone(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - - if (!(obj instanceof Point)) - return false; - - Point other = (Point) obj; - if (this.point.length != other.point.length) - return false; - - for (int i = 0; i < point.length; i++) { - if (!MathUtil.equals(this.point[i], other.point[i])) - return false; - } - return true; - } - - @Override - public int hashCode() { - int n = 0; - for (double d : point) { - n *= 37; - n += (int) (d * 1000); - } - return n; - } - - @Override - public String toString() { - return "Point" + Arrays.toString(point); - } -} diff --git a/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java b/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java deleted file mode 100644 index 16a2fbcc..00000000 --- a/src/net/sf/openrocket/optimization/general/multidim/FunctionCacheComparator.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.optimization.general.multidim; - -import java.util.Comparator; - -import net.sf.openrocket.optimization.general.FunctionCache; -import net.sf.openrocket.optimization.general.Point; - -/** - * A comparator that orders Points in a function value order, smallest first. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FunctionCacheComparator implements Comparator<Point> { - - private final FunctionCache cache; - - public FunctionCacheComparator(FunctionCache cache) { - this.cache = cache; - } - - @Override - public int compare(Point o1, Point o2) { - double v1 = cache.getValue(o1); - double v2 = cache.getValue(o2); - - return Double.compare(v1, v2); - } - -} diff --git a/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java b/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java deleted file mode 100644 index d4d04fc9..00000000 --- a/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java +++ /dev/null @@ -1,324 +0,0 @@ -package net.sf.openrocket.optimization.general.multidim; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.FunctionCache; -import net.sf.openrocket.optimization.general.FunctionOptimizer; -import net.sf.openrocket.optimization.general.OptimizationController; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.ParallelFunctionCache; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Statistics; - -/** - * A customized implementation of the parallel multidirectional search algorithm by Dennis and Torczon. - * <p> - * This is a parallel pattern search optimization algorithm. The function evaluations are performed - * using an ExecutorService. By default a ThreadPoolExecutor is used that has as many thread defined - * as the system has processors. - * <p> - * The optimization can be aborted by interrupting the current thread. - */ -public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Statistics { - private static final LogHelper log = Application.getLogger(); - - private List<Point> simplex = new ArrayList<Point>(); - - private ParallelFunctionCache functionExecutor; - - private boolean useExpansion = false; - private boolean useCoordinateSearch = false; - - private int stepCount = 0; - private int reflectionAcceptance = 0; - private int expansionAcceptance = 0; - private int coordinateAcceptance = 0; - private int reductionFallback = 0; - - - public MultidirectionalSearchOptimizer() { - // No-op - } - - public MultidirectionalSearchOptimizer(ParallelFunctionCache functionCache) { - this.functionExecutor = functionCache; - } - - - - @Override - public void optimize(Point initial, OptimizationController control) throws OptimizationException { - FunctionCacheComparator comparator = new FunctionCacheComparator(functionExecutor); - - final List<Point> pattern = SearchPattern.square(initial.dim()); - log.info("Starting optimization at " + initial + " with pattern " + pattern); - - try { - - boolean simplexComputed = false; - double step = 0.5; - - // Set up the current simplex - simplex.clear(); - simplex.add(initial); - for (Point p : pattern) { - simplex.add(initial.add(p.mul(step))); - } - - // Normal iterations - List<Point> reflection = new ArrayList<Point>(simplex.size()); - List<Point> expansion = new ArrayList<Point>(simplex.size()); - List<Point> coordinateSearch = new ArrayList<Point>(simplex.size()); - Point current; - double currentValue; - boolean continueOptimization = true; - while (continueOptimization) { - - log.debug("Starting optimization step with simplex " + simplex + - (simplexComputed ? "" : " (not computed)")); - stepCount++; - - if (!simplexComputed) { - // TODO: Could something be computed in parallel? - functionExecutor.compute(simplex); - functionExecutor.waitFor(simplex); - Collections.sort(simplex, comparator); - simplexComputed = true; - } - - current = simplex.get(0); - currentValue = functionExecutor.getValue(current); - - /* - * Compute and queue the next points in likely order of usefulness. - * Expansion is unlikely as we're mainly dealing with bounded optimization. - */ - createReflection(simplex, reflection); - if (useCoordinateSearch) - createCoordinateSearch(current, step, coordinateSearch); - if (useExpansion) - createExpansion(simplex, expansion); - - functionExecutor.compute(reflection); - if (useCoordinateSearch) - functionExecutor.compute(coordinateSearch); - if (useExpansion) - functionExecutor.compute(expansion); - - // Check reflection acceptance - log.debug("Computing reflection"); - functionExecutor.waitFor(reflection); - - if (accept(reflection, currentValue)) { - - log.debug("Reflection was successful, aborting coordinate search, " + - (useExpansion ? "computing" : "skipping") + " expansion"); - - if (useCoordinateSearch) - functionExecutor.abort(coordinateSearch); - - simplex.clear(); - simplex.add(current); - simplex.addAll(reflection); - Collections.sort(simplex, comparator); - - if (useExpansion) { - - /* - * Assume expansion to be unsuccessful, queue next reflection while computing expansion. - */ - createReflection(simplex, reflection); - - functionExecutor.compute(reflection); - functionExecutor.waitFor(expansion); - - if (accept(expansion, currentValue)) { - log.debug("Expansion was successful, aborting reflection"); - functionExecutor.abort(reflection); - - simplex.clear(); - simplex.add(current); - simplex.addAll(expansion); - step *= 2; - Collections.sort(simplex, comparator); - expansionAcceptance++; - } else { - log.debug("Expansion failed"); - reflectionAcceptance++; - } - - } else { - reflectionAcceptance++; - } - - } else { - - log.debug("Reflection was unsuccessful, aborting expansion, computing coordinate search"); - - functionExecutor.abort(expansion); - - /* - * Assume coordinate search to be unsuccessful, queue contraction step while computing. - */ - halveStep(simplex); - functionExecutor.compute(simplex); - - if (useCoordinateSearch) { - functionExecutor.waitFor(coordinateSearch); - - if (accept(coordinateSearch, currentValue)) { - - log.debug("Coordinate search successful, reseting simplex"); - List<Point> toAbort = new LinkedList<Point>(simplex); - simplex.clear(); - simplex.add(current); - for (Point p : pattern) { - simplex.add(current.add(p.mul(step))); - } - toAbort.removeAll(simplex); - functionExecutor.abort(toAbort); - simplexComputed = false; - coordinateAcceptance++; - - } else { - log.debug("Coordinate search unsuccessful, halving step."); - step /= 2; - simplexComputed = false; - reductionFallback++; - } - } else { - log.debug("Coordinate search not used, halving step."); - step /= 2; - simplexComputed = false; - reductionFallback++; - } - - } - - log.debug("Ending optimization step with simplex " + simplex); - - continueOptimization = control.stepTaken(current, currentValue, simplex.get(0), - functionExecutor.getValue(simplex.get(0)), step); - - if (Thread.interrupted()) { - throw new InterruptedException(); - } - - } - - } catch (InterruptedException e) { - log.info("Optimization was interrupted with InterruptedException"); - } - - log.info("Finishing optimization at point " + simplex.get(0) + " value = " + - functionExecutor.getValue(simplex.get(0))); - log.info("Optimization statistics: " + getStatistics()); - } - - - - private void createReflection(List<Point> base, List<Point> reflection) { - Point current = base.get(0); - reflection.clear(); - - /* new = - (old - current) + current = 2*current - old */ - for (int i = 1; i < base.size(); i++) { - Point p = base.get(i); - p = current.mul(2).sub(p); - reflection.add(p); - } - } - - private void createExpansion(List<Point> base, List<Point> expansion) { - Point current = base.get(0); - expansion.clear(); - for (int i = 1; i < base.size(); i++) { - Point p = current.mul(3).sub(base.get(i).mul(2)); - expansion.add(p); - } - } - - private void halveStep(List<Point> base) { - Point current = base.get(0); - for (int i = 1; i < base.size(); i++) { - Point p = base.get(i); - - /* new = (old - current)*0.5 + current = old*0.5 + current*0.5 = (old + current)*0.5 */ - - p = p.add(current).mul(0.5); - base.set(i, p); - } - } - - private void createCoordinateSearch(Point current, double step, List<Point> coordinateDirections) { - coordinateDirections.clear(); - for (int i = 0; i < current.dim(); i++) { - Point p = new Point(current.dim()); - p = p.set(i, step); - coordinateDirections.add(current.add(p)); - coordinateDirections.add(current.sub(p)); - } - } - - - private boolean accept(List<Point> points, double currentValue) { - for (Point p : points) { - if (functionExecutor.getValue(p) < currentValue) { - return true; - } - } - return false; - } - - - - @Override - public Point getOptimumPoint() { - if (simplex.size() == 0) { - throw new IllegalStateException("Optimization has not been called, simplex is empty"); - } - return simplex.get(0); - } - - @Override - public double getOptimumValue() { - return functionExecutor.getValue(getOptimumPoint()); - } - - @Override - public FunctionCache getFunctionCache() { - return functionExecutor; - } - - @Override - public void setFunctionCache(FunctionCache functionCache) { - if (!(functionCache instanceof ParallelFunctionCache)) { - throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache); - } - this.functionExecutor = (ParallelFunctionCache) functionCache; - } - - @Override - public String getStatistics() { - return "MultidirectionalSearchOptimizer[stepCount=" + stepCount + - ", reflectionAcceptance=" + reflectionAcceptance + - ", expansionAcceptance=" + expansionAcceptance + - ", coordinateAcceptance=" + coordinateAcceptance + - ", reductionFallback=" + reductionFallback; - } - - @Override - public void resetStatistics() { - stepCount = 0; - reflectionAcceptance = 0; - expansionAcceptance = 0; - coordinateAcceptance = 0; - reductionFallback = 0; - } - -} diff --git a/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java b/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java deleted file mode 100644 index 1a7df333..00000000 --- a/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java +++ /dev/null @@ -1,95 +0,0 @@ -package net.sf.openrocket.optimization.general.multidim; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.util.MathUtil; - -/** - * A helper class to create search patterns. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SearchPattern { - - /** - * Create a square search pattern with the specified dimensionality. - * - * @param dimensionality the dimensionality - */ - public static List<Point> square(int dimensionality) { - List<Point> pattern = new ArrayList<Point>(dimensionality); - - for (int i = 0; i < dimensionality; i++) { - double[] p = new double[dimensionality]; - p[i] = 1.0; - pattern.add(new Point(p)); - } - return pattern; - } - - - - /** - * Create a regular simplex search pattern with the specified dimensionality. - * - * @param dimensionality the dimensionality - */ - public static List<Point> regularSimplex(int dimensionality) { - if (dimensionality <= 0) { - throw new IllegalArgumentException("Illegal dimensionality " + dimensionality); - } - - List<Point> pattern = new ArrayList<Point>(dimensionality); - - double[] coordinates = new double[dimensionality]; - double dot = -1.0 / dimensionality; - - /* - * First construct an origin-centered regular simplex. - * http://en.wikipedia.org/wiki/Simplex#Cartesian_coordinates_for_regular_n-dimensional_simplex_in_Rn - */ - - for (int i = 0; i < dimensionality; i++) { - // Compute the next point coordinate - double value = 1; - - for (int j = 0; j < i; j++) { - value -= MathUtil.pow2(coordinates[j]); - } - value = MathUtil.safeSqrt(value); - - coordinates[i] = value; - pattern.add(new Point(coordinates)); - - // Compute the i-coordinate for all next points - value = dot; - for (int j = 0; j < i; j++) { - value -= MathUtil.pow2(coordinates[j]); - } - value = value / coordinates[i]; - - coordinates[i] = value; - } - - // Minimum point - Point min = pattern.get(dimensionality - 1); - min = min.set(dimensionality - 1, -min.get(dimensionality - 1)); - - - /* - * Shift simplex to have a corner at the origin and scale to unit length. - */ - if (dimensionality > 1) { - double scale = 1.0 / (pattern.get(1).sub(pattern.get(0)).length()); - for (int i = 0; i < dimensionality; i++) { - Point p = pattern.get(i); - p = p.sub(min).mul(scale); - pattern.set(i, p); - } - } - - return pattern; - } -} diff --git a/src/net/sf/openrocket/optimization/general/onedim/GoldenSectionSearchOptimizer.java b/src/net/sf/openrocket/optimization/general/onedim/GoldenSectionSearchOptimizer.java deleted file mode 100644 index 906eea57..00000000 --- a/src/net/sf/openrocket/optimization/general/onedim/GoldenSectionSearchOptimizer.java +++ /dev/null @@ -1,272 +0,0 @@ -package net.sf.openrocket.optimization.general.onedim; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.FunctionCache; -import net.sf.openrocket.optimization.general.FunctionOptimizer; -import net.sf.openrocket.optimization.general.OptimizationController; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.ParallelFunctionCache; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Statistics; - -/** - * An implementation of a one-dimensional golden section search method - * (see e.g. Nonlinear programming, Bazaraa, Sherall, Shetty, 2nd edition, p. 270) - * <p> - * This implementation attempts to guess future evaluations and computes them in parallel - * with the next point. - * <p> - * The optimization can be aborted by interrupting the current thread. - */ -public class GoldenSectionSearchOptimizer implements FunctionOptimizer, Statistics { - private static final LogHelper log = Application.getLogger(); - - private static final double ALPHA = (Math.sqrt(5) - 1) / 2.0; - - - private ParallelFunctionCache functionExecutor; - - private Point current = null; - - private int guessSuccess = 0; - private int guessFailure = 0; - - - /** - * Construct an optimizer with no function executor. - */ - public GoldenSectionSearchOptimizer() { - // No-op - } - - /** - * Construct an optimizer. - * - * @param functionExecutor the function executor. - */ - public GoldenSectionSearchOptimizer(ParallelFunctionCache functionExecutor) { - super(); - this.functionExecutor = functionExecutor; - } - - @Override - public void optimize(Point initial, OptimizationController control) throws OptimizationException { - - if (initial.dim() != 1) { - throw new IllegalArgumentException("Only single-dimensional optimization supported, dim=" + - initial.dim()); - } - - log.info("Starting golden section search for optimization"); - - Point guessAC = null; - Point guessBD = null; - - try { - boolean guessedAC; - - Point previous = p(0); - double previousValue = Double.NaN; - current = previous; - double currentValue = Double.NaN; - - /* - * Initialize the points + computation. - */ - Point a = p(0); - Point d = p(1.0); - Point b = section1(a, d); - Point c = section2(a, d); - - functionExecutor.compute(a); - functionExecutor.compute(d); - functionExecutor.compute(b); - functionExecutor.compute(c); - - // Wait for points a and d, which normally are already precomputed - functionExecutor.waitFor(a); - functionExecutor.waitFor(d); - - boolean continueOptimization = true; - while (continueOptimization) { - - /* - * Get values at A & D for guessing. - * These are pre-calculated except during the first step. - */ - double fa, fd; - fa = functionExecutor.getValue(a); - fd = functionExecutor.getValue(d); - - - /* - * Start calculating possible two next points. The order of evaluation - * is selected based on the function values at A and D. - */ - guessAC = section1(a, c); - guessBD = section2(b, d); - if (Double.isNaN(fd) || fa < fd) { - guessedAC = true; - functionExecutor.compute(guessAC); - functionExecutor.compute(guessBD); - } else { - guessedAC = false; - functionExecutor.compute(guessBD); - functionExecutor.compute(guessAC); - } - - - /* - * Get values at B and C. - */ - double fb, fc; - functionExecutor.waitFor(b); - functionExecutor.waitFor(c); - fb = functionExecutor.getValue(b); - fc = functionExecutor.getValue(c); - - double min = MathUtil.min(fa, fb, fc, fd); - if (Double.isNaN(min)) { - throw new OptimizationException("Unable to compute initial function values"); - } - - - /* - * Update previous and current values for step control. - */ - previousValue = currentValue; - currentValue = min; - previous = current; - if (min == fa) { - current = a; - } else if (min == fb) { - current = b; - } else if (min == fc) { - current = c; - } else { - current = d; - } - - - /* - * Select next positions. These are already being calculated in the background - * as guessAC and guessBD. - */ - if (min == fa || min == fb) { - d = c; - c = b; - b = guessAC; - functionExecutor.abort(guessBD); - guessBD = null; - log.debug("Selecting A-C region, a=" + a.get(0) + " c=" + c.get(0)); - if (guessedAC) { - guessSuccess++; - } else { - guessFailure++; - } - } else { - a = b; - b = c; - c = guessBD; - functionExecutor.abort(guessAC); - guessAC = null; - log.debug("Selecting B-D region, b=" + b.get(0) + " d=" + d.get(0)); - if (!guessedAC) { - guessSuccess++; - } else { - guessFailure++; - } - } - - - /* - * Check optimization control. - */ - continueOptimization = control.stepTaken(previous, previousValue, - current, currentValue, c.get(0) - a.get(0)); - - if (Thread.interrupted()) { - throw new InterruptedException(); - } - - } - - - } catch (InterruptedException e) { - log.info("Optimization was interrupted with InterruptedException"); - } - - if (guessAC != null) { - functionExecutor.abort(guessAC); - } - if (guessBD != null) { - functionExecutor.abort(guessBD); - } - - - log.info("Finishing optimization at point " + getOptimumPoint() + " value " + getOptimumValue()); - log.info("Optimization statistics: " + getStatistics()); - } - - - private Point p(double v) { - return new Point(v); - } - - - private Point section1(Point a, Point b) { - double va = a.get(0); - double vb = b.get(0); - return p(va + (1 - ALPHA) * (vb - va)); - } - - private Point section2(Point a, Point b) { - double va = a.get(0); - double vb = b.get(0); - return p(va + ALPHA * (vb - va)); - } - - - - @Override - public Point getOptimumPoint() { - return current; - } - - - @Override - public double getOptimumValue() { - if (getOptimumPoint() == null) { - return Double.NaN; - } - return functionExecutor.getValue(getOptimumPoint()); - } - - @Override - public FunctionCache getFunctionCache() { - return functionExecutor; - } - - @Override - public void setFunctionCache(FunctionCache functionCache) { - if (!(functionCache instanceof ParallelFunctionCache)) { - throw new IllegalArgumentException("Function cache needs to be a ParallelFunctionCache: " + functionCache); - } - this.functionExecutor = (ParallelFunctionCache) functionCache; - } - - @Override - public String getStatistics() { - return String.format("Guess hit rate %d/%d = %.3f", guessSuccess, guessSuccess + guessFailure, - ((double) guessSuccess) / (guessSuccess + guessFailure)); - } - - @Override - public void resetStatistics() { - guessSuccess = 0; - guessFailure = 0; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java deleted file mode 100644 index 2acdb049..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.unit.UnitGroup; - -/** - * A parameter of a rocket or simulation that can be optimized - * (for example max. altitude or velocity). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface OptimizableParameter { - - /** - * Return the label name for this optimization parameter. - * - * @return the name for the optimization parameter (e.g. "Flight altitude") - */ - public String getName(); - - /** - * Compute the value for this optimization parameter for the simulation. - * The return value can be any double value. - * <p> - * This method can return NaN in case of a problem computing - * - * @param simulation the simulation - * @return the parameter value (any double value) - * @throws OptimizationException if an error occurs preventing the optimization from continuing - */ - public double computeValue(Simulation simulation) throws OptimizationException, InterruptedException; - - - /** - * Return the unit group associated with the computed value. - * @return the unit group of the computed value. - */ - public UnitGroup getUnitGroup(); - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java deleted file mode 100644 index f7884046..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizationGoal.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization; - -/** - * A goal for an optimization process, for example minimizing, maximizing or seeking - * a specific parameter value. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface OptimizationGoal { - - /** - * Compute a value which, when minimized, yields the desired goal of the - * optimization problem. - * - * @param value the function actual value - * @return the value to minimize to reach the goal - */ - double getMinimizationParameter(double value); - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java deleted file mode 100644 index 2f224269..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java +++ /dev/null @@ -1,186 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.Function; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.Pair; - -/** - * A Function that optimizes a specific RocketOptimizationParameter to some goal - * by modifying a base simulation using SimulationModifiers. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocketOptimizationFunction implements Function { - private static final LogHelper log = Application.getLogger(); - - private static final double OUTSIDE_DOMAIN_SCALE = 1.0e200; - - /* - * NOTE: This class must be thread-safe!!! - */ - - private final Simulation baseSimulation; - private final OptimizableParameter parameter; - private final OptimizationGoal goal; - private final SimulationDomain domain; - private final SimulationModifier[] modifiers; - - - private final List<RocketOptimizationListener> listeners = new ArrayList<RocketOptimizationListener>(); - - - /** - * Sole constructor. - * <p> - * The dimensionality of the resulting function is the same as the length of the - * modifiers array. - * - * @param baseSimulation the base simulation to modify - * @param parameter the rocket parameter to optimize - * @param goal the goal of the rocket parameter - * @param modifiers the modifiers that modify the simulation - */ - public RocketOptimizationFunction(Simulation baseSimulation, OptimizableParameter parameter, - OptimizationGoal goal, SimulationDomain domain, SimulationModifier... modifiers) { - this.baseSimulation = baseSimulation; - this.parameter = parameter; - this.goal = goal; - this.domain = domain; - this.modifiers = modifiers.clone(); - if (modifiers.length == 0) { - throw new IllegalArgumentException("No SimulationModifiers specified"); - } - } - - - @Override - public double evaluate(Point point) throws InterruptedException, OptimizationException { - - /* - * parameterValue is the computed parameter value (e.g. altitude) - * goalValue is the value that needs to be minimized - */ - double goalValue, parameterValue; - - log.debug("Computing optimization function value at point " + point); - - // Create the new simulation based on the point - double[] p = point.asArray(); - if (p.length != modifiers.length) { - throw new IllegalArgumentException("Point has length " + p.length + " while function has " + - modifiers.length + " simulation modifiers"); - } - - Simulation simulation = newSimulationInstance(baseSimulation); - for (int i = 0; i < modifiers.length; i++) { - modifiers[i].modify(simulation, p[i]); - } - - - // Check whether the point is within the simulation domain - Pair<Double, Value> d = domain.getDistanceToDomain(simulation); - double distance = d.getU(); - Value referenceValue = d.getV(); - if (distance > 0 || Double.isNaN(distance)) { - if (Double.isNaN(distance)) { - goalValue = Double.MAX_VALUE; - } else { - goalValue = (distance + 1) * OUTSIDE_DOMAIN_SCALE; - } - log.debug("Optimization point is outside of domain, distance=" + distance + " goal function value=" + goalValue); - - fireEvent(simulation, point, referenceValue, null, goalValue); - - return goalValue; - } - - - // Compute the optimization value - parameterValue = parameter.computeValue(simulation); - goalValue = goal.getMinimizationParameter(parameterValue); - - if (Double.isNaN(goalValue)) { - log.warn("Computed goal value was NaN, baseSimulation=" + baseSimulation + " parameter=" + parameter + - " goal=" + goal + " modifiers=" + Arrays.toString(modifiers) + " simulation=" + simulation + - " parameter value=" + parameterValue); - goalValue = Double.MAX_VALUE; - } - - log.verbose("Parameter value at point " + point + " is " + parameterValue + ", goal function value=" + goalValue); - - fireEvent(simulation, point, referenceValue, new Value(parameterValue, parameter.getUnitGroup().getDefaultUnit()), - goalValue); - - return goalValue; - } - - - - - - /** - * Returns a new deep copy of the simulation and rocket. This methods performs - * synchronization on the simulation for thread protection. - * <p> - * Note: This method is package-private for unit testing purposes. - * - * @return a new deep copy of the simulation and rocket - */ - Simulation newSimulationInstance(Simulation simulation) { - synchronized (baseSimulation) { - Rocket newRocket = simulation.getRocket().copyWithOriginalID(); - Simulation newSimulation = simulation.duplicateSimulation(newRocket); - return newSimulation; - } - } - - - /** - * Add a listener to this function. The listener will be notified each time the - * function is successfully evaluated. - * <p> - * Note that the listener may be called from other threads and must be thread-safe! - * - * @param listener the listener to add. - */ - public void addRocketOptimizationListener(RocketOptimizationListener listener) { - listeners.add(listener); - } - - public void removeRocketOptimizationListener(RocketOptimizationListener listener) { - listeners.remove(listener); - } - - - - private void fireEvent(Simulation simulation, Point p, Value domainReference, Value parameterValue, double goalValue) - throws OptimizationException { - - if (listeners.isEmpty()) { - return; - } - - - Value[] values = new Value[p.dim()]; - for (int i = 0; i < values.length; i++) { - double value = modifiers[i].getCurrentSIValue(simulation); - UnitGroup unit = modifiers[i].getUnitGroup(); - values[i] = new Value(value, unit.getDefaultUnit()); - } - - for (RocketOptimizationListener l : listeners) { - l.evaluated(p, values, domainReference, parameterValue, goalValue); - } - } -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java deleted file mode 100644 index 05fc429b..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization; - -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.unit.Value; - -/** - * A listener for the progress of rocket optimization. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface RocketOptimizationListener { - - /** - * Called after successful function evaluation. - * - * @param point the optimization point. - * @param state the values to which the rocket has been modified in SI units, in the order of "point". - * @param domainReference the domain reference description (or null if unavailable) - * @param parameterValue the parameter value (or NaN if unavailable) - * @param goalValue the goal value (return value of the function) - */ - public void evaluated(Point point, Value[] state, Value domainReference, Value parameterValue, double goalValue); - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java deleted file mode 100644 index d2a204ec..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.Pair; - -/** - * An interface defining a function domain which limits allowed function values. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface SimulationDomain { - - /** - * Return a value determining whether the simulation is within the domain limits - * of an optimization process. If the returned value is negative or zero, the - * simulation is within the domain; if the value is positive, the returned value - * is an indication of how far from the domain the value is; if the returned value - * is NaN, the simulation is outside of the domain. - * - * @param simulation the simulation to check. - * @return A pair indicating the status. The first element is the reference value, - * a negative value or zero if the simulation is in the domain, - * a positive value or NaN if not. The second is the value - * indication of the domain (may be null). - */ - public Pair<Double, Value> getDistanceToDomain(Simulation simulation); - - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java deleted file mode 100644 index 6ed85a6e..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.ChangeSource; - -/** - * An interface what modifies a single parameter in a rocket simulation - * based on a double value in the range [0...1]. - * <p> - * The implementation must fire change events when the minimum and maximum ranges - * are modified, NOT when the actual modified value changes. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface SimulationModifier extends ChangeSource { - - /** - * Return a short name describing this modifier. - * @return a name describing this modifier. - */ - public String getName(); - - /** - * Return a longer description describing this modifiers. - * @return a description of the modifier. - */ - public String getDescription(); - - /** - * Return the object this modifier is related to. This is for example the - * rocket component this modifier is modifying. This object can be used by a - * UI to group related modifiers. - * - * @return the object this modifier is related to, or <code>null</code>. - */ - public Object getRelatedObject(); - - - /** - * Return the current value of the modifier in SI units. - * @return the current value of this parameter in SI units. - * @throws OptimizationException if fetching the current value fails - */ - public double getCurrentSIValue(Simulation simulation) throws OptimizationException; - - - /** - * Return the minimum value (corresponding to scaled value 0) in SI units. - * @return the value corresponding to scaled value 0. - */ - public double getMinValue(); - - /** - * Set the minimum value (corresponding to scaled value 0) in SI units. - * @param value the value corresponding to scaled value 0. - */ - public void setMinValue(double value); - - - /** - * Return the maximum value (corresponding to scaled value 1) in SI units. - * @return the value corresponding to scaled value 1. - */ - public double getMaxValue(); - - /** - * Set the maximum value (corresponding to scaled value 1) in SI units. - * @param value the value corresponding to scaled value 1. - */ - public void setMaxValue(double value); - - - /** - * Return the unit group used for the values returned by {@link #getCurrentSIValue(Simulation)} etc. - * @return the unit group - */ - public UnitGroup getUnitGroup(); - - - /** - * Return the current scaled value. This is normally within the range [0...1], but - * can be outside the range if the current value is outside of the min and max values. - * - * @return the current value of this parameter (normally between [0 ... 1]) - * @throws OptimizationException if fetching the current value fails - */ - public double getCurrentScaledValue(Simulation simulation) throws OptimizationException; - - - - /** - * Modify the specified simulation to the corresponding parameter value. - * - * @param simulation the simulation to modify - * @param scaledValue the scaled value in the range [0...1] - * @throws OptimizationException if the modification fails - */ - public void modify(Simulation simulation, double scaledValue) throws OptimizationException; - - - /** - * Compare whether this SimulationModifier is equivalent to another simulation modifier. - * "Equivalent" means that the simulation modifier corresponds to the same modification in - * another rocket instance (e.g. the same modification on another rocket component that - * has the same component ID). - */ - public boolean equals(Object obj); -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java b/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java deleted file mode 100644 index fac678ea..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java +++ /dev/null @@ -1,20 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.domains; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.Pair; - -/** - * A simulation domain that includes all points in the domain. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class IdentitySimulationDomain implements SimulationDomain { - - @Override - public Pair<Double, Value> getDistanceToDomain(Simulation simulation) { - return new Pair<Double, Value>(-1.0, null); - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java b/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java deleted file mode 100644 index 631b45c9..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java +++ /dev/null @@ -1,139 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.domains; - -import net.sf.openrocket.aerodynamics.AerodynamicCalculator; -import net.sf.openrocket.aerodynamics.BarrowmanCalculator; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; -import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.unit.Value; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Pair; - -/** - * A simulation domain that limits the required stability of the rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class StabilityDomain implements SimulationDomain { - - private final double minimum; - private final boolean minAbsolute; - private final double maximum; - private final boolean maxAbsolute; - - - /** - * Sole constructor. - * - * @param minimum minimum stability requirement (or <code>NaN</code> for no limit) - * @param minAbsolute <code>true</code> if minimum is an absolute SI measurement, - * <code>false</code> if it is relative to the rocket caliber - * @param maximum maximum stability requirement (or <code>NaN</code> for no limit) - * @param maxAbsolute <code>true</code> if maximum is an absolute SI measurement, - * <code>false</code> if it is relative to the rocket caliber - */ - public StabilityDomain(double minimum, boolean minAbsolute, double maximum, boolean maxAbsolute) { - super(); - this.minimum = minimum; - this.minAbsolute = minAbsolute; - this.maximum = maximum; - this.maxAbsolute = maxAbsolute; - } - - - - - @Override - public Pair<Double, Value> getDistanceToDomain(Simulation simulation) { - Coordinate cp, cg; - double cpx, cgx; - double absolute; - double relative; - - /* - * These are instantiated each time because this class must be thread-safe. - * Caching would in any case be inefficient since the rocket changes all the time. - */ - AerodynamicCalculator aerodynamicCalculator = new BarrowmanCalculator(); - MassCalculator massCalculator = new BasicMassCalculator(); - - - Configuration configuration = simulation.getConfiguration(); - FlightConditions conditions = new FlightConditions(configuration); - conditions.setMach(Application.getPreferences().getDefaultMach()); - conditions.setAOA(0); - conditions.setRollRate(0); - - // TODO: HIGH: This re-calculates the worst theta value every time - cp = aerodynamicCalculator.getWorstCP(configuration, conditions, null); - cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); - - if (cp.weight > 0.000001) - cpx = cp.x; - else - cpx = Double.NaN; - - if (cg.weight > 0.000001) - cgx = cg.x; - else - cgx = Double.NaN; - - - // Calculate the reference (absolute or relative) - absolute = cpx - cgx; - - double diameter = 0; - for (RocketComponent c : configuration) { - if (c instanceof SymmetricComponent) { - double d1 = ((SymmetricComponent) c).getForeRadius() * 2; - double d2 = ((SymmetricComponent) c).getAftRadius() * 2; - diameter = MathUtil.max(diameter, d1, d2); - } - } - relative = absolute / diameter; - - - Value desc; - if (minAbsolute && maxAbsolute) { - desc = new Value(absolute, UnitGroup.UNITS_LENGTH); - } else { - desc = new Value(relative, UnitGroup.UNITS_STABILITY_CALIBERS); - } - - double ref; - if (minAbsolute) { - ref = minimum - absolute; - if (ref > 0) { - return new Pair<Double, Value>(ref, desc); - } - } else { - ref = minimum - relative; - if (ref > 0) { - return new Pair<Double, Value>(ref, desc); - } - } - - if (maxAbsolute) { - ref = absolute - maximum; - if (ref > 0) { - return new Pair<Double, Value>(ref, desc); - } - } else { - ref = relative - maximum; - if (ref > 0) { - return new Pair<Double, Value>(ref, desc); - } - } - - return new Pair<Double, Value>(0.0, desc); - } -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java deleted file mode 100644 index e9576eea..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/goals/MaximizationGoal.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.goals; - -import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; - -/** - * An optimization goal that maximizes a function value. The method simply - * returns the opposite of the function value. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MaximizationGoal implements OptimizationGoal { - - @Override - public double getMinimizationParameter(double value) { - return -value; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java deleted file mode 100644 index c6f8fc60..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/goals/MinimizationGoal.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.goals; - -import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; - -/** - * An optimization goal that minimizes a function value. The method simply - * returns the function value. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MinimizationGoal implements OptimizationGoal { - - @Override - public double getMinimizationParameter(double value) { - return value; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java b/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java deleted file mode 100644 index 34d433ab..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/goals/ValueSeekGoal.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.goals; - -import net.sf.openrocket.optimization.rocketoptimization.OptimizationGoal; - - -/** - * An optimization goal that seeks for a specific function value. - * The method returns the Euclidic distance from the desired value. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ValueSeekGoal implements OptimizationGoal { - - private final double goal; - - /** - * Sole constructor. - * - * @param goal the function value to optimize towards. - */ - public ValueSeekGoal(double goal) { - this.goal = goal; - } - - @Override - public double getMinimizationParameter(double value) { - return Math.abs(value - goal); - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java deleted file mode 100644 index 1d8c0fd1..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java +++ /dev/null @@ -1,211 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.modifiers; - -import java.util.ArrayList; -import java.util.EventListener; -import java.util.EventObject; -import java.util.List; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.StateChangeListener; - -/** - * An abstract implementation of the SimulationModifier interface. An implementation - * needs only to implement the {@link #getCurrentSIValue(Simulation)} and - * {@link #modify(net.sf.openrocket.document.Simulation, double)} methods. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class AbstractSimulationModifier implements SimulationModifier { - - private final String name; - private final String description; - private final Object relatedObject; - private final UnitGroup unitGroup; - - private double minValue = 0.0; - private double maxValue = 1.0; - - private final List<EventListener> listeners = new ArrayList<EventListener>(); - - - /** - * Sole constructor. - * - * @param modifierName the name of this modifier (returned by {@link #getName()}) - * @param modifierDescription the description of this modifier (returned by {@link #getDescription()}) - * @param relatedObject the related object (returned by {@link #getRelatedObject()}) - * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) - */ - public AbstractSimulationModifier(String modifierName, String modifierDescription, Object relatedObject, - UnitGroup unitGroup) { - this.name = modifierName; - this.description = modifierDescription; - this.relatedObject = relatedObject; - this.unitGroup = unitGroup; - - if (this.name == null || this.description == null || this.relatedObject == null || this.unitGroup == null) { - throw new IllegalArgumentException("null value provided:" + - " name=" + this.name + " description=" + description + " relatedObject=" + relatedObject + - " unitGroup=" + unitGroup); - } - } - - - @Override - public String getName() { - return name; - } - - @Override - public String getDescription() { - return description; - } - - @Override - public Object getRelatedObject() { - return relatedObject; - } - - @Override - public double getCurrentScaledValue(Simulation simulation) throws OptimizationException { - double value = getCurrentSIValue(simulation); - return toScaledValue(value); - } - - - - /** - * Returns the scaled value (normally within [0...1]). If the min...max range is singular, - * this method returns 0.0, 1.0 or 0.5 depending on whether the value is less than, - * greater than or equal to the limit. - * - * @param value the value in SI units - * @return the value in scaled range (normally within [0...1]) - */ - protected double toScaledValue(double value) { - if (MathUtil.equals(minValue, maxValue)) { - if (value > maxValue) - return 1.0; - if (value < minValue) - return 0.0; - return 0.5; - } - - return MathUtil.map(value, minValue, maxValue, 0.0, 1.0); - } - - - /** - * Returns the base value (in SI units). - * - * @param value the value in scaled range (normally within [0...1]) - * @return the value in SI units - */ - protected double toBaseValue(double value) { - return MathUtil.map(value, 0.0, 1.0, minValue, maxValue); - } - - - - @Override - public double getMinValue() { - return minValue; - } - - @Override - public void setMinValue(double value) { - if (MathUtil.equals(minValue, value)) - return; - this.minValue = value; - if (maxValue < minValue) - maxValue = minValue; - fireChangeEvent(); - } - - @Override - public double getMaxValue() { - return maxValue; - } - - @Override - public void setMaxValue(double value) { - if (MathUtil.equals(maxValue, value)) - return; - this.maxValue = value; - if (minValue > maxValue) - minValue = maxValue; - fireChangeEvent(); - } - - @Override - public UnitGroup getUnitGroup() { - return unitGroup; - } - - - @Override - public void addChangeListener(EventListener listener) { - listeners.add(listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - listeners.remove(listener); - } - - - /** - * Fire a change event to the listeners. - */ - protected void fireChangeEvent() { - EventObject event = new EventObject(this); - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] list = listeners.toArray(new EventListener[0]); - for (EventListener l : list) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(event); - } - } - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - - AbstractSimulationModifier other = (AbstractSimulationModifier) obj; - if (!this.description.equals(other.description)) - return false; - if (!this.name.equals(other.name)) - return false; - if (!this.relatedObject.equals(other.relatedObject)) - return false; - if (!this.unitGroup.equals(other.unitGroup)) - return false; - - return true; - } - - - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (description.hashCode()); - result = prime * result + (name.hashCode()); - result = prime * result + (relatedObject.hashCode()); - result = prime * result + (unitGroup.hashCode()); - return result; - } - - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java deleted file mode 100644 index bd80889a..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java +++ /dev/null @@ -1,50 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.modifiers; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.unit.UnitGroup; - -/** - * A generic simulation modifier that modifies a value of a certain RocketComponent - * based on the component's ID and the value name. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class GenericComponentModifier extends GenericModifier<RocketComponent> { - - private final Class<? extends RocketComponent> componentClass; - private final String componentId; - - /** - * Sole constructor. - * - * @param modifierName the name of this modifier (returned by {@link #getName()}) - * @param modifierDescription the description of this modifier (returned by {@link #getDescription()}) - * @param relatedObject the related object (returned by {@link #getRelatedObject()}) - * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) - * @param multiplier the multiplier by which the value returned by the getter is multiplied - * to obtain the desired value - * @param componentClass the RocketComponent class type that is being modified - * @param componentId the ID of the component to modify - * @param methodName the base name of the getter/setter methods (without "get"/"set") - */ - public GenericComponentModifier(String modifierName, String modifierDescription, Object relatedObject, UnitGroup unitGroup, - double multiplier, Class<? extends RocketComponent> componentClass, String componentId, String methodName) { - super(modifierName, modifierDescription, relatedObject, unitGroup, multiplier, componentClass, methodName); - - this.componentClass = componentClass; - this.componentId = componentId; - } - - @Override - protected RocketComponent getModifiedObject(Simulation simulation) throws OptimizationException { - RocketComponent c = simulation.getRocket().findComponent(componentId); - if (c == null) { - throw new OptimizationException("Could not find component of type " + componentClass.getSimpleName() - + " with correct ID"); - } - return c; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java deleted file mode 100644 index 07724c8f..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.modifiers; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Reflection.Method; - -/** - * A generic SimulationModifier that uses reflection to get and set a double value. - * Implementations need to implement the {@link #getModifiedObject(Simulation)} method - * to return which object is modified. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class GenericModifier<T> extends AbstractSimulationModifier { - private static final LogHelper log = Application.getLogger(); - - private final double multiplier; - - private final Class<? extends T> modifiedClass; - private final String methodName; - - private final Method getter; - private final Method setter; - - - /** - * Sole constructor. - * - * @param modifierName the name of this modifier (returned by {@link #getName()}) - * @param modifierDescription the description of this modifier (returned by {@link #getDescription()}) - * @param relatedObject the related object (returned by {@link #getRelatedObject()}) - * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) - * @param multiplier the multiplier by which the value returned by the getter is multiplied - * to obtain the desired value - * @param modifiedClass the class type that {@link #getModifiedObject(Simulation)} returns - * @param methodName the base name of the getter/setter methods (without "get"/"set") - */ - public GenericModifier(String modifierName, String modifierDescription, Object relatedObject, UnitGroup unitGroup, - double multiplier, Class<? extends T> modifiedClass, String methodName) { - super(modifierName, modifierDescription, relatedObject, unitGroup); - this.multiplier = multiplier; - - this.modifiedClass = modifiedClass; - this.methodName = methodName; - - if (MathUtil.equals(multiplier, 0)) { - throw new IllegalArgumentException("multiplier is zero"); - } - - try { - methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1); - getter = new Method(modifiedClass.getMethod("get" + methodName)); - setter = new Method(modifiedClass.getMethod("set" + methodName, double.class)); - } catch (SecurityException e) { - throw new BugException("Trying to find method get/set" + methodName + " in class " + modifiedClass, e); - } catch (NoSuchMethodException e) { - throw new BugException("Trying to find method get/set" + methodName + " in class " + modifiedClass, e); - } - } - - - - @Override - public double getCurrentSIValue(Simulation simulation) throws OptimizationException { - T modifiable = getModifiedObject(simulation); - if (modifiable == null) { - throw new OptimizationException("BUG: getModifiedObject() returned null"); - } - return ((Double) getter.invoke(modifiable)) * multiplier; - } - - - @Override - public void modify(Simulation simulation, double scaledValue) throws OptimizationException { - T modifiable = getModifiedObject(simulation); - if (modifiable == null) { - throw new OptimizationException("BUG: getModifiedObject() returned null"); - } - double siValue = toBaseValue(scaledValue) / multiplier; - log.verbose("Setting setter=" + setter + " modifiable=" + modifiable + " siValue=" + siValue + "scaledValue=" + scaledValue); - setter.invoke(modifiable, siValue); - } - - - /** - * Return the object from the simulation that will be modified. - * @param simulation the simulation - * @return the object to modify - * - * @throws OptimizationException if the object cannot be found - */ - protected abstract T getModifiedObject(Simulation simulation) throws OptimizationException; - - - - @Override - public String toString() { - return "GenericModifier[modifiedClass=" + modifiedClass.getCanonicalName() + ", methodName=" + methodName + ", multiplier=" + multiplier + "]"; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/DeploymentVelocityParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/DeploymentVelocityParameter.java deleted file mode 100644 index 0add6648..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/DeploymentVelocityParameter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.system.RecoveryDeviceDeploymentEndListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the total flight time. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DeploymentVelocityParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected SimulationListener[] getSimulationListeners() { - return new SimulationListener[] { new RecoveryDeviceDeploymentEndListener() }; - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_VELOCITY_TOTAL); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_VELOCITY; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java deleted file mode 100644 index 4effe72b..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the speed the rocket hits the ground. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class GroundHitVelocityParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_VELOCITY_TOTAL); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_VELOCITY; - } - - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java deleted file mode 100644 index cb387d71..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the distance where a rocket lands. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class LandingDistanceParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_POSITION_XY); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_DISTANCE; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java deleted file mode 100644 index eb6e7a61..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the maximum acceleration during a simulated flight. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MaximumAccelerationParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected SimulationListener[] getSimulationListeners() { - return new SimulationListener[] { new ApogeeEndListener() }; - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getMaximum(FlightDataType.TYPE_ACCELERATION_TOTAL); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_ACCELERATION; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java deleted file mode 100644 index f427d7fc..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the maximum altitude of a simulated flight. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MaximumAltitudeParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected SimulationListener[] getSimulationListeners() { - return new SimulationListener[] { new ApogeeEndListener() }; - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_DISTANCE; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java deleted file mode 100644 index 3062e4b8..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the maximum velocity during a simulated flight. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MaximumVelocityParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected SimulationListener[] getSimulationListeners() { - return new SimulationListener[] { new ApogeeEndListener() }; - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getMaximum(FlightDataType.TYPE_VELOCITY_TOTAL); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_VELOCITY; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/SimulationBasedParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/SimulationBasedParameter.java deleted file mode 100644 index e814dd30..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/SimulationBasedParameter.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import java.util.Arrays; - -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.exception.MotorIgnitionException; -import net.sf.openrocket.simulation.exception.SimulationCalculationException; -import net.sf.openrocket.simulation.exception.SimulationCancelledException; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.exception.SimulationLaunchException; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.simulation.listeners.system.InterruptListener; -import net.sf.openrocket.startup.Application; - -/** - * An abstract optimization parameter that simulates a rocket flight and obtains - * a value from the simulation result. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class SimulationBasedParameter implements OptimizableParameter { - - private static final LogHelper log = Application.getLogger(); - - @Override - public double computeValue(Simulation simulation) throws OptimizationException, InterruptedException { - try { - log.debug("Running simulation for " + getName()); - - SimulationListener[] listeners = getSimulationListeners(); - listeners = Arrays.copyOf(listeners, listeners.length + 1); - listeners[listeners.length - 1] = new InterruptListener(); - simulation.simulate(listeners); - - double value = getResultValue(simulation.getSimulatedData()); - log.debug("Parameter '" + getName() + " was " + value); - return value; - } catch (MotorIgnitionException e) { - // A problem with motor ignition will cause optimization to fail - throw new OptimizationException(e); - } catch (SimulationLaunchException e) { - // Other launch exceptions result in illegal value - return Double.NaN; - } catch (SimulationCalculationException e) { - // Calculation errors result in illegal value - return Double.NaN; - } catch (SimulationCancelledException e) { - // Simulation cancellation stops the optimization - throw (InterruptedException) new InterruptedException("Optimization was interrupted").initCause(e); - } catch (SimulationException e) { - // Other exceptions fail - throw new OptimizationException(e); - } - } - - - /** - * Return the optimization parameter from the simulation flight data. - * - * @param simulatedData the simulated data. - * @return the optimization parameter. - */ - protected abstract double getResultValue(FlightData simulatedData); - - /** - * Return an array of simulation listeners to provide to the simulation. - * This may include a listener that stops the simulation after the necessary value - * has been computed. - * <p> - * This array should NOT contain InterruptListener, it will be added implicitly. - * - * @return an array of simulation listeners to include. - */ - protected SimulationListener[] getSimulationListeners() { - return new SimulationListener[0]; - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java deleted file mode 100644 index 1d46f753..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.aerodynamics.AerodynamicCalculator; -import net.sf.openrocket.aerodynamics.BarrowmanCalculator; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -/** - * An optimization parameter that computes either the absolute or relative stability of a rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class StabilityParameter implements OptimizableParameter { - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - private final boolean absolute; - - public StabilityParameter(boolean absolute) { - this.absolute = absolute; - } - - - @Override - public String getName() { - return trans.get("name") + " (" + getUnitGroup().getDefaultUnit().getUnit() + ")"; - } - - @Override - public double computeValue(Simulation simulation) throws OptimizationException { - Coordinate cp, cg; - double cpx, cgx; - double stability; - - log.debug("Calculating stability of simulation, absolute=" + absolute); - - /* - * These are instantiated each time because this class must be thread-safe. - * Caching would in any case be inefficient since the rocket changes all the time. - */ - AerodynamicCalculator aerodynamicCalculator = new BarrowmanCalculator(); - MassCalculator massCalculator = new BasicMassCalculator(); - - - Configuration configuration = simulation.getConfiguration(); - FlightConditions conditions = new FlightConditions(configuration); - conditions.setMach(Application.getPreferences().getDefaultMach()); - conditions.setAOA(0); - conditions.setRollRate(0); - - cp = aerodynamicCalculator.getWorstCP(configuration, conditions, null); - cg = massCalculator.getCG(configuration, MassCalcType.LAUNCH_MASS); - - if (cp.weight > 0.000001) - cpx = cp.x; - else - cpx = Double.NaN; - - if (cg.weight > 0.000001) - cgx = cg.x; - else - cgx = Double.NaN; - - - // Calculate the reference (absolute or relative) - stability = cpx - cgx; - - if (!absolute) { - double diameter = 0; - for (RocketComponent c : configuration) { - if (c instanceof SymmetricComponent) { - double d1 = ((SymmetricComponent) c).getForeRadius() * 2; - double d2 = ((SymmetricComponent) c).getAftRadius() * 2; - diameter = MathUtil.max(diameter, d1, d2); - } - } - stability = stability / diameter; - } - - log.debug("Resulting stability is " + stability + ", absolute=" + absolute); - - return stability; - } - - @Override - public UnitGroup getUnitGroup() { - if (absolute) { - return UnitGroup.UNITS_LENGTH; - } else { - return UnitGroup.UNITS_STABILITY_CALIBERS; - } - } - -} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/TotalFlightTimeParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/TotalFlightTimeParameter.java deleted file mode 100644 index 560227a2..00000000 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/TotalFlightTimeParameter.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.optimization.rocketoptimization.parameters; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.simulation.FlightData; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * An optimization parameter that computes the total flight time. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class TotalFlightTimeParameter extends SimulationBasedParameter { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getName() { - return trans.get("name"); - } - - @Override - protected double getResultValue(FlightData simulatedData) { - return simulatedData.getBranch(0).getLast(FlightDataType.TYPE_TIME); - } - - @Override - public UnitGroup getUnitGroup() { - return UnitGroup.UNITS_FLIGHT_TIME; - } - -} diff --git a/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java b/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java deleted file mode 100644 index 7eadfb6a..00000000 --- a/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.sf.openrocket.optimization.services; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.DeploymentVelocityParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.GroundHitVelocityParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.LandingDistanceParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumAccelerationParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumAltitudeParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumVelocityParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.StabilityParameter; -import net.sf.openrocket.optimization.rocketoptimization.parameters.TotalFlightTimeParameter; - -/** - * Default implementation for optimization parameter service. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class DefaultOptimizableParameterService implements OptimizableParameterService { - - @Override - public Collection<OptimizableParameter> getParameters(OpenRocketDocument document) { - List<OptimizableParameter> list = new ArrayList<OptimizableParameter>(); - - list.add(new MaximumAltitudeParameter()); - list.add(new MaximumVelocityParameter()); - list.add(new MaximumAccelerationParameter()); - list.add(new StabilityParameter(false)); - list.add(new StabilityParameter(true)); - list.add(new GroundHitVelocityParameter()); - list.add(new LandingDistanceParameter()); - list.add(new TotalFlightTimeParameter()); - list.add(new DeploymentVelocityParameter()); - - return list; - } - -} diff --git a/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java b/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java deleted file mode 100644 index 565330aa..00000000 --- a/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java +++ /dev/null @@ -1,329 +0,0 @@ -package net.sf.openrocket.optimization.services; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.optimization.rocketoptimization.modifiers.GenericComponentModifier; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.EllipticalFinSet; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.InternalComponent; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.Parachute; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.RecoveryDevice.DeployEvent; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.Streamer; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.Reflection.Method; - -public class DefaultSimulationModifierService implements SimulationModifierService { - - private static final Translator trans = Application.getTranslator(); - - private static final double DEFAULT_RANGE_MULTIPLIER = 2.0; - - - private static final Map<Class<?>, List<ModifierDefinition>> definitions = new HashMap<Class<?>, List<ModifierDefinition>>(); - static { - //addModifier("optimization.modifier.", unitGroup, multiplier, componentClass, methodName); - - /* - * Note: Each component type must contain only mutually exclusive variables. - * For example, body tube does not have inner diameter definition because it is - * defined by the outer diameter and thickness. - */ - - addModifier("optimization.modifier.nosecone.length", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Length"); - addModifier("optimization.modifier.nosecone.diameter", UnitGroup.UNITS_LENGTH, 2.0, NoseCone.class, "AftRadius", "isAftRadiusAutomatic"); - addModifier("optimization.modifier.nosecone.thickness", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Thickness", "isFilled"); - - addModifier("optimization.modifier.transition.length", UnitGroup.UNITS_LENGTH, 1.0, Transition.class, "Length"); - addModifier("optimization.modifier.transition.forediameter", UnitGroup.UNITS_LENGTH, 2.0, Transition.class, "ForeRadius", "isForeRadiusAutomatic"); - addModifier("optimization.modifier.transition.aftdiameter", UnitGroup.UNITS_LENGTH, 2.0, Transition.class, "AftRadius", "isAftRadiusAutomatic"); - addModifier("optimization.modifier.transition.thickness", UnitGroup.UNITS_LENGTH, 1.0, Transition.class, "Thickness", "isFilled"); - - addModifier("optimization.modifier.bodytube.length", UnitGroup.UNITS_LENGTH, 1.0, BodyTube.class, "Length"); - addModifier("optimization.modifier.bodytube.outerDiameter", UnitGroup.UNITS_LENGTH, 2.0, BodyTube.class, "OuterRadius", "isOuterRadiusAutomatic"); - addModifier("optimization.modifier.bodytube.thickness", UnitGroup.UNITS_LENGTH, 1.0, BodyTube.class, "Thickness", "isFilled"); - - addModifier("optimization.modifier.trapezoidfinset.rootChord", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "RootChord"); - addModifier("optimization.modifier.trapezoidfinset.tipChord", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "TipChord"); - addModifier("optimization.modifier.trapezoidfinset.sweep", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "Sweep"); - addModifier("optimization.modifier.trapezoidfinset.height", UnitGroup.UNITS_LENGTH, 1.0, TrapezoidFinSet.class, "Height"); - addModifier("optimization.modifier.finset.cant", UnitGroup.UNITS_ANGLE, 1.0, TrapezoidFinSet.class, "CantAngle"); - - addModifier("optimization.modifier.ellipticalfinset.length", UnitGroup.UNITS_LENGTH, 1.0, EllipticalFinSet.class, "Length"); - addModifier("optimization.modifier.ellipticalfinset.height", UnitGroup.UNITS_LENGTH, 1.0, EllipticalFinSet.class, "Height"); - addModifier("optimization.modifier.finset.cant", UnitGroup.UNITS_ANGLE, 1.0, EllipticalFinSet.class, "CantAngle"); - - addModifier("optimization.modifier.finset.cant", UnitGroup.UNITS_ANGLE, 1.0, FreeformFinSet.class, "CantAngle"); - - addModifier("optimization.modifier.launchlug.length", UnitGroup.UNITS_LENGTH, 1.0, LaunchLug.class, "Length"); - addModifier("optimization.modifier.launchlug.outerDiameter", UnitGroup.UNITS_LENGTH, 2.0, LaunchLug.class, "OuterRadius"); - addModifier("optimization.modifier.launchlug.thickness", UnitGroup.UNITS_LENGTH, 1.0, LaunchLug.class, "Thickness"); - - - addModifier("optimization.modifier.masscomponent.mass", UnitGroup.UNITS_MASS, 1.0, MassComponent.class, "ComponentMass"); - - addModifier("optimization.modifier.parachute.diameter", UnitGroup.UNITS_LENGTH, 1.0, Parachute.class, "Diameter"); - addModifier("optimization.modifier.parachute.coefficient", UnitGroup.UNITS_NONE, 1.0, Parachute.class, "CD"); - - addModifier("optimization.modifier.streamer.length", UnitGroup.UNITS_LENGTH, 1.0, Streamer.class, "StripLength"); - addModifier("optimization.modifier.streamer.width", UnitGroup.UNITS_LENGTH, 1.0, Streamer.class, "StripWidth"); - addModifier("optimization.modifier.streamer.aspectRatio", UnitGroup.UNITS_NONE, 1.0, Streamer.class, "AspectRatio"); - addModifier("optimization.modifier.streamer.coefficient", UnitGroup.UNITS_NONE, 1.0, Streamer.class, "CD", "isCDAutomatic"); - - } - - private static void addModifier(String modifierNameKey, UnitGroup unitGroup, double multiplier, - Class<? extends RocketComponent> componentClass, String methodName) { - addModifier(modifierNameKey, unitGroup, multiplier, componentClass, methodName, null); - } - - private static void addModifier(String modifierNameKey, UnitGroup unitGroup, double multiplier, - Class<? extends RocketComponent> componentClass, String methodName, String autoMethod) { - - String modifierDescriptionKey = modifierNameKey + ".desc"; - - List<ModifierDefinition> list = definitions.get(componentClass); - if (list == null) { - list = new ArrayList<DefaultSimulationModifierService.ModifierDefinition>(); - definitions.put(componentClass, list); - } - - ModifierDefinition definition = new ModifierDefinition(modifierNameKey, modifierDescriptionKey, unitGroup, - multiplier, componentClass, methodName, autoMethod); - list.add(definition); - } - - - - - @Override - public Collection<SimulationModifier> getModifiers(OpenRocketDocument document) { - List<SimulationModifier> modifiers = new ArrayList<SimulationModifier>(); - - Rocket rocket = document.getRocket(); - - // Simulation is used to calculate default min/max values - Simulation simulation = new Simulation(rocket); - simulation.getConfiguration().setMotorConfigurationID(null); - - for (RocketComponent c : rocket) { - - // Attribute modifiers - List<ModifierDefinition> list = definitions.get(c.getClass()); - if (list != null) { - for (ModifierDefinition def : list) { - - // Ignore modifier if value is set to automatic - if (def.autoMethod != null) { - Method m = Reflection.findMethod(c.getClass(), def.autoMethod); - if ((Boolean) m.invoke(c)) { - continue; - } - } - - SimulationModifier mod = new GenericComponentModifier( - trans.get(def.modifierNameKey), trans.get(def.modifierDescriptionKey), c, def.unitGroup, - def.multiplier, def.componentClass, c.getID(), def.methodName); - setDefaultMinMax(mod, simulation); - modifiers.add(mod); - } - } - - - // Add override modifiers if mass/CG is overridden - if (c.isMassOverridden()) { - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.rocketcomponent.overrideMass"), - trans.get("optimization.modifier.rocketcomponent.overrideMass.desc"), - c, UnitGroup.UNITS_MASS, - 1.0, c.getClass(), c.getID(), "OverrideMass"); - setDefaultMinMax(mod, simulation); - modifiers.add(mod); - } - if (c.isCGOverridden()) { - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.rocketcomponent.overrideCG"), - trans.get("optimization.modifier.rocketcomponent.overrideCG.desc"), - c, UnitGroup.UNITS_LENGTH, - 1.0, c.getClass(), c.getID(), "OverrideCGX"); - mod.setMinValue(0); - mod.setMaxValue(c.getLength()); - modifiers.add(mod); - } - - - // Conditional motor mount parameters - if (c instanceof MotorMount) { - MotorMount mount = (MotorMount) c; - if (mount.isMotorMount()) { - - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.motormount.overhang"), - trans.get("optimization.modifier.motormount.overhang.desc"), - c, UnitGroup.UNITS_LENGTH, - 1.0, c.getClass(), c.getID(), "MotorOverhang"); - setDefaultMinMax(mod, simulation); - modifiers.add(mod); - - mod = new GenericComponentModifier( - trans.get("optimization.modifier.motormount.delay"), - trans.get("optimization.modifier.motormount.delay.desc"), - c, UnitGroup.UNITS_SHORT_TIME, - 1.0, c.getClass(), c.getID(), "IgnitionDelay"); - mod.setMinValue(0); - mod.setMaxValue(5); - modifiers.add(mod); - - } - } - - - // Inner component positioning - if (c instanceof InternalComponent) { - RocketComponent parent = c.getParent(); - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.internalcomponent.position"), - trans.get("optimization.modifier.internalcomponent.position.desc"), - c, UnitGroup.UNITS_LENGTH, - 1.0, c.getClass(), c.getID(), "PositionValue"); - mod.setMinValue(0); - mod.setMaxValue(parent.getLength()); - modifiers.add(mod); - } - - - // Custom min/max for fin set position - if (c instanceof FinSet) { - RocketComponent parent = c.getParent(); - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.finset.position"), - trans.get("optimization.modifier.finset.position.desc"), - c, UnitGroup.UNITS_LENGTH, - 1.0, c.getClass(), c.getID(), "PositionValue"); - mod.setMinValue(0); - mod.setMaxValue(parent.getLength()); - modifiers.add(mod); - } - - - // Custom min/max for launch lug position - if (c instanceof LaunchLug) { - RocketComponent parent = c.getParent(); - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.launchlug.position"), - trans.get("optimization.modifier.launchlug.position.desc"), - c, UnitGroup.UNITS_LENGTH, - 1.0, c.getClass(), c.getID(), "PositionValue"); - mod.setMinValue(0); - mod.setMaxValue(parent.getLength()); - modifiers.add(mod); - } - - - // Recovery device deployment altitude and delay - if (c instanceof RecoveryDevice) { - RecoveryDevice device = (RecoveryDevice) c; - - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier.recoverydevice.deployDelay"), - trans.get("optimization.modifier.recoverydevice.deployDelay.desc"), - c, UnitGroup.UNITS_SHORT_TIME, - 1.0, c.getClass(), c.getID(), "DeployDelay"); - mod.setMinValue(0); - mod.setMaxValue(10); - modifiers.add(mod); - - if (device.getDeployEvent() == DeployEvent.ALTITUDE) { - mod = new GenericComponentModifier( - trans.get("optimization.modifier.recoverydevice.deployAltitude"), - trans.get("optimization.modifier.recoverydevice.deployAltitude.desc"), - c, UnitGroup.UNITS_DISTANCE, - 1.0, c.getClass(), c.getID(), "DeployAltitude"); - setDefaultMinMax(mod, simulation); - modifiers.add(mod); - } - } - - - // Conditional shape parameter of Transition - if (c instanceof Transition) { - Transition transition = (Transition) c; - Transition.Shape shape = transition.getType(); - if (shape.usesParameter()) { - SimulationModifier mod = new GenericComponentModifier( - trans.get("optimization.modifier." + c.getClass().getSimpleName().toLowerCase() + ".shapeparameter"), - trans.get("optimization.modifier." + c.getClass().getSimpleName().toLowerCase() + ".shapeparameter.desc"), - c, UnitGroup.UNITS_NONE, - 1.0, c.getClass(), c.getID(), "ShapeParameter"); - mod.setMinValue(shape.minParameter()); - mod.setMaxValue(shape.maxParameter()); - modifiers.add(mod); - } - } - } - - return modifiers; - } - - - private void setDefaultMinMax(SimulationModifier mod, Simulation simulation) { - try { - double current = mod.getCurrentSIValue(simulation); - mod.setMinValue(current / DEFAULT_RANGE_MULTIPLIER); - mod.setMaxValue(current * DEFAULT_RANGE_MULTIPLIER); - } catch (OptimizationException e) { - throw new BugException("Simulation modifier threw exception", e); - } - } - - - /* - * String modifierName, Object relatedObject, UnitGroup unitGroup, - double multiplier, Class<? extends RocketComponent> componentClass, String componentId, String methodName - */ - - private static class ModifierDefinition { - private final String modifierNameKey; - private final String modifierDescriptionKey; - private final UnitGroup unitGroup; - private final double multiplier; - private final Class<? extends RocketComponent> componentClass; - private final String methodName; - private final String autoMethod; - - - public ModifierDefinition(String modifierNameKey, String modifierDescriptionKey, UnitGroup unitGroup, - double multiplier, Class<? extends RocketComponent> componentClass, String methodName, String autoMethod) { - this.modifierNameKey = modifierNameKey; - this.modifierDescriptionKey = modifierDescriptionKey; - this.unitGroup = unitGroup; - this.multiplier = multiplier; - this.componentClass = componentClass; - this.methodName = methodName; - this.autoMethod = autoMethod; - } - - } -} diff --git a/src/net/sf/openrocket/optimization/services/OptimizableParameterService.java b/src/net/sf/openrocket/optimization/services/OptimizableParameterService.java deleted file mode 100644 index d7842e4d..00000000 --- a/src/net/sf/openrocket/optimization/services/OptimizableParameterService.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.optimization.services; - -import java.util.Collection; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; - -/** - * A service for generating rocket optimization parameters. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface OptimizableParameterService { - - /** - * Return all available rocket optimization parameters for this document. - * These should be new instances unless the parameter implementation is stateless. - * - * @param document the design document - * @return a collection of the rocket optimization parameters. - */ - public Collection<OptimizableParameter> getParameters(OpenRocketDocument document); - - -} diff --git a/src/net/sf/openrocket/optimization/services/OptimizationServiceHelper.java b/src/net/sf/openrocket/optimization/services/OptimizationServiceHelper.java deleted file mode 100644 index 152ccaf7..00000000 --- a/src/net/sf/openrocket/optimization/services/OptimizationServiceHelper.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.sf.openrocket.optimization.services; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.ServiceLoader; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.util.TestRockets; - -public final class OptimizationServiceHelper { - - private OptimizationServiceHelper() { - // Prevent instantiation - } - - /** - * Return the simulation modifiers for an OpenRocketDocument. This queries the - * getModifiers() method from all available services and returns a collection of all - * the modifiers. - * - * @param document the OpenRocketDocument. - * @return a collection of all simulation modifiers applicable to the document. - */ - public static Collection<SimulationModifier> getSimulationModifiers(OpenRocketDocument document) { - List<SimulationModifier> list = new ArrayList<SimulationModifier>(); - - ServiceLoader<SimulationModifierService> loader = ServiceLoader.load(SimulationModifierService.class); - for (SimulationModifierService service : loader) { - list.addAll(service.getModifiers(document)); - } - - return list; - } - - - - /** - * Return the optimization parameters for an OpenRocketDocument. This queries the - * getParameters() method from all available services and returns a collection of all - * the modifiers. - * - * @param document the OpenRocketDocument. - * @return a collection of all optimization parameters applicable to the document. - */ - public static Collection<OptimizableParameter> getOptimizableParameters(OpenRocketDocument document) { - List<OptimizableParameter> list = new ArrayList<OptimizableParameter>(); - - ServiceLoader<OptimizableParameterService> loader = ServiceLoader.load(OptimizableParameterService.class); - for (OptimizableParameterService service : loader) { - list.addAll(service.getParameters(document)); - } - - return list; - } - - - public static void main(String[] args) { - Rocket r = TestRockets.makeBigBlue(); - OpenRocketDocument document = new OpenRocketDocument(r); - System.out.println("Simulation modifiers: " + getSimulationModifiers(document)); - System.out.println("Optimization parameters: " + getOptimizableParameters(document)); - } -} diff --git a/src/net/sf/openrocket/optimization/services/SimulationModifierService.java b/src/net/sf/openrocket/optimization/services/SimulationModifierService.java deleted file mode 100644 index 3f386379..00000000 --- a/src/net/sf/openrocket/optimization/services/SimulationModifierService.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.sf.openrocket.optimization.services; - -import java.util.Collection; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; - -/** - * A service for generating simulation modifiers. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface SimulationModifierService { - - /** - * Return all available simulation modifiers for this document. - * - * @param document the design document - * @return a collection of the rocket optimization parameters. - */ - public Collection<SimulationModifier> getModifiers(OpenRocketDocument document); - - -} diff --git a/src/net/sf/openrocket/preset/ComponentPreset.java b/src/net/sf/openrocket/preset/ComponentPreset.java deleted file mode 100644 index 87343e93..00000000 --- a/src/net/sf/openrocket/preset/ComponentPreset.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.sf.openrocket.preset; - -import net.sf.openrocket.motor.Manufacturer; -import net.sf.openrocket.rocketcomponent.RocketComponent; - -/** - * A model for a preset component. - * <p> - * A preset component contains a component class type, manufacturer information, - * part information, and a method that returns a prototype of the preset component. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class ComponentPreset { - - private final Manufacturer manufacturer; - private final String partNo; - private final String partDescription; - private final RocketComponent prototype; - - - public ComponentPreset(Manufacturer manufacturer, String partNo, String partDescription, - RocketComponent prototype) { - this.manufacturer = manufacturer; - this.partNo = partNo; - this.partDescription = partDescription; - this.prototype = prototype.copy(); - - if (prototype.getParent() != null) { - throw new IllegalArgumentException("Prototype component cannot have a parent"); - } - if (prototype.getChildCount() > 0) { - throw new IllegalArgumentException("Prototype component cannot have children"); - } - } - - - /** - * Return the component class that this preset defines. - */ - public Class<? extends RocketComponent> getComponentClass() { - return prototype.getClass(); - } - - /** - * Return the manufacturer of this preset component. - */ - public Manufacturer getManufacturer() { - return manufacturer; - } - - /** - * Return the part number. This is the part identifier (e.g. "BT-50"). - */ - public String getPartNo() { - return partNo; - } - - /** - * Return the part description. This is a longer description of the component. - */ - public String getPartDescription() { - return partDescription; - } - - /** - * Return a prototype component. This component may be modified freely. - */ - public RocketComponent getPrototype() { - return prototype.copy(); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/BodyComponent.java b/src/net/sf/openrocket/rocketcomponent/BodyComponent.java deleted file mode 100644 index a102e87f..00000000 --- a/src/net/sf/openrocket/rocketcomponent/BodyComponent.java +++ /dev/null @@ -1,79 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - - - -/** - * Class to represent a body object. The object can be described as a function of - * the cylindrical coordinates x and angle theta as r = f(x,theta). The component - * need not be symmetrical in any way (e.g. square tube, slanted cone etc). - * - * It defines the methods getRadius(x,theta) and getInnerRadius(x,theta), as well - * as get/setLength(). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public abstract class BodyComponent extends ExternalComponent { - - /** - * Default constructor. Sets the relative position to POSITION_RELATIVE_AFTER, - * i.e. body components come after one another. - */ - public BodyComponent() { - super(RocketComponent.Position.AFTER); - } - - - /** - * Get the outer radius of the component at cylindrical coordinate (x,theta). - * - * Note that the return value may be negative for a slanted object. - * - * @param x Distance in x direction - * @param theta Angle about the x-axis - * @return Distance to the outer edge of the object - */ - public abstract double getRadius(double x, double theta); - - - /** - * Get the inner radius of the component at cylindrical coordinate (x,theta). - * - * Note that the return value may be negative for a slanted object. - * - * @param x Distance in x direction - * @param theta Angle about the x-axis - * @return Distance to the inner edge of the object - */ - public abstract double getInnerRadius(double x, double theta); - - - @Override - protected void loadFromPreset(RocketComponent preset) { - BodyComponent c = (BodyComponent) preset; - this.setLength(c.getLength()); - - super.loadFromPreset(preset); - } - - - - /** - * Sets the length of the body component. - * <p> - * Note: This should be overridden by the subcomponents which need to call - * clearPreset(). (BodyTube allows changing length without resetting the preset.) - */ - public void setLength(double length) { - if (this.length == length) - return; - this.length = Math.max(length, 0); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - @Override - public boolean allowsChildren() { - return true; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/BodyTube.java b/src/net/sf/openrocket/rocketcomponent/BodyTube.java deleted file mode 100644 index 64c1b4a9..00000000 --- a/src/net/sf/openrocket/rocketcomponent/BodyTube.java +++ /dev/null @@ -1,463 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -/** - * Rocket body tube component. Has only two parameters, a radius and length. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class BodyTube extends SymmetricComponent implements MotorMount, Coaxial { - private static final Translator trans = Application.getTranslator(); - - private double outerRadius = 0; - private boolean autoRadius = false; // Radius chosen automatically based on parent component - - // When changing the inner radius, thickness is modified - - private boolean motorMount = false; - private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>(); - private HashMap<String, Motor> motors = new HashMap<String, Motor>(); - private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC; - private double ignitionDelay = 0; - private double overhang = 0; - - - - public BodyTube() { - super(); - this.length = 8 * DEFAULT_RADIUS; - this.outerRadius = DEFAULT_RADIUS; - this.autoRadius = true; - } - - public BodyTube(double length, double radius) { - super(); - this.outerRadius = Math.max(radius, 0); - this.length = Math.max(length, 0); - } - - - public BodyTube(double length, double radius, boolean filled) { - this(length, radius); - this.filled = filled; - } - - public BodyTube(double length, double radius, double thickness) { - this(length, radius); - this.filled = false; - this.thickness = thickness; - } - - - /************ Get/set component parameter methods ************/ - - /** - * Return the outer radius of the body tube. - * - * @return the outside radius of the tube - */ - @Override - public double getOuterRadius() { - if (autoRadius) { - // Return auto radius from front or rear - double r = -1; - SymmetricComponent c = this.getPreviousSymmetricComponent(); - if (c != null) { - r = c.getFrontAutoRadius(); - } - if (r < 0) { - c = this.getNextSymmetricComponent(); - if (c != null) { - r = c.getRearAutoRadius(); - } - } - if (r < 0) - r = DEFAULT_RADIUS; - return r; - } - return outerRadius; - } - - - /** - * Set the outer radius of the body tube. If the radius is less than the wall thickness, - * the wall thickness is decreased accordingly of the value of the radius. - * This method sets the automatic radius off. - * - * @param radius the outside radius in standard units - */ - @Override - public void setOuterRadius(double radius) { - if ((this.outerRadius == radius) && (autoRadius == false)) - return; - - this.autoRadius = false; - this.outerRadius = Math.max(radius, 0); - - if (this.thickness > this.outerRadius) - this.thickness = this.outerRadius; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - clearPreset(); - } - - - /** - * Returns whether the radius is selected automatically or not. - * Returns false also in case automatic radius selection is not possible. - */ - public boolean isOuterRadiusAutomatic() { - return autoRadius; - } - - /** - * Sets whether the radius is selected automatically or not. - */ - public void setOuterRadiusAutomatic(boolean auto) { - if (autoRadius == auto) - return; - - autoRadius = auto; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - clearPreset(); - } - - - @Override - protected void loadFromPreset(RocketComponent preset) { - BodyTube c = (BodyTube) preset; - this.setOuterRadius(c.getOuterRadius()); - - super.loadFromPreset(preset); - } - - - @Override - public double getAftRadius() { - return getOuterRadius(); - } - - @Override - public double getForeRadius() { - return getOuterRadius(); - } - - @Override - public boolean isAftRadiusAutomatic() { - return isOuterRadiusAutomatic(); - } - - @Override - public boolean isForeRadiusAutomatic() { - return isOuterRadiusAutomatic(); - } - - - - @Override - protected double getFrontAutoRadius() { - if (isOuterRadiusAutomatic()) { - // Search for previous SymmetricComponent - SymmetricComponent c = this.getPreviousSymmetricComponent(); - if (c != null) { - return c.getFrontAutoRadius(); - } else { - return -1; - } - } - return getOuterRadius(); - } - - @Override - protected double getRearAutoRadius() { - if (isOuterRadiusAutomatic()) { - // Search for next SymmetricComponent - SymmetricComponent c = this.getNextSymmetricComponent(); - if (c != null) { - return c.getRearAutoRadius(); - } else { - return -1; - } - } - return getOuterRadius(); - } - - - - - - @Override - public double getInnerRadius() { - if (filled) - return 0; - return Math.max(getOuterRadius() - thickness, 0); - } - - @Override - public void setInnerRadius(double r) { - setThickness(getOuterRadius() - r); - } - - - - - /** - * Return the component name. - */ - @Override - public String getComponentName() { - //// Body tube - return trans.get("BodyTube.BodyTube"); - } - - - /************ Component calculations ***********/ - - // From SymmetricComponent - /** - * Returns the outer radius at the position x. This returns the same value as getOuterRadius(). - */ - @Override - public double getRadius(double x) { - return getOuterRadius(); - } - - /** - * Returns the inner radius at the position x. If the tube is filled, returns always zero. - */ - @Override - public double getInnerRadius(double x) { - if (filled) - return 0.0; - else - return Math.max(getOuterRadius() - thickness, 0); - } - - - /** - * Returns the body tube's center of gravity. - */ - @Override - public Coordinate getComponentCG() { - return new Coordinate(length / 2, 0, 0, getComponentMass()); - } - - /** - * Returns the body tube's volume. - */ - @Override - public double getComponentVolume() { - double r = getOuterRadius(); - if (filled) - return getFilledVolume(r, length); - else - return getFilledVolume(r, length) - getFilledVolume(getInnerRadius(0), length); - } - - - @Override - public double getLongitudinalUnitInertia() { - // 1/12 * (3 * (r1^2 + r2^2) + h^2) - return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getLength())) / 12; - } - - @Override - public double getRotationalUnitInertia() { - // 1/2 * (r1^2 + r2^2) - return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius())) / 2; - } - - - - - /** - * Helper function for cylinder volume. - */ - private static double getFilledVolume(double r, double l) { - return Math.PI * r * r * l; - } - - - /** - * Adds bounding coordinates to the given set. The body tube will fit within the - * convex hull of the points. - * - * Currently the points are simply a rectangular box around the body tube. - */ - @Override - public Collection<Coordinate> getComponentBounds() { - Collection<Coordinate> bounds = new ArrayList<Coordinate>(8); - double r = getOuterRadius(); - addBound(bounds, 0, r); - addBound(bounds, length, r); - return bounds; - } - - - - /** - * Check whether the given type can be added to this component. BodyTubes allow any - * InternalComponents or ExternalComponents, excluding BodyComponents, to be added. - * - * @param type The RocketComponent class type to add. - * @return Whether such a component can be added. - */ - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - if (InternalComponent.class.isAssignableFrom(type)) - return true; - if (ExternalComponent.class.isAssignableFrom(type) && - !BodyComponent.class.isAssignableFrom(type)) - return true; - return false; - } - - //////////////// Motor mount ///////////////// - - @Override - public boolean isMotorMount() { - return motorMount; - } - - @Override - public void setMotorMount(boolean mount) { - if (motorMount == mount) - return; - motorMount = mount; - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - @Override - public Motor getMotor(String id) { - if (id == null) - return null; - - // Check whether the id is valid for the current rocket - RocketComponent root = this.getRoot(); - if (!(root instanceof Rocket)) - return null; - if (!((Rocket) root).isMotorConfigurationID(id)) - return null; - - return motors.get(id); - } - - @Override - public void setMotor(String id, Motor motor) { - if (id == null) { - if (motor != null) { - throw new IllegalArgumentException("Cannot set non-null motor for id null"); - } - } - Motor current = motors.get(id); - if ((motor == null && current == null) || - (motor != null && motor.equals(current))) - return; - motors.put(id, motor); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - @Override - public double getMotorDelay(String id) { - Double delay = ejectionDelays.get(id); - if (delay == null) - return Motor.PLUGGED; - return delay; - } - - @Override - public void setMotorDelay(String id, double delay) { - ejectionDelays.put(id, delay); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - @Override - public int getMotorCount() { - return 1; - } - - @Override - public double getMotorMountDiameter() { - return getInnerRadius() * 2; - } - - @Override - public IgnitionEvent getIgnitionEvent() { - return ignitionEvent; - } - - @Override - public void setIgnitionEvent(IgnitionEvent event) { - if (ignitionEvent == event) - return; - ignitionEvent = event; - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - } - - - @Override - public double getIgnitionDelay() { - return ignitionDelay; - } - - @Override - public void setIgnitionDelay(double delay) { - if (MathUtil.equals(delay, ignitionDelay)) - return; - ignitionDelay = delay; - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - } - - - @Override - public double getMotorOverhang() { - return overhang; - } - - @Override - public void setMotorOverhang(double overhang) { - if (MathUtil.equals(this.overhang, overhang)) - return; - this.overhang = overhang; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - @Override - public Coordinate getMotorPosition(String id) { - Motor motor = motors.get(id); - if (motor == null) { - throw new IllegalArgumentException("No motor with id " + id + " defined."); - } - - return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang()); - } - - - - - /* - * (non-Javadoc) - * Copy the motor and ejection delay HashMaps. - * - * @see rocketcomponent.RocketComponent#copy() - */ - @SuppressWarnings("unchecked") - @Override - protected RocketComponent copyWithOriginalID() { - RocketComponent c = super.copyWithOriginalID(); - ((BodyTube) c).motors = (HashMap<String, Motor>) motors.clone(); - ((BodyTube) c).ejectionDelays = (HashMap<String, Double>) ejectionDelays.clone(); - return c; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/Bulkhead.java b/src/net/sf/openrocket/rocketcomponent/Bulkhead.java deleted file mode 100644 index 24a1552f..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Bulkhead.java +++ /dev/null @@ -1,46 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - - -public class Bulkhead extends RadiusRingComponent { - private static final Translator trans = Application.getTranslator(); - - public Bulkhead() { - setOuterRadiusAutomatic(true); - setLength(0.002); - } - - @Override - public double getInnerRadius() { - return 0; - } - - @Override - public void setInnerRadius(double r) { - // No-op - } - - @Override - public void setOuterRadiusAutomatic(boolean auto) { - super.setOuterRadiusAutomatic(auto); - } - - @Override - public String getComponentName() { - //// Bulkhead - return trans.get("Bulkhead.Bulkhead"); - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/CenteringRing.java b/src/net/sf/openrocket/rocketcomponent/CenteringRing.java deleted file mode 100644 index 21b73e2a..00000000 --- a/src/net/sf/openrocket/rocketcomponent/CenteringRing.java +++ /dev/null @@ -1,70 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.util.Coordinate; - - -public class CenteringRing extends RadiusRingComponent { - - public CenteringRing() { - setOuterRadiusAutomatic(true); - setInnerRadiusAutomatic(true); - setLength(0.002); - } - - - @Override - public double getInnerRadius() { - // Implement sibling inner radius automation - if (isInnerRadiusAutomatic()) { - innerRadius = 0; - // Component can be parentless if disattached from rocket - if (this.getParent() != null) { - for (RocketComponent sibling : this.getParent().getChildren()) { - /* - * Only InnerTubes are considered when determining the automatic - * inner radius (for now). - */ - if (!(sibling instanceof InnerTube)) // Excludes itself - continue; - - double pos1 = this.toRelative(Coordinate.NUL, sibling)[0].x; - double pos2 = this.toRelative(new Coordinate(getLength()), sibling)[0].x; - if (pos2 < 0 || pos1 > sibling.getLength()) - continue; - - innerRadius = Math.max(innerRadius, ((InnerTube) sibling).getOuterRadius()); - } - innerRadius = Math.min(innerRadius, getOuterRadius()); - } - } - - return super.getInnerRadius(); - } - - - @Override - public void setOuterRadiusAutomatic(boolean auto) { - super.setOuterRadiusAutomatic(auto); - } - - @Override - public void setInnerRadiusAutomatic(boolean auto) { - super.setInnerRadiusAutomatic(auto); - } - - @Override - public String getComponentName() { - return "Centering ring"; - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java b/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java deleted file mode 100644 index 73e8a6de..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ClusterConfiguration.java +++ /dev/null @@ -1,118 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - - -/** - * Class that defines different cluster configurations available for the InnerTube. - * The class is immutable, and all the constructors are private. Therefore the only - * available cluster configurations are those available in the static fields. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ClusterConfiguration { - // Helper vars - private static final double R5 = 1.0/(2*Math.sin(2*Math.PI/10)); - private static final double SQRT2 = Math.sqrt(2); - private static final double SQRT3 = Math.sqrt(3); - - /** A single motor */ - public static final ClusterConfiguration SINGLE = new ClusterConfiguration("single", 0,0); - - /** Definitions of cluster configurations. Do not modify array. */ - public static final ClusterConfiguration[] CONFIGURATIONS = { - // Single row - SINGLE, - new ClusterConfiguration("double", -0.5,0, 0.5,0), - new ClusterConfiguration("3-row", -1.0,0, 0.0,0, 1.0,0), - new ClusterConfiguration("4-row", -1.5,0, -0.5,0, 0.5,0, 1.5,0), - - // Ring of tubes - new ClusterConfiguration("3-ring", -0.5,-1.0/(2*SQRT3), - 0.5,-1.0/(2*SQRT3), - 0, 1.0/SQRT3), - new ClusterConfiguration("4-ring", -0.5,0.5, 0.5,0.5, 0.5,-0.5, -0.5,-0.5), - new ClusterConfiguration("5-ring", 0,R5, - R5*Math.sin(2*Math.PI/5),R5*Math.cos(2*Math.PI/5), - R5*Math.sin(2*Math.PI*2/5),R5*Math.cos(2*Math.PI*2/5), - R5*Math.sin(2*Math.PI*3/5),R5*Math.cos(2*Math.PI*3/5), - R5*Math.sin(2*Math.PI*4/5),R5*Math.cos(2*Math.PI*4/5)), - new ClusterConfiguration("6-ring", 0,1, SQRT3/2,0.5, SQRT3/2,-0.5, - 0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5), - - // Centered with ring - new ClusterConfiguration("3-star", 0,0, 0,1, SQRT3/2,-0.5, -SQRT3/2,-0.5), - new ClusterConfiguration("4-star", 0,0, -1/SQRT2,1/SQRT2, 1/SQRT2,1/SQRT2, - 1/SQRT2,-1/SQRT2, -1/SQRT2,-1/SQRT2), - new ClusterConfiguration("5-star", 0,0, 0,1, - Math.sin(2*Math.PI/5),Math.cos(2*Math.PI/5), - Math.sin(2*Math.PI*2/5),Math.cos(2*Math.PI*2/5), - Math.sin(2*Math.PI*3/5),Math.cos(2*Math.PI*3/5), - Math.sin(2*Math.PI*4/5),Math.cos(2*Math.PI*4/5)), - new ClusterConfiguration("6-star", 0,0, 0,1, SQRT3/2,0.5, SQRT3/2,-0.5, - 0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5) - }; - - - - private final List<Double> points; - private final String xmlName; - - - private ClusterConfiguration(String xmlName, double... points) { - this.xmlName = xmlName; - if (points.length == 0 || points.length%2 == 1) { - throw new IllegalArgumentException("Illegal number of points specified: "+ - points.length); - } - List<Double> l = new ArrayList<Double>(points.length); - for (double d: points) - l.add(d); - - this.points = Collections.unmodifiableList(l); - } - - public String getXMLName() { - return xmlName; - } - - public int getClusterCount() { - return points.size()/2; - } - - /** - * Returns the relative positions of the cluster components. The list is of length - * <code>2*getClusterCount()</code> with (x,y) value pairs. The origin is at (0,0) - * and the values are positioned so that the closest clusters have distance of 1. - * - * @return a list of (x,y) coordinate pairs. - */ - public List<Double> getPoints() { - return points; // Unmodifiable - } - - /** - * Return the points rotated by <code>rotation</code> radians. - * @param rotation Rotation amount. - */ - public List<Double> getPoints(double rotation) { - double cos = Math.cos(rotation); - double sin = Math.sin(rotation); - List<Double> ret = new ArrayList<Double>(points.size()); - for (int i=0; i<points.size()/2; i++) { - double x = points.get(2*i); - double y = points.get(2*i+1); - ret.add( x*cos + y*sin); - ret.add(-x*sin + y*cos); - } - return ret; - } - - - @Override - public String toString() { - return xmlName; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/Clusterable.java b/src/net/sf/openrocket/rocketcomponent/Clusterable.java deleted file mode 100644 index fba858b1..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Clusterable.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.util.ChangeSource; - -public interface Clusterable extends ChangeSource { - - public ClusterConfiguration getClusterConfiguration(); - public void setClusterConfiguration(ClusterConfiguration cluster); - public double getClusterSeparation(); - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Coaxial.java b/src/net/sf/openrocket/rocketcomponent/Coaxial.java deleted file mode 100644 index ad2338fc..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Coaxial.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Coaxial.java - */ -package net.sf.openrocket.rocketcomponent; - -/** - * This interface defines the API for components that are axially - * symmetric. It differs from RadialParent in that RadialParent applies - * to axially symmetric components whose radius varies with position, while - * this interface is for components that have a constant radius over it's length. - */ -public interface Coaxial { - - /** - * Get the length of the radius of the inside dimension, in standard units. - * - * @return the inner radius - */ - double getInnerRadius(); - - /** - * Set the length of the radius of the inside dimension, in standard units. - * - * @param v the length of the inner radius - */ - void setInnerRadius(double v); - - /** - * Get the length of the radius of the outside dimension, in standard units. - * - * @return the outer radius - */ - double getOuterRadius(); - - /** - * Set the length of the radius of the outside dimension, in standard units. - * - * @param v the length of the outer radius - */ - void setOuterRadius(double v); - - /** - * Get the wall thickness of the component. Typically this is just - * the outer radius - inner radius. - * - * @return the thickness of the wall - */ - double getThickness(); - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentAssembly.java b/src/net/sf/openrocket/rocketcomponent/ComponentAssembly.java deleted file mode 100644 index de5b78d2..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ComponentAssembly.java +++ /dev/null @@ -1,100 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.Collection; -import java.util.Collections; - -import net.sf.openrocket.util.Coordinate; - - - -/** - * A base of component assemblies. - * <p> - * Note that the mass and CG overrides of the <code>ComponentAssembly</code> class - * overrides all sibling mass/CG as well as its own. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class ComponentAssembly extends RocketComponent { - - /** - * Sets the position of the components to POSITION_RELATIVE_AFTER. - * (Should have no effect.) - */ - public ComponentAssembly() { - super(RocketComponent.Position.AFTER); - } - - /** - * Null method (ComponentAssembly has no bounds of itself). - */ - @Override - public Collection<Coordinate> getComponentBounds() { - return Collections.emptyList(); - } - - /** - * Null method (ComponentAssembly has no mass of itself). - */ - @Override - public Coordinate getComponentCG() { - return Coordinate.NUL; - } - - /** - * Null method (ComponentAssembly has no mass of itself). - */ - @Override - public double getComponentMass() { - return 0; - } - - /** - * Null method (ComponentAssembly has no mass of itself). - */ - @Override - public double getLongitudinalUnitInertia() { - return 0; - } - - /** - * Null method (ComponentAssembly has no mass of itself). - */ - @Override - public double getRotationalUnitInertia() { - return 0; - } - - /** - * Components have no aerodynamic effect, so return false. - */ - @Override - public boolean isAerodynamic() { - return false; - } - - /** - * Component have no effect on mass, so return false (even though the override values - * may have an effect). - */ - @Override - public boolean isMassive() { - return false; - } - - @Override - public boolean getOverrideSubcomponents() { - return true; - } - - @Override - public void setOverrideSubcomponents(boolean override) { - // No-op - } - - @Override - public boolean isOverrideSubcomponentsEnabled() { - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java b/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java deleted file mode 100644 index 5a2a2e24..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ComponentChangeEvent.java +++ /dev/null @@ -1,103 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.EventObject; - -public class ComponentChangeEvent extends EventObject { - private static final long serialVersionUID = 1L; - - - /** A change that does not affect simulation results in any way (name, color, etc.) */ - public static final int NONFUNCTIONAL_CHANGE = 1; - /** A change that affects the mass properties of the rocket */ - public static final int MASS_CHANGE = 2; - /** A change that affects the aerodynamic properties of the rocket */ - public static final int AERODYNAMIC_CHANGE = 4; - /** A change that affects the mass and aerodynamic properties of the rocket */ - public static final int BOTH_CHANGE = MASS_CHANGE | AERODYNAMIC_CHANGE; // Mass & Aerodynamic - - /** A change that affects the rocket tree structure */ - public static final int TREE_CHANGE = 8; - /** A change caused by undo/redo. */ - public static final int UNDO_CHANGE = 16; - /** A change in the motor configurations or names */ - public static final int MOTOR_CHANGE = 32; - /** A change that affects the events occurring during flight. */ - public static final int EVENT_CHANGE = 64; - - /** A bit-field that contains all possible change types. */ - public static final int ALL_CHANGE = 0xFFFFFFFF; - - private final int type; - - - public ComponentChangeEvent(RocketComponent component, int type) { - super(component); - if (type == 0) { - throw new IllegalArgumentException("no event type provided"); - } - this.type = type; - } - - - /** - * Return the source component of this event as specified in the constructor. - */ - @Override - public RocketComponent getSource() { - return (RocketComponent) super.getSource(); - } - - - public boolean isAerodynamicChange() { - return (type & AERODYNAMIC_CHANGE) != 0; - } - - public boolean isMassChange() { - return (type & MASS_CHANGE) != 0; - } - - public boolean isOtherChange() { - return (type & BOTH_CHANGE) == 0; - } - - public boolean isTreeChange() { - return (type & TREE_CHANGE) != 0; - } - - public boolean isUndoChange() { - return (type & UNDO_CHANGE) != 0; - } - - public boolean isMotorChange() { - return (type & MOTOR_CHANGE) != 0; - } - - public int getType() { - return type; - } - - @Override - public String toString() { - String s = ""; - - if ((type & NONFUNCTIONAL_CHANGE) != 0) - s += ",nonfunc"; - if (isMassChange()) - s += ",mass"; - if (isAerodynamicChange()) - s += ",aero"; - if (isTreeChange()) - s += ",tree"; - if (isUndoChange()) - s += ",undo"; - if (isMotorChange()) - s += ",motor"; - if ((type & EVENT_CHANGE) != 0) - s += ",event"; - - if (s.length() > 0) - s = s.substring(1); - - return "ComponentChangeEvent[" + s + "]"; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java b/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java deleted file mode 100644 index dba150ae..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ComponentChangeListener.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.EventListener; - -public interface ComponentChangeListener extends EventListener { - - public void componentChanged(ComponentChangeEvent e); - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Configuration.java b/src/net/sf/openrocket/rocketcomponent/Configuration.java deleted file mode 100644 index 4a76df71..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Configuration.java +++ /dev/null @@ -1,470 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.BitSet; -import java.util.Collection; -import java.util.Collections; -import java.util.EventListener; -import java.util.EventObject; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.StateChangeListener; - - -/** - * A class defining a rocket configuration, including motors and which stages are active. - * - * TODO: HIGH: Remove motor ignition times from this class. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Configuration implements Cloneable, ChangeSource, ComponentChangeListener, - Iterable<RocketComponent>, Monitorable { - - private Rocket rocket; - private BitSet stages = new BitSet(); - - private String motorConfiguration = null; - - private List<EventListener> listenerList = new ArrayList<EventListener>(); - - - /* Cached data */ - private int boundsModID = -1; - private ArrayList<Coordinate> cachedBounds = new ArrayList<Coordinate>(); - private double cachedLength = -1; - - private int refLengthModID = -1; - private double cachedRefLength = -1; - - - private int modID = 0; - - - /** - * Create a new configuration with the specified <code>Rocket</code> with - * <code>null</code> motor configuration. - * - * @param rocket the rocket - */ - public Configuration(Rocket rocket) { - this.rocket = rocket; - setAllStages(); - rocket.addComponentChangeListener(this); - } - - - - public Rocket getRocket() { - return rocket; - } - - - public void setAllStages() { - stages.clear(); - stages.set(0, rocket.getStageCount()); - fireChangeEvent(); - } - - - /** - * Set all stages up to and including the given stage number. For example, - * <code>setToStage(0)</code> will set only the first stage active. - * - * @param stage the stage number. - */ - public void setToStage(int stage) { - stages.clear(); - stages.set(0, stage + 1, true); - // stages.set(stage+1, rocket.getStageCount(), false); - fireChangeEvent(); - } - - - /** - * Check whether the up-most stage of the rocket is in this configuration. - * - * @return <code>true</code> if the first stage is active in this configuration. - */ - public boolean isHead() { - return isStageActive(0); - } - - - - /** - * Check whether the stage specified by the index is active. - */ - public boolean isStageActive(int stage) { - if (stage >= rocket.getStageCount()) - return false; - return stages.get(stage); - } - - public int getStageCount() { - return rocket.getStageCount(); - } - - public int getActiveStageCount() { - int count = 0; - int s = rocket.getStageCount(); - - for (int i = 0; i < s; i++) { - if (stages.get(i)) - count++; - } - return count; - } - - public int[] getActiveStages() { - int stageCount = rocket.getStageCount(); - List<Integer> active = new ArrayList<Integer>(); - int[] ret; - - for (int i = 0; i < stageCount; i++) { - if (stages.get(i)) { - active.add(i); - } - } - - ret = new int[active.size()]; - for (int i = 0; i < ret.length; i++) { - ret[i] = active.get(i); - } - - return ret; - } - - - /** - * Return the reference length associated with the current configuration. The - * reference length type is retrieved from the <code>Rocket</code>. - * - * @return the reference length for this configuration. - */ - public double getReferenceLength() { - if (rocket.getModID() != refLengthModID) { - refLengthModID = rocket.getModID(); - cachedRefLength = rocket.getReferenceType().getReferenceLength(this); - } - return cachedRefLength; - } - - - public double getReferenceArea() { - return Math.PI * MathUtil.pow2(getReferenceLength() / 2); - } - - - public String getMotorConfigurationID() { - return motorConfiguration; - } - - public void setMotorConfigurationID(String id) { - if ((motorConfiguration == null && id == null) || - (id != null && id.equals(motorConfiguration))) - return; - - motorConfiguration = id; - fireChangeEvent(); - } - - public String getMotorConfigurationDescription() { - return rocket.getMotorConfigurationNameOrDescription(motorConfiguration); - } - - - - - - /** - * Removes the listener connection to the rocket and listeners of this object. - * This configuration may not be used after a call to this method! - */ - public void release() { - rocket.removeComponentChangeListener(this); - listenerList = null; - rocket = null; - } - - - //////////////// Listeners //////////////// - - @Override - public void addChangeListener(EventListener listener) { - listenerList.add(listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - listenerList.remove(listener); - } - - protected void fireChangeEvent() { - EventObject e = new EventObject(this); - - this.modID++; - boundsModID = -1; - refLengthModID = -1; - - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] listeners = listenerList.toArray(new EventListener[0]); - for (EventListener l : listeners) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(e); - } - } - } - - - @Override - public void componentChanged(ComponentChangeEvent e) { - fireChangeEvent(); - } - - - /////////////// Helper methods /////////////// - - /** - * Return whether this configuration has any motors defined to it. - * - * @return true if this configuration has active motor mounts with motors defined to them. - */ - public boolean hasMotors() { - for (RocketComponent c : this) { - if (c instanceof MotorMount) { - MotorMount mount = (MotorMount) c; - if (!mount.isMotorMount()) - continue; - if (mount.getMotor(this.motorConfiguration) != null) { - return true; - } - } - } - return false; - } - - - /** - * Return whether a component is in the currently active stages. - */ - public boolean isComponentActive(final RocketComponent c) { - int stage = c.getStageNumber(); - return isStageActive(stage); - } - - - /** - * Return the bounds of the current configuration. The bounds are cached. - * - * @return a <code>Collection</code> containing coordinates bouding the rocket. - */ - public Collection<Coordinate> getBounds() { - if (rocket.getModID() != boundsModID) { - boundsModID = rocket.getModID(); - cachedBounds.clear(); - - double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; - for (RocketComponent component : this) { - for (Coordinate c : component.getComponentBounds()) { - for (Coordinate coord : component.toAbsolute(c)) { - cachedBounds.add(coord); - if (coord.x < minX) - minX = coord.x; - if (coord.x > maxX) - maxX = coord.x; - } - } - } - - if (Double.isInfinite(minX) || Double.isInfinite(maxX)) { - cachedLength = 0; - } else { - cachedLength = maxX - minX; - } - } - return cachedBounds.clone(); - } - - - /** - * Returns the length of the rocket configuration, from the foremost bound X-coordinate - * to the aft-most X-coordinate. The value is cached. - * - * @return the length of the rocket in the X-direction. - */ - public double getLength() { - if (rocket.getModID() != boundsModID) - getBounds(); // Calculates the length - - return cachedLength; - } - - - - - /** - * Return an iterator that iterates over the currently active components. - * The <code>Rocket</code> and <code>Stage</code> components are not returned, - * but instead all components that are within currently active stages. - */ - @Override - public Iterator<RocketComponent> iterator() { - return new ConfigurationIterator(); - } - - - /** - * Return an iterator that iterates over all <code>MotorMount</code>s within the - * current configuration that have an active motor. - * - * @return an iterator over active motor mounts. - */ - public Iterator<MotorMount> motorIterator() { - return new MotorIterator(); - } - - - /** - * Perform a deep-clone. The object references are also cloned and no - * listeners are listening on the cloned object. The rocket instance remains the same. - */ - @Override - public Configuration clone() { - try { - Configuration config = (Configuration) super.clone(); - config.listenerList = new ArrayList<EventListener>(); - config.stages = (BitSet) this.stages.clone(); - config.cachedBounds = new ArrayList<Coordinate>(); - config.boundsModID = -1; - config.refLengthModID = -1; - rocket.addComponentChangeListener(config); - return config; - } catch (CloneNotSupportedException e) { - throw new BugException("clone not supported!", e); - } - } - - - @Override - public int getModID() { - return modID + rocket.getModID(); - } - - - /** - * A class that iterates over all currently active components. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - private class ConfigurationIterator implements Iterator<RocketComponent> { - Iterator<Iterator<RocketComponent>> iterators; - Iterator<RocketComponent> current = null; - - public ConfigurationIterator() { - List<Iterator<RocketComponent>> list = new ArrayList<Iterator<RocketComponent>>(); - - for (RocketComponent stage : rocket.getChildren()) { - if (isComponentActive(stage)) { - list.add(stage.iterator(false)); - } - } - - // Get iterators and initialize current - iterators = list.iterator(); - if (iterators.hasNext()) { - current = iterators.next(); - } else { - List<RocketComponent> l = Collections.emptyList(); - current = l.iterator(); - } - } - - - @Override - public boolean hasNext() { - if (!current.hasNext()) - getNextIterator(); - - return current.hasNext(); - } - - @Override - public RocketComponent next() { - if (!current.hasNext()) - getNextIterator(); - - return current.next(); - } - - /** - * Get the next iterator that has items. If such an iterator does - * not exist, current is left to an empty iterator. - */ - private void getNextIterator() { - while ((!current.hasNext()) && iterators.hasNext()) { - current = iterators.next(); - } - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove unsupported"); - } - } - - private class MotorIterator implements Iterator<MotorMount> { - private final Iterator<RocketComponent> iterator; - private MotorMount next = null; - - public MotorIterator() { - this.iterator = iterator(); - } - - @Override - public boolean hasNext() { - getNext(); - return (next != null); - } - - @Override - public MotorMount next() { - getNext(); - if (next == null) { - throw new NoSuchElementException("iterator called for too long"); - } - - MotorMount ret = next; - next = null; - return ret; - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove unsupported"); - } - - private void getNext() { - if (next != null) - return; - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - if (c instanceof MotorMount) { - MotorMount mount = (MotorMount) c; - if (mount.isMotorMount() && mount.getMotor(motorConfiguration) != null) { - next = mount; - return; - } - } - } - } - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java b/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java deleted file mode 100644 index 0fa9497c..00000000 --- a/src/net/sf/openrocket/rocketcomponent/EllipticalFinSet.java +++ /dev/null @@ -1,78 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -public class EllipticalFinSet extends FinSet { - private static final Translator trans = Application.getTranslator(); - - private static final int POINTS = 31; - - // Static positioning for the fin points - private static final double[] POINT_X = new double[POINTS]; - private static final double[] POINT_Y = new double[POINTS]; - static { - for (int i = 0; i < POINTS; i++) { - double a = Math.PI * (POINTS - 1 - i) / (POINTS - 1); - POINT_X[i] = (Math.cos(a) + 1) / 2; - POINT_Y[i] = Math.sin(a); - } - POINT_X[0] = 0; - POINT_Y[0] = 0; - POINT_X[POINTS - 1] = 1; - POINT_Y[POINTS - 1] = 0; - } - - - private double height = 0.05; - - public EllipticalFinSet() { - this.length = 0.05; - } - - - @Override - public Coordinate[] getFinPoints() { - double len = MathUtil.max(length, 0.0001); - Coordinate[] points = new Coordinate[POINTS]; - for (int i = 0; i < POINTS; i++) { - points[i] = new Coordinate(POINT_X[i] * len, POINT_Y[i] * height); - } - return points; - } - - @Override - public double getSpan() { - return height; - } - - @Override - public String getComponentName() { - //// Elliptical fin set - return trans.get("EllipticalFinSet.Ellipticalfinset"); - } - - - public double getHeight() { - return height; - } - - public void setHeight(double height) { - if (MathUtil.equals(this.height, height)) - return; - this.height = height; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - public void setLength(double length) { - if (MathUtil.equals(this.length, length)) - return; - this.length = length; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - -} diff --git a/src/net/sf/openrocket/rocketcomponent/EngineBlock.java b/src/net/sf/openrocket/rocketcomponent/EngineBlock.java deleted file mode 100644 index 2c2c590b..00000000 --- a/src/net/sf/openrocket/rocketcomponent/EngineBlock.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - - -public class EngineBlock extends ThicknessRingComponent { - - public EngineBlock() { - super(); - setOuterRadiusAutomatic(true); - setThickness(0.005); - setLength(0.005); - } - - @Override - public void setOuterRadiusAutomatic(boolean auto) { - super.setOuterRadiusAutomatic(auto); - } - - @Override - public String getComponentName() { - return "Engine block"; - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java b/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java deleted file mode 100644 index 8d958da6..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ExternalComponent.java +++ /dev/null @@ -1,164 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.List; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.material.Material.Type; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * Class of components with well-defined physical appearance and which have an effect on - * aerodynamic simulation. They have material defined for them, and the mass of the component - * is calculated using the component's volume. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public abstract class ExternalComponent extends RocketComponent { - - public enum Finish { - //// Rough - ROUGH("ExternalComponent.Rough", 500e-6), - //// Unfinished - UNFINISHED("ExternalComponent.Unfinished", 150e-6), - //// Regular paint - NORMAL("ExternalComponent.Regularpaint", 60e-6), - //// Smooth paint - SMOOTH("ExternalComponent.Smoothpaint", 20e-6), - //// Polished - POLISHED("ExternalComponent.Polished", 2e-6); - - private static final Translator trans = Application.getTranslator(); - private final String name; - private final double roughnessSize; - - Finish(String name, double roughness) { - this.name = name; - this.roughnessSize = roughness; - } - - public double getRoughnessSize() { - return roughnessSize; - } - - @Override - public String toString() { - return trans.get(name) + " (" + UnitGroup.UNITS_ROUGHNESS.toStringUnit(roughnessSize) + ")"; - } - } - - - /** - * The material of the component. - */ - protected Material material = null; - - protected Finish finish = Finish.NORMAL; - - - - /** - * Constructor that sets the relative position of the component. - */ - public ExternalComponent(RocketComponent.Position relativePosition) { - super(relativePosition); - this.material = Application.getPreferences().getDefaultComponentMaterial(this.getClass(), Material.Type.BULK); - } - - /** - * Returns the volume of the component. This value is used in calculating the mass - * of the object. - */ - public abstract double getComponentVolume(); - - /** - * Calculates the mass of the component as the product of the volume and interior density. - */ - @Override - public double getComponentMass() { - return material.getDensity() * getComponentVolume(); - } - - /** - * ExternalComponent has aerodynamic effect, so return true. - */ - @Override - public boolean isAerodynamic() { - return true; - } - - /** - * ExternalComponent has effect on the mass, so return true. - */ - @Override - public boolean isMassive() { - return true; - } - - - public Material getMaterial() { - return material; - } - - public void setMaterial(Material mat) { - if (mat.getType() != Material.Type.BULK) { - throw new IllegalArgumentException("ExternalComponent requires a bulk material" + - " type=" + mat.getType()); - } - - if (material.equals(mat)) - return; - material = mat; - clearPreset(); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - clearPreset(); - } - - public Finish getFinish() { - return finish; - } - - public void setFinish(Finish finish) { - if (this.finish == finish) - return; - this.finish = finish; - fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); - } - - - @Override - protected void loadFromPreset(RocketComponent preset) { - super.loadFromPreset(preset); - - // Surface finish is left unchanged - - ExternalComponent c = (ExternalComponent) preset; - - Material mat = c.getMaterial(); - if (c.isMassOverridden()) { - double mass = c.getOverrideMass(); - double volume = getComponentVolume(); - double density; - if (volume > 0.00001) { - density = mass / volume; - } else { - density = 1000; - } - mat = Material.newMaterial(Type.BULK, mat.getName(), density, true); - } - - setMaterial(mat); - } - - - @Override - protected List<RocketComponent> copyFrom(RocketComponent c) { - ExternalComponent src = (ExternalComponent) c; - this.finish = src.finish; - this.material = src.material; - return super.copyFrom(c); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/FinSet.java b/src/net/sf/openrocket/rocketcomponent/FinSet.java deleted file mode 100644 index 5e40810b..00000000 --- a/src/net/sf/openrocket/rocketcomponent/FinSet.java +++ /dev/null @@ -1,713 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Transformation; - - -public abstract class FinSet extends ExternalComponent { - private static final Translator trans = Application.getTranslator(); - - /** - * Maximum allowed cant of fins. - */ - public static final double MAX_CANT = (15.0 * Math.PI / 180); - - - public enum CrossSection { - //// Square - SQUARE(trans.get("FinSet.CrossSection.SQUARE"), 1.00), - //// Rounded - ROUNDED(trans.get("FinSet.CrossSection.ROUNDED"), 0.99), - //// Airfoil - AIRFOIL(trans.get("FinSet.CrossSection.AIRFOIL"), 0.85); - - private final String name; - private final double volume; - - CrossSection(String name, double volume) { - this.name = name; - this.volume = volume; - } - - public double getRelativeVolume() { - return volume; - } - - @Override - public String toString() { - return name; - } - } - - public enum TabRelativePosition { - //// Root chord leading edge - FRONT(trans.get("FinSet.TabRelativePosition.FRONT")), - //// Root chord midpoint - CENTER(trans.get("FinSet.TabRelativePosition.CENTER")), - //// Root chord trailing edge - END(trans.get("FinSet.TabRelativePosition.END")); - - private final String name; - - TabRelativePosition(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - /** - * Number of fins. - */ - protected int fins = 3; - - /** - * Rotation about the x-axis by 2*PI/fins. - */ - protected Transformation finRotation = Transformation.rotate_x(2 * Math.PI / fins); - - /** - * Rotation angle of the first fin. Zero corresponds to the positive y-axis. - */ - protected double rotation = 0; - - /** - * Rotation about the x-axis by angle this.rotation. - */ - protected Transformation baseRotation = Transformation.rotate_x(rotation); - - - /** - * Cant angle of fins. - */ - protected double cantAngle = 0; - - /* Cached value: */ - private Transformation cantRotation = null; - - - /** - * Thickness of the fins. - */ - protected double thickness = 0.003; - - - /** - * The cross-section shape of the fins. - */ - protected CrossSection crossSection = CrossSection.SQUARE; - - - /* - * Fin tab properties. - */ - private double tabHeight = 0; - private double tabLength = 0.05; - private double tabShift = 0; - private TabRelativePosition tabRelativePosition = TabRelativePosition.CENTER; - - - // Cached fin area & CG. Validity of both must be checked using finArea! - // Fin area does not include fin tabs, CG does. - private double finArea = -1; - private double finCGx = -1; - private double finCGy = -1; - - - /** - * New FinSet with given number of fins and given base rotation angle. - * Sets the component relative position to POSITION_RELATIVE_BOTTOM, - * i.e. fins are positioned at the bottom of the parent component. - */ - public FinSet() { - super(RocketComponent.Position.BOTTOM); - } - - - - /** - * Return the number of fins in the set. - * @return The number of fins. - */ - public int getFinCount() { - return fins; - } - - /** - * Sets the number of fins in the set. - * @param n The number of fins, greater of equal to one. - */ - public void setFinCount(int n) { - if (fins == n) - return; - if (n < 1) - n = 1; - if (n > 8) - n = 8; - fins = n; - finRotation = Transformation.rotate_x(2 * Math.PI / fins); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public Transformation getFinRotationTransformation() { - return finRotation; - } - - /** - * Gets the base rotation amount of the first fin. - * @return The base rotation amount. - */ - public double getBaseRotation() { - return rotation; - } - - /** - * Sets the base rotation amount of the first fin. - * @param r The base rotation amount. - */ - public void setBaseRotation(double r) { - r = MathUtil.reduce180(r); - if (MathUtil.equals(r, rotation)) - return; - rotation = r; - baseRotation = Transformation.rotate_x(rotation); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public Transformation getBaseRotationTransformation() { - return baseRotation; - } - - - - public double getCantAngle() { - return cantAngle; - } - - public void setCantAngle(double cant) { - cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT); - if (MathUtil.equals(cant, cantAngle)) - return; - this.cantAngle = cant; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - public Transformation getCantRotation() { - if (cantRotation == null) { - if (MathUtil.equals(cantAngle, 0)) { - cantRotation = Transformation.IDENTITY; - } else { - Transformation t = new Transformation(-length / 2, 0, 0); - t = Transformation.rotate_y(cantAngle).applyTransformation(t); - t = new Transformation(length / 2, 0, 0).applyTransformation(t); - cantRotation = t; - } - } - return cantRotation; - } - - - - public double getThickness() { - return thickness; - } - - public void setThickness(double r) { - if (thickness == r) - return; - thickness = Math.max(r, 0); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - public CrossSection getCrossSection() { - return crossSection; - } - - public void setCrossSection(CrossSection cs) { - if (crossSection == cs) - return; - crossSection = cs; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - - - @Override - public void setRelativePosition(RocketComponent.Position position) { - super.setRelativePosition(position); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - @Override - public void setPositionValue(double value) { - super.setPositionValue(value); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - - public double getTabHeight() { - return tabHeight; - } - - public void setTabHeight(double height) { - height = MathUtil.max(height, 0); - if (MathUtil.equals(this.tabHeight, height)) - return; - this.tabHeight = height; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public double getTabLength() { - return tabLength; - } - - public void setTabLength(double length) { - length = MathUtil.max(length, 0); - if (MathUtil.equals(this.tabLength, length)) - return; - this.tabLength = length; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public double getTabShift() { - return tabShift; - } - - public void setTabShift(double shift) { - this.tabShift = shift; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public TabRelativePosition getTabRelativePosition() { - return tabRelativePosition; - } - - public void setTabRelativePosition(TabRelativePosition position) { - if (this.tabRelativePosition == position) - return; - - - double front = getTabFrontEdge(); - switch (position) { - case FRONT: - this.tabShift = front; - break; - - case CENTER: - this.tabShift = front + tabLength / 2 - getLength() / 2; - break; - - case END: - this.tabShift = front + tabLength - getLength(); - break; - - default: - throw new IllegalArgumentException("position=" + position); - } - this.tabRelativePosition = position; - - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - /** - * Return the tab front edge position from the front of the fin. - */ - public double getTabFrontEdge() { - switch (this.tabRelativePosition) { - case FRONT: - return tabShift; - - case CENTER: - return getLength() / 2 - tabLength / 2 + tabShift; - - case END: - return getLength() - tabLength + tabShift; - - default: - throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); - } - } - - /** - * Return the tab trailing edge position *from the front of the fin*. - */ - public double getTabTrailingEdge() { - switch (this.tabRelativePosition) { - case FRONT: - return tabLength + tabShift; - case CENTER: - return getLength() / 2 + tabLength / 2 + tabShift; - - case END: - return getLength() + tabShift; - - default: - throw new IllegalStateException("tabRelativePosition=" + tabRelativePosition); - } - } - - - - - /////////// Calculation methods /////////// - - /** - * Return the area of one side of one fin. This does NOT include the area of - * the fin tab. - * - * @return the area of one side of one fin. - */ - public double getFinArea() { - if (finArea < 0) - calculateAreaCG(); - - return finArea; - } - - - /** - * Return the unweighted CG of a single fin. The X-coordinate is relative to - * the root chord trailing edge and the Y-coordinate to the fin root chord. - * - * @return the unweighted CG coordinate of a single fin. - */ - public Coordinate getFinCG() { - if (finArea < 0) - calculateAreaCG(); - - return new Coordinate(finCGx, finCGy, 0); - } - - - - @Override - public double getComponentVolume() { - return fins * (getFinArea() + tabHeight * tabLength) * thickness * - crossSection.getRelativeVolume(); - } - - - @Override - public Coordinate getComponentCG() { - if (finArea < 0) - calculateAreaCG(); - - double mass = getComponentMass(); // safe - - if (fins == 1) { - return baseRotation.transform( - new Coordinate(finCGx, finCGy + getBodyRadius(), 0, mass)); - } else { - return new Coordinate(finCGx, 0, 0, mass); - } - } - - - private void calculateAreaCG() { - Coordinate[] points = this.getFinPoints(); - finArea = 0; - finCGx = 0; - finCGy = 0; - - for (int i = 0; i < points.length - 1; i++) { - final double x0 = points[i].x; - final double x1 = points[i + 1].x; - final double y0 = points[i].y; - final double y1 = points[i + 1].y; - - double da = (y0 + y1) * (x1 - x0) / 2; - finArea += da; - if (Math.abs(y0 - y1) < 0.00001) { - finCGx += (x0 + x1) / 2 * da; - finCGy += y0 / 2 * da; - } else { - finCGx += (x0 * (2 * y0 + y1) + x1 * (y0 + 2 * y1)) / (3 * (y0 + y1)) * da; - finCGy += (y1 + y0 * y0 / (y0 + y1)) / 3 * da; - } - } - - if (finArea < 0) - finArea = 0; - - // Add effect of fin tabs to CG - double tabArea = tabLength * tabHeight; - if (!MathUtil.equals(tabArea, 0)) { - - double x = (getTabFrontEdge() + getTabTrailingEdge()) / 2; - double y = -this.tabHeight / 2; - - finCGx += x * tabArea; - finCGy += y * tabArea; - - } - - if ((finArea + tabArea) > 0) { - finCGx /= (finArea + tabArea); - finCGy /= (finArea + tabArea); - } else { - finCGx = (points[0].x + points[points.length - 1].x) / 2; - finCGy = 0; - } - } - - - /* - * Return an approximation of the longitudinal unitary inertia of the fin set. - * The process is the following: - * - * 1. Approximate the fin with a rectangular fin - * - * 2. The inertia of one fin is taken as the average of the moments of inertia - * through its center perpendicular to the plane, and the inertia through - * its center parallel to the plane - * - * 3. If there are multiple fins, the inertia is shifted to the center of the fin - * set and multiplied by the number of fins. - */ - @Override - public double getLongitudinalUnitInertia() { - double area = getFinArea(); - if (MathUtil.equals(area, 0)) - return 0; - - // Approximate fin with a rectangular fin - // w2 and h2 are squares of the fin width and height - double w = getLength(); - double h = getSpan(); - double w2, h2; - - if (MathUtil.equals(w * h, 0)) { - w2 = area; - h2 = area; - } else { - w2 = w * area / h; - h2 = h * area / w; - } - - double inertia = (h2 + 2 * w2) / 24; - - if (fins == 1) - return inertia; - - double radius = getBodyRadius(); - - return fins * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + radius)); - } - - - /* - * Return an approximation of the rotational unitary inertia of the fin set. - * The process is the following: - * - * 1. Approximate the fin with a rectangular fin and calculate the inertia of the - * rectangular approximate - * - * 2. If there are multiple fins, shift the inertia center to the fin set center - * and multiply with the number of fins. - */ - @Override - public double getRotationalUnitInertia() { - double area = getFinArea(); - if (MathUtil.equals(area, 0)) - return 0; - - // Approximate fin with a rectangular fin - double w = getLength(); - double h = getSpan(); - - if (MathUtil.equals(w * h, 0)) { - h = MathUtil.safeSqrt(area); - } else { - h = MathUtil.safeSqrt(h * area / w); - } - - if (fins == 1) - return h * h / 12; - - double radius = getBodyRadius(); - - return fins * (h * h / 12 + MathUtil.pow2(h / 2 + radius)); - } - - - /** - * Adds the fin set's bounds to the collection. - */ - @Override - public Collection<Coordinate> getComponentBounds() { - List<Coordinate> bounds = new ArrayList<Coordinate>(); - double r = getBodyRadius(); - - for (Coordinate point : getFinPoints()) { - addFinBound(bounds, point.x, point.y + r); - } - - return bounds; - } - - - /** - * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for - * all fin rotations. - */ - private void addFinBound(Collection<Coordinate> set, double x, double y) { - Coordinate c; - int i; - - c = new Coordinate(x, y, thickness / 2); - c = baseRotation.transform(c); - set.add(c); - for (i = 1; i < fins; i++) { - c = finRotation.transform(c); - set.add(c); - } - - c = new Coordinate(x, y, -thickness / 2); - c = baseRotation.transform(c); - set.add(c); - for (i = 1; i < fins; i++) { - c = finRotation.transform(c); - set.add(c); - } - } - - - - @Override - public void componentChanged(ComponentChangeEvent e) { - if (e.isAerodynamicChange()) { - finArea = -1; - cantRotation = null; - } - } - - - /** - * Return the radius of the BodyComponent the fin set is situated on. Currently - * only supports SymmetricComponents and returns the radius at the starting point of the - * root chord. - * - * @return radius of the underlying BodyComponent or 0 if none exists. - */ - public double getBodyRadius() { - RocketComponent s; - - s = this.getParent(); - while (s != null) { - if (s instanceof SymmetricComponent) { - double x = this.toRelative(new Coordinate(0, 0, 0), s)[0].x; - return ((SymmetricComponent) s).getRadius(x); - } - s = s.getParent(); - } - return 0; - } - - @Override - public boolean allowsChildren() { - return false; - } - - /** - * Allows nothing to be attached to a FinSet. - * - * @return <code>false</code> - */ - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } - - - - - /** - * Return a list of coordinates defining the geometry of a single fin. - * The coordinates are the XY-coordinates of points defining the shape of a single fin, - * where the origin is the leading root edge. Therefore, the first point must be (0,0,0). - * All Z-coordinates must be zero, and the last coordinate must have Y=0. - * - * @return List of XY-coordinates. - */ - public abstract Coordinate[] getFinPoints(); - - - /** - * Return a list of coordinates defining the geometry of a single fin, including a - * possible fin tab. The coordinates are the XY-coordinates of points defining the - * shape of a single fin, where the origin is the leading root edge. This implementation - * calls {@link #getFinPoints()} and adds the necessary points for the fin tab. - * The tab coordinates will have a negative y value. - * - * @return List of XY-coordinates. - */ - public Coordinate[] getFinPointsWithTab() { - Coordinate[] points = getFinPoints(); - - if (MathUtil.equals(getTabHeight(), 0) || - MathUtil.equals(getTabLength(), 0)) - return points; - - double x1 = getTabFrontEdge(); - double x2 = getTabTrailingEdge(); - double y = -getTabHeight(); - - int n = points.length; - points = Arrays.copyOf(points, points.length + 4); - points[n] = new Coordinate(x2, 0); - points[n + 1] = new Coordinate(x2, y); - points[n + 2] = new Coordinate(x1, y); - points[n + 3] = new Coordinate(x1, 0); - return points; - } - - - - /** - * Get the span of a single fin. That is, the length from the root to the tip of the fin. - * @return Span of a single fin. - */ - public abstract double getSpan(); - - - @Override - protected List<RocketComponent> copyFrom(RocketComponent c) { - FinSet src = (FinSet) c; - this.fins = src.fins; - this.finRotation = src.finRotation; - this.rotation = src.rotation; - this.baseRotation = src.baseRotation; - this.cantAngle = src.cantAngle; - this.cantRotation = src.cantRotation; - this.thickness = src.thickness; - this.crossSection = src.crossSection; - this.tabHeight = src.tabHeight; - this.tabLength = src.tabLength; - this.tabRelativePosition = src.tabRelativePosition; - this.tabShift = src.tabShift; - - return super.copyFrom(c); - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java b/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java deleted file mode 100644 index ffa44be3..00000000 --- a/src/net/sf/openrocket/rocketcomponent/FreeformFinSet.java +++ /dev/null @@ -1,339 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; - - -public class FreeformFinSet extends FinSet { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - private ArrayList<Coordinate> points = new ArrayList<Coordinate>(); - - public FreeformFinSet() { - points.add(Coordinate.NUL); - points.add(new Coordinate(0.025, 0.05)); - points.add(new Coordinate(0.075, 0.05)); - points.add(new Coordinate(0.05, 0)); - - this.length = 0.05; - } - - - public FreeformFinSet(Coordinate[] finpoints) throws IllegalFinPointException { - setPoints(finpoints); - } - - /* - public FreeformFinSet(FinSet finset) { - Coordinate[] finpoints = finset.getFinPoints(); - this.copyFrom(finset); - - points.clear(); - for (Coordinate c: finpoints) { - points.add(c); - } - this.length = points.get(points.size()-1).x - points.get(0).x; - } - */ - - - /** - * Convert an existing fin set into a freeform fin set. The specified - * fin set is taken out of the rocket tree (if any) and the new component - * inserted in its stead. - * <p> - * The specified fin set should not be used after the call! - * - * @param finset the fin set to convert. - * @return the new freeform fin set. - */ - public static FreeformFinSet convertFinSet(FinSet finset) { - log.info("Converting " + finset.getComponentName() + " into freeform fin set"); - final RocketComponent root = finset.getRoot(); - FreeformFinSet freeform; - List<RocketComponent> toInvalidate = Collections.emptyList(); - - try { - if (root instanceof Rocket) { - ((Rocket) root).freeze(); - } - - // Get fin set position and remove fin set - final RocketComponent parent = finset.getParent(); - final int position; - if (parent != null) { - position = parent.getChildPosition(finset); - parent.removeChild(position); - } else { - position = -1; - } - - - // Create the freeform fin set - Coordinate[] finpoints = finset.getFinPoints(); - try { - freeform = new FreeformFinSet(finpoints); - } catch (IllegalFinPointException e) { - throw new BugException("Illegal fin points when converting existing fin to " + - "freeform fin, fin=" + finset + " points=" + Arrays.toString(finpoints), - e); - } - - // Copy component attributes - toInvalidate = freeform.copyFrom(finset); - - // Set name - final String componentTypeName = finset.getComponentName(); - final String name = freeform.getName(); - - if (name.startsWith(componentTypeName)) { - freeform.setName(freeform.getComponentName() + - name.substring(componentTypeName.length())); - } - - // Add freeform fin set to parent - if (parent != null) { - parent.addChild(freeform, position); - } - - } finally { - if (root instanceof Rocket) { - ((Rocket) root).thaw(); - } - // Invalidate components after events have been fired - for (RocketComponent c : toInvalidate) { - c.invalidate(); - } - } - return freeform; - } - - - - /** - * Add a fin point between indices <code>index-1</code> and <code>index</code>. - * The point is placed at the midpoint of the current segment. - * - * @param index the fin point before which to add the new point. - */ - public void addPoint(int index) { - double x0, y0, x1, y1; - - x0 = points.get(index - 1).x; - y0 = points.get(index - 1).y; - x1 = points.get(index).x; - y1 = points.get(index).y; - - points.add(index, new Coordinate((x0 + x1) / 2, (y0 + y1) / 2)); - // adding a point within the segment affects neither mass nor aerodynamics - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - /** - * Remove the fin point with the given index. The first and last fin points - * cannot be removed, and will cause an <code>IllegalFinPointException</code> - * if attempted. - * - * @param index the fin point index to remove - * @throws IllegalFinPointException if removing would result in invalid fin planform - */ - public void removePoint(int index) throws IllegalFinPointException { - if (index == 0 || index == points.size() - 1) { - throw new IllegalFinPointException("cannot remove first or last point"); - } - - ArrayList<Coordinate> copy = this.points.clone(); - copy.remove(index); - validate(copy); - this.points = copy; - - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - public int getPointCount() { - return points.size(); - } - - public void setPoints(Coordinate[] points) throws IllegalFinPointException { - ArrayList<Coordinate> list = new ArrayList<Coordinate>(points.length); - for (Coordinate p : points) { - list.add(p); - } - validate(list); - this.points = list; - - this.length = points[points.length - 1].x; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - /** - * Set the point at position <code>i</code> to coordinates (x,y). - * <p> - * Note that this method enforces basic fin shape restrictions (non-negative y, - * first and last point locations) silently, but throws an - * <code>IllegalFinPointException</code> if the point causes fin segments to - * intersect. - * <p> - * Moving of the first point in the X-axis is allowed, but this actually moves - * all of the other points the corresponding distance back. - * - * @param index the point index to modify. - * @param x the x-coordinate. - * @param y the y-coordinate. - * @throws IllegalFinPointException if the specified fin point would cause intersecting - * segments - */ - public void setPoint(int index, double x, double y) throws IllegalFinPointException { - if (y < 0) - y = 0; - - double x0, y0, x1, y1; - - if (index == 0) { - - // Restrict point - x = Math.min(x, points.get(points.size() - 1).x); - y = 0; - x0 = Double.NaN; - y0 = Double.NaN; - x1 = points.get(1).x; - y1 = points.get(1).y; - - } else if (index == points.size() - 1) { - - // Restrict point - x = Math.max(x, 0); - y = 0; - x0 = points.get(index - 1).x; - y0 = points.get(index - 1).y; - x1 = Double.NaN; - y1 = Double.NaN; - - } else { - - x0 = points.get(index - 1).x; - y0 = points.get(index - 1).y; - x1 = points.get(index + 1).x; - y1 = points.get(index + 1).y; - - } - - - - // Check for intersecting - double px0, py0, px1, py1; - px0 = 0; - py0 = 0; - for (int i = 1; i < points.size(); i++) { - px1 = points.get(i).x; - py1 = points.get(i).y; - - if (i != index - 1 && i != index && i != index + 1) { - if (intersects(x0, y0, x, y, px0, py0, px1, py1)) { - throw new IllegalFinPointException("segments intersect"); - } - } - if (i != index && i != index + 1 && i != index + 2) { - if (intersects(x, y, x1, y1, px0, py0, px1, py1)) { - throw new IllegalFinPointException("segments intersect"); - } - } - - px0 = px1; - py0 = py1; - } - - if (index == 0) { - - System.out.println("Set point zero to x:" + x); - for (int i = 1; i < points.size(); i++) { - Coordinate c = points.get(i); - points.set(i, c.setX(c.x - x)); - } - - } else { - - points.set(index, new Coordinate(x, y)); - - } - if (index == 0 || index == points.size() - 1) { - this.length = points.get(points.size() - 1).x; - } - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - private boolean intersects(double ax0, double ay0, double ax1, double ay1, - double bx0, double by0, double bx1, double by1) { - - double d = ((by1 - by0) * (ax1 - ax0) - (bx1 - bx0) * (ay1 - ay0)); - - double ua = ((bx1 - bx0) * (ay0 - by0) - (by1 - by0) * (ax0 - bx0)) / d; - double ub = ((ax1 - ax0) * (ay0 - by0) - (ay1 - ay0) * (ax0 - bx0)) / d; - - return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1); - } - - - @Override - public Coordinate[] getFinPoints() { - return points.toArray(new Coordinate[0]); - } - - @Override - public double getSpan() { - double max = 0; - for (Coordinate c : points) { - if (c.y > max) - max = c.y; - } - return max; - } - - @Override - public String getComponentName() { - //// Freeform fin set - return trans.get("FreeformFinSet.FreeformFinSet"); - } - - - @Override - protected RocketComponent copyWithOriginalID() { - RocketComponent c = super.copyWithOriginalID(); - ((FreeformFinSet) c).points = this.points.clone(); - return c; - } - - private void validate(ArrayList<Coordinate> pts) throws IllegalFinPointException { - final int n = pts.size(); - if (pts.get(0).x != 0 || pts.get(0).y != 0 || - pts.get(n - 1).x < 0 || pts.get(n - 1).y != 0) { - throw new IllegalFinPointException("Start or end point illegal."); - } - for (int i = 0; i < n - 1; i++) { - for (int j = i + 2; j < n - 1; j++) { - if (intersects(pts.get(i).x, pts.get(i).y, pts.get(i + 1).x, pts.get(i + 1).y, - pts.get(j).x, pts.get(j).y, pts.get(j + 1).x, pts.get(j + 1).y)) { - throw new IllegalFinPointException("segments intersect"); - } - } - if (pts.get(i).z != 0) { - throw new IllegalFinPointException("z-coordinate not zero"); - } - } - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/IllegalFinPointException.java b/src/net/sf/openrocket/rocketcomponent/IllegalFinPointException.java deleted file mode 100644 index 8f9a88ac..00000000 --- a/src/net/sf/openrocket/rocketcomponent/IllegalFinPointException.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -/** - * An exception signifying that an operation on the freeform fin set points was - * illegal (segments intersect, removing first or last point, etc). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class IllegalFinPointException extends Exception { - - public IllegalFinPointException() { - - } - - public IllegalFinPointException(String message) { - super(message); - } - - public IllegalFinPointException(Throwable cause) { - super(cause); - } - - public IllegalFinPointException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/InnerTube.java b/src/net/sf/openrocket/rocketcomponent/InnerTube.java deleted file mode 100644 index 0b8c92d2..00000000 --- a/src/net/sf/openrocket/rocketcomponent/InnerTube.java +++ /dev/null @@ -1,339 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -/** - * This class defines an inner tube that can be used as a motor mount. The component - * may also be clustered. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class InnerTube extends ThicknessRingComponent - implements Clusterable, RadialParent, MotorMount { - private static final Translator trans = Application.getTranslator(); - - private ClusterConfiguration cluster = ClusterConfiguration.SINGLE; - private double clusterScale = 1.0; - private double clusterRotation = 0.0; - - - private boolean motorMount = false; - private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>(); - private HashMap<String, Motor> motors = new HashMap<String, Motor>(); - private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC; - private double ignitionDelay = 0; - private double overhang = 0; - - - /** - * Main constructor. - */ - public InnerTube() { - // A-C motor size: - this.setOuterRadius(0.019 / 2); - this.setInnerRadius(0.018 / 2); - this.setLength(0.070); - } - - - @Override - public double getInnerRadius(double x) { - return getInnerRadius(); - } - - - @Override - public double getOuterRadius(double x) { - return getOuterRadius(); - } - - - @Override - public String getComponentName() { - //// Inner Tube - return trans.get("InnerTube.InnerTube"); - } - - @Override - public boolean allowsChildren() { - return true; - } - - /** - * Allow all InternalComponents to be added to this component. - */ - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return InternalComponent.class.isAssignableFrom(type); - } - - - - ///////////// Cluster methods ////////////// - - /** - * Get the current cluster configuration. - * @return The current cluster configuration. - */ - @Override - public ClusterConfiguration getClusterConfiguration() { - return cluster; - } - - /** - * Set the current cluster configuration. - * @param cluster The cluster configuration. - */ - @Override - public void setClusterConfiguration(ClusterConfiguration cluster) { - this.cluster = cluster; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - /** - * Return the number of tubes in the cluster. - * @return Number of tubes in the current cluster. - */ - @Override - public int getClusterCount() { - return cluster.getClusterCount(); - } - - /** - * Get the cluster scaling. A value of 1.0 indicates that the tubes are packed - * touching each other, larger values separate the tubes and smaller values - * pack inside each other. - */ - public double getClusterScale() { - return clusterScale; - } - - /** - * Set the cluster scaling. - * @see #getClusterScale() - */ - public void setClusterScale(double scale) { - scale = Math.max(scale, 0); - if (MathUtil.equals(clusterScale, scale)) - return; - clusterScale = scale; - fireComponentChangeEvent(new ComponentChangeEvent(this, ComponentChangeEvent.MASS_CHANGE)); - } - - - - /** - * @return the clusterRotation - */ - public double getClusterRotation() { - return clusterRotation; - } - - - /** - * @param rotation the clusterRotation to set - */ - public void setClusterRotation(double rotation) { - rotation = MathUtil.reduce180(rotation); - if (clusterRotation == rotation) - return; - this.clusterRotation = rotation; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - /** - * Return the distance between the closest two cluster inner tube center points. - * This is equivalent to the cluster scale multiplied by the tube diameter. - */ - @Override - public double getClusterSeparation() { - return 2 * getOuterRadius() * clusterScale; - } - - - public List<Coordinate> getClusterPoints() { - List<Coordinate> list = new ArrayList<Coordinate>(getClusterCount()); - List<Double> points = cluster.getPoints(clusterRotation - getRadialDirection()); - double separation = getClusterSeparation(); - for (int i = 0; i < points.size() / 2; i++) { - list.add(new Coordinate(0, points.get(2 * i) * separation, points.get(2 * i + 1) * separation)); - } - return list; - } - - - @Override - public Coordinate[] shiftCoordinates(Coordinate[] array) { - array = super.shiftCoordinates(array); - - int count = getClusterCount(); - if (count == 1) - return array; - - List<Coordinate> points = getClusterPoints(); - if (points.size() != count) { - throw new BugException("Inconsistent cluster configuration, cluster count=" + count + - " point count=" + points.size()); - } - Coordinate[] newArray = new Coordinate[array.length * count]; - for (int i = 0; i < array.length; i++) { - for (int j = 0; j < count; j++) { - newArray[i * count + j] = array[i].add(points.get(j)); - } - } - - return newArray; - } - - - - - //////////////// Motor mount ///////////////// - - @Override - public boolean isMotorMount() { - return motorMount; - } - - @Override - public void setMotorMount(boolean mount) { - if (motorMount == mount) - return; - motorMount = mount; - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - @Override - public Motor getMotor(String id) { - if (id == null) - return null; - - // Check whether the id is valid for the current rocket - RocketComponent root = this.getRoot(); - if (!(root instanceof Rocket)) - return null; - if (!((Rocket) root).isMotorConfigurationID(id)) - return null; - - return motors.get(id); - } - - @Override - public void setMotor(String id, Motor motor) { - if (id == null) { - if (motor != null) { - throw new IllegalArgumentException("Cannot set non-null motor for id null"); - } - } - Motor current = motors.get(id); - if ((motor == null && current == null) || - (motor != null && motor.equals(current))) - return; - motors.put(id, motor); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - @Override - public double getMotorDelay(String id) { - Double delay = ejectionDelays.get(id); - if (delay == null) - return Motor.PLUGGED; - return delay; - } - - @Override - public void setMotorDelay(String id, double delay) { - ejectionDelays.put(id, delay); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - @Deprecated - @Override - public int getMotorCount() { - return getClusterCount(); - } - - @Override - public double getMotorMountDiameter() { - return getInnerRadius() * 2; - } - - @Override - public IgnitionEvent getIgnitionEvent() { - return ignitionEvent; - } - - @Override - public void setIgnitionEvent(IgnitionEvent event) { - if (ignitionEvent == event) - return; - ignitionEvent = event; - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - } - - - @Override - public double getIgnitionDelay() { - return ignitionDelay; - } - - @Override - public void setIgnitionDelay(double delay) { - if (MathUtil.equals(delay, ignitionDelay)) - return; - ignitionDelay = delay; - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - } - - - @Override - public double getMotorOverhang() { - return overhang; - } - - @Override - public void setMotorOverhang(double overhang) { - if (MathUtil.equals(this.overhang, overhang)) - return; - this.overhang = overhang; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - @Override - public Coordinate getMotorPosition(String id) { - Motor motor = motors.get(id); - if (motor == null) { - throw new IllegalArgumentException("No motor with id " + id + " defined."); - } - - return new Coordinate(this.getLength() - motor.getLength() + this.getMotorOverhang()); - } - - /* - * (non-Javadoc) - * Copy the motor and ejection delay HashMaps. - * - * @see rocketcomponent.RocketComponent#copy() - */ - @SuppressWarnings("unchecked") - @Override - protected RocketComponent copyWithOriginalID() { - RocketComponent c = super.copyWithOriginalID(); - ((InnerTube) c).motors = (HashMap<String, Motor>) motors.clone(); - ((InnerTube) c).ejectionDelays = (HashMap<String, Double>) ejectionDelays.clone(); - return c; - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/rocketcomponent/InternalComponent.java b/src/net/sf/openrocket/rocketcomponent/InternalComponent.java deleted file mode 100644 index 1f2dba40..00000000 --- a/src/net/sf/openrocket/rocketcomponent/InternalComponent.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - - -/** - * A component internal to the rocket. Internal components have no effect on the - * the aerodynamics of a rocket, only its mass properties (though the location of the - * components is not enforced to be within external components). Internal components - * are always attached relative to the parent component, which can be internal or - * external, or absolutely. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class InternalComponent extends RocketComponent { - - public InternalComponent() { - super(RocketComponent.Position.BOTTOM); - } - - - @Override - public final void setRelativePosition(RocketComponent.Position position) { - super.setRelativePosition(position); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - @Override - public final void setPositionValue(double value) { - super.setPositionValue(value); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - /** - * Non-aerodynamic components. - * @return <code>false</code> - */ - @Override - public final boolean isAerodynamic() { - return false; - } - - /** - * Is massive. - * @return <code>true</code> - */ - @Override - public final boolean isMassive() { - return true; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/LaunchLug.java b/src/net/sf/openrocket/rocketcomponent/LaunchLug.java deleted file mode 100644 index 5614e077..00000000 --- a/src/net/sf/openrocket/rocketcomponent/LaunchLug.java +++ /dev/null @@ -1,208 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayList; -import java.util.Collection; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - - -public class LaunchLug extends ExternalComponent implements Coaxial { - - private static final Translator trans = Application.getTranslator(); - - private double radius; - private double thickness; - - private double radialDirection = 0; - - /* These are calculated when the component is first attached to any Rocket */ - private double shiftY, shiftZ; - - - - public LaunchLug() { - super(Position.MIDDLE); - radius = 0.01 / 2; - thickness = 0.001; - length = 0.03; - } - - - @Override - public double getOuterRadius() { - return radius; - } - - @Override - public void setOuterRadius(double radius) { - if (MathUtil.equals(this.radius, radius)) - return; - this.radius = radius; - this.thickness = Math.min(this.thickness, this.radius); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - @Override - public double getInnerRadius() { - return radius - thickness; - } - - @Override - public void setInnerRadius(double innerRadius) { - setOuterRadius(innerRadius + thickness); - } - - @Override - public double getThickness() { - return thickness; - } - - public void setThickness(double thickness) { - if (MathUtil.equals(this.thickness, thickness)) - return; - this.thickness = MathUtil.clamp(thickness, 0, radius); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - public double getRadialDirection() { - return radialDirection; - } - - public void setRadialDirection(double direction) { - direction = MathUtil.reduce180(direction); - if (MathUtil.equals(this.radialDirection, direction)) - return; - this.radialDirection = direction; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - public void setLength(double length) { - if (MathUtil.equals(this.length, length)) - return; - this.length = length; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - - - @Override - public void setRelativePosition(RocketComponent.Position position) { - super.setRelativePosition(position); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - @Override - public void setPositionValue(double value) { - super.setPositionValue(value); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - @Override - public Coordinate[] shiftCoordinates(Coordinate[] array) { - array = super.shiftCoordinates(array); - - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(0, shiftY, shiftZ); - } - - return array; - } - - - @Override - public void componentChanged(ComponentChangeEvent e) { - super.componentChanged(e); - - /* - * shiftY and shiftZ must be computed here since calculating them - * in shiftCoordinates() would cause an infinite loop due to .toRelative - */ - RocketComponent body; - double parentRadius; - - for (body = this.getParent(); body != null; body = body.getParent()) { - if (body instanceof SymmetricComponent) - break; - } - - if (body == null) { - parentRadius = 0; - } else { - SymmetricComponent s = (SymmetricComponent) body; - double x1, x2; - x1 = this.toRelative(Coordinate.NUL, body)[0].x; - x2 = this.toRelative(new Coordinate(length, 0, 0), body)[0].x; - x1 = MathUtil.clamp(x1, 0, body.getLength()); - x2 = MathUtil.clamp(x2, 0, body.getLength()); - parentRadius = Math.max(s.getRadius(x1), s.getRadius(x2)); - } - - shiftY = Math.cos(radialDirection) * (parentRadius + radius); - shiftZ = Math.sin(radialDirection) * (parentRadius + radius); - - // System.out.println("Computed shift: y="+shiftY+" z="+shiftZ); - } - - - - - @Override - public double getComponentVolume() { - return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius - thickness)); - } - - @Override - public Collection<Coordinate> getComponentBounds() { - ArrayList<Coordinate> set = new ArrayList<Coordinate>(); - addBound(set, 0, radius); - addBound(set, length, radius); - return set; - } - - @Override - public Coordinate getComponentCG() { - return new Coordinate(length / 2, 0, 0, getComponentMass()); - } - - @Override - public String getComponentName() { - //// Launch lug - return trans.get("LaunchLug.Launchlug"); - } - - @Override - public double getLongitudinalUnitInertia() { - // 1/12 * (3 * (r1^2 + r2^2) + h^2) - return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getOuterRadius()) + MathUtil.pow2(getLength())) / 12; - } - - @Override - public double getRotationalUnitInertia() { - // 1/2 * (r1^2 + r2^2) - return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getOuterRadius())) / 2; - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - // Allow nothing to be attached to a LaunchLug - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/MassComponent.java b/src/net/sf/openrocket/rocketcomponent/MassComponent.java deleted file mode 100644 index 49c139b0..00000000 --- a/src/net/sf/openrocket/rocketcomponent/MassComponent.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; - -/** - * This class represents a generic component that has a specific mass and an approximate shape. - * The mass is accessed via get/setComponentMass. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MassComponent extends MassObject { - private static final Translator trans = Application.getTranslator(); - - private double mass = 0; - - - public MassComponent() { - super(); - } - - public MassComponent(double length, double radius, double mass) { - super(length, radius); - this.mass = mass; - } - - - @Override - public double getComponentMass() { - return mass; - } - - public void setComponentMass(double mass) { - mass = Math.max(mass, 0); - if (MathUtil.equals(this.mass, mass)) - return; - this.mass = mass; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - @Override - public String getComponentName() { - //// Mass component - return trans.get("MassComponent.MassComponent"); - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - // Allow no components to be attached to a MassComponent - return false; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/MassObject.java b/src/net/sf/openrocket/rocketcomponent/MassObject.java deleted file mode 100644 index fb4aa744..00000000 --- a/src/net/sf/openrocket/rocketcomponent/MassObject.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.util.ArrayList; -import java.util.Collection; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -/** - * A MassObject is an internal component that can a specific weight, but not necessarily a strictly bound shape. It is - * represented as a homogeneous cylinder and drawn in the rocket figure with rounded corners. - * <p/> - * Subclasses of this class need only implement the {@link #getComponentMass()}, {@link #getComponentName()} and {@link - * #isCompatible(RocketComponent)} methods. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class MassObject extends InternalComponent { - - private double radius; - - private double radialPosition; - private double radialDirection; - - private double shiftY = 0; - private double shiftZ = 0; - - - public MassObject() { - this(0.025, 0.0125); - } - - public MassObject(double length, double radius) { - super(); - - this.length = length; - this.radius = radius; - - this.setRelativePosition(Position.TOP); - this.setPositionValue(0.0); - } - - - public void setLength(double length) { - length = Math.max(length, 0); - if (MathUtil.equals(this.length, length)) { - return; - } - this.length = length; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public final double getRadius() { - return radius; - } - - - public final void setRadius(double radius) { - radius = Math.max(radius, 0); - if (MathUtil.equals(this.radius, radius)) { - return; - } - this.radius = radius; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - public final double getRadialPosition() { - return radialPosition; - } - - public final void setRadialPosition(double radialPosition) { - radialPosition = Math.max(radialPosition, 0); - if (MathUtil.equals(this.radialPosition, radialPosition)) { - return; - } - this.radialPosition = radialPosition; - shiftY = radialPosition * Math.cos(radialDirection); - shiftZ = radialPosition * Math.sin(radialDirection); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public final double getRadialDirection() { - return radialDirection; - } - - public final void setRadialDirection(double radialDirection) { - radialDirection = MathUtil.reduce180(radialDirection); - if (MathUtil.equals(this.radialDirection, radialDirection)) { - return; - } - this.radialDirection = radialDirection; - shiftY = radialPosition * Math.cos(radialDirection); - shiftZ = radialPosition * Math.sin(radialDirection); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - /** - * Shift the coordinates according to the radial position and direction. - */ - @Override - public final Coordinate[] shiftCoordinates(Coordinate[] array) { - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(0, shiftY, shiftZ); - } - return array; - } - - @Override - public final Coordinate getComponentCG() { - return new Coordinate(length / 2, shiftY, shiftZ, getComponentMass()); - } - - @Override - public final double getLongitudinalUnitInertia() { - return (3 * pow2(radius) + pow2(length)) / 12; - } - - @Override - public final double getRotationalUnitInertia() { - return pow2(radius) / 2; - } - - @Override - public final Collection<Coordinate> getComponentBounds() { - Collection<Coordinate> c = new ArrayList<Coordinate>(); - addBound(c, 0, radius); - addBound(c, length, radius); - return c; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/MotorMount.java b/src/net/sf/openrocket/rocketcomponent/MotorMount.java deleted file mode 100644 index 1c849d4d..00000000 --- a/src/net/sf/openrocket/rocketcomponent/MotorMount.java +++ /dev/null @@ -1,213 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Coordinate; - -public interface MotorMount extends ChangeSource { - - public static enum IgnitionEvent { - //// Automatic (launch or ejection charge) - AUTOMATIC("MotorMount.IgnitionEvent.AUTOMATIC") { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - int count = source.getRocket().getStageCount(); - int stage = source.getStageNumber(); - - if (stage == count - 1) { - return LAUNCH.isActivationEvent(e, source); - } else { - return EJECTION_CHARGE.isActivationEvent(e, source); - } - } - }, - //// Launch - LAUNCH("MotorMount.IgnitionEvent.LAUNCH") { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - return (e.getType() == FlightEvent.Type.LAUNCH); - } - }, - //// First ejection charge of previous stage - EJECTION_CHARGE("MotorMount.IgnitionEvent.EJECTION_CHARGE") { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - if (e.getType() != FlightEvent.Type.EJECTION_CHARGE) - return false; - - int charge = e.getSource().getStageNumber(); - int mount = source.getStageNumber(); - return (mount + 1 == charge); - } - }, - //// First burnout of previous stage - BURNOUT("MotorMount.IgnitionEvent.BURNOUT") { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - if (e.getType() != FlightEvent.Type.BURNOUT) - return false; - - int charge = e.getSource().getStageNumber(); - int mount = source.getStageNumber(); - return (mount + 1 == charge); - } - }, - //// Never - NEVER("MotorMount.IgnitionEvent.NEVER") { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - return false; - } - }, - ; - - - private static final Translator trans = Application.getTranslator(); - private final String description; - - IgnitionEvent(String description) { - this.description = description; - } - - public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source); - - @Override - public String toString() { - return trans.get(description); - } - }; - - - /** - * Is the component currently a motor mount. - * - * @return whether the component holds a motor. - */ - public boolean isMotorMount(); - - /** - * Set whether the component is currently a motor mount. - */ - public void setMotorMount(boolean mount); - - - /** - * Return the motor for the motor configuration. May return <code>null</code> - * if no motor has been set. This method must return <code>null</code> if ID - * is <code>null</code> or if the ID is not valid for the current rocket - * (or if the component is not part of any rocket). - * - * @param id the motor configuration ID - * @return the motor, or <code>null</code> if not set. - */ - public Motor getMotor(String id); - - /** - * Set the motor for the motor configuration. May be set to <code>null</code> - * to remove the motor. - * - * @param id the motor configuration ID - * @param motor the motor, or <code>null</code>. - */ - public void setMotor(String id, Motor motor); - - /** - * Get the number of similar motors clustered. - * - * TODO: HIGH: This should not be used, since the components themselves can be clustered - * - * @return the number of motors. - */ - @Deprecated - public int getMotorCount(); - - - - /** - * Return the ejection charge delay of given motor configuration. - * A "plugged" motor without an ejection charge is given by - * {@link Motor#PLUGGED} (<code>Double.POSITIVE_INFINITY</code>). - * - * @param id the motor configuration ID - * @return the ejection charge delay. - */ - public double getMotorDelay(String id); - - /** - * Set the ejection change delay of the given motor configuration. - * The ejection charge is disable (a "plugged" motor) is set by - * {@link Motor#PLUGGED} (<code>Double.POSITIVE_INFINITY</code>). - * - * @param id the motor configuration ID - * @param delay the ejection charge delay. - */ - public void setMotorDelay(String id, double delay); - - - /** - * Return the event that ignites this motor. - * - * @return the {@link IgnitionEvent} that ignites this motor. - */ - public IgnitionEvent getIgnitionEvent(); - - /** - * Sets the event that ignites this motor. - * - * @param event the {@link IgnitionEvent} that ignites this motor. - */ - public void setIgnitionEvent(IgnitionEvent event); - - - /** - * Returns the ignition delay of this motor. - * - * @return the ignition delay - */ - public double getIgnitionDelay(); - - /** - * Sets the ignition delay of this motor. - * - * @param delay the ignition delay. - */ - public void setIgnitionDelay(double delay); - - - /** - * Return the distance that the motors hang outside this motor mount. - * - * @return the overhang length. - */ - public double getMotorOverhang(); - - /** - * Sets the distance that the motors hang outside this motor mount. - * - * @param overhang the overhang length. - */ - public void setMotorOverhang(double overhang); - - - - /** - * Return the inner diameter of the motor mount. - * - * @return the inner diameter of the motor mount. - */ - public double getMotorMountDiameter(); - - - /** - * Return the position of the motor relative to this component. The coordinate - * is that of the front cap of the motor. - * - * @return the position of the motor relative to this component. - * @throws IllegalArgumentException if a motor with the specified ID does not exist. - */ - public Coordinate getMotorPosition(String id); - -} diff --git a/src/net/sf/openrocket/rocketcomponent/NoseCone.java b/src/net/sf/openrocket/rocketcomponent/NoseCone.java deleted file mode 100644 index a4de3a9c..00000000 --- a/src/net/sf/openrocket/rocketcomponent/NoseCone.java +++ /dev/null @@ -1,123 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -/** - * Rocket nose cones of various types. Implemented as a transition with the - * fore radius == 0. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class NoseCone extends Transition { - private static final Translator trans = Application.getTranslator(); - - - /********* Constructors **********/ - public NoseCone() { - this(Transition.Shape.OGIVE, 6 * DEFAULT_RADIUS, DEFAULT_RADIUS); - } - - public NoseCone(Transition.Shape type, double length, double radius) { - super(); - super.setType(type); - super.setForeRadiusAutomatic(false); - super.setForeRadius(0); - super.setForeShoulderLength(0); - super.setForeShoulderRadius(0.9 * radius); - super.setForeShoulderThickness(0); - super.setForeShoulderCapped(filled); - super.setThickness(0.002); - super.setLength(length); - super.setClipped(false); - - } - - - /********** Get/set methods for component parameters **********/ - - @Override - public double getForeRadius() { - return 0; - } - - @Override - public void setForeRadius(double r) { - // No-op - } - - @Override - public boolean isForeRadiusAutomatic() { - return false; - } - - @Override - public void setForeRadiusAutomatic(boolean b) { - // No-op - } - - @Override - public double getForeShoulderLength() { - return 0; - } - - @Override - public double getForeShoulderRadius() { - return 0; - } - - @Override - public double getForeShoulderThickness() { - return 0; - } - - @Override - public boolean isForeShoulderCapped() { - return false; - } - - @Override - public void setForeShoulderCapped(boolean capped) { - // No-op - } - - @Override - public void setForeShoulderLength(double foreShoulderLength) { - // No-op - } - - @Override - public void setForeShoulderRadius(double foreShoulderRadius) { - // No-op - } - - @Override - public void setForeShoulderThickness(double foreShoulderThickness) { - // No-op - } - - @Override - public boolean isClipped() { - return false; - } - - @Override - public void setClipped(boolean b) { - // No-op - } - - - - /********** RocketComponent methods **********/ - - /** - * Return component name. - */ - @Override - public String getComponentName() { - //// Nose cone - return trans.get("NoseCone.NoseCone"); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Parachute.java b/src/net/sf/openrocket/rocketcomponent/Parachute.java deleted file mode 100644 index 640810bb..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Parachute.java +++ /dev/null @@ -1,122 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; - -public class Parachute extends RecoveryDevice { - private static final Translator trans = Application.getTranslator(); - - public static final double DEFAULT_CD = 0.8; - - private double diameter; - - private Material lineMaterial; - private int lineCount = 6; - private double lineLength = 0.3; - - - public Parachute() { - this.diameter = 0.3; - this.lineMaterial = Application.getPreferences().getDefaultComponentMaterial(Parachute.class, Material.Type.LINE); - this.lineLength = 0.3; - } - - - public double getDiameter() { - return diameter; - } - - public void setDiameter(double d) { - if (MathUtil.equals(this.diameter, d)) - return; - this.diameter = d; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - public final Material getLineMaterial() { - return lineMaterial; - } - - public final void setLineMaterial(Material mat) { - if (mat.getType() != Material.Type.LINE) { - throw new IllegalArgumentException("Attempted to set non-line material " + mat); - } - if (mat.equals(lineMaterial)) - return; - this.lineMaterial = mat; - if (getLineCount() != 0) - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - else - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - public final int getLineCount() { - return lineCount; - } - - public final void setLineCount(int n) { - if (this.lineCount == n) - return; - this.lineCount = n; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public final double getLineLength() { - return lineLength; - } - - public final void setLineLength(double length) { - if (MathUtil.equals(this.lineLength, length)) - return; - this.lineLength = length; - if (getLineCount() != 0) - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - else - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - @Override - public double getComponentCD(double mach) { - return DEFAULT_CD; // TODO: HIGH: Better parachute CD estimate? - } - - @Override - public double getArea() { - return Math.PI * MathUtil.pow2(diameter / 2); - } - - public void setArea(double area) { - if (MathUtil.equals(getArea(), area)) - return; - diameter = MathUtil.safeSqrt(area / Math.PI) * 2; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - @Override - public double getComponentMass() { - return super.getComponentMass() + - getLineCount() * getLineLength() * getLineMaterial().getDensity(); - } - - @Override - public String getComponentName() { - //// Parachute - return trans.get("Parachute.Parachute"); - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/RadialParent.java b/src/net/sf/openrocket/rocketcomponent/RadialParent.java deleted file mode 100644 index 41c731af..00000000 --- a/src/net/sf/openrocket/rocketcomponent/RadialParent.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -public interface RadialParent { - - /** - * Return the outer radius of the component at local coordinate <code>x</code>. - * Values for <code>x < 0</code> and <code>x > getLength()</code> are undefined. - * - * @param x the lengthwise position in the coordinates of this component. - * @return the outer radius of the component at that position. - */ - public double getOuterRadius(double x); - - /** - * Return the inner radius of the component at local coordinate <code>x</code>. - * Values for <code>x < 0</code> and <code>x > getLength()</code> are undefined. - * - * @param x the lengthwise position in the coordinates of this component. - * @return the inner radius of the component at that position. - */ - public double getInnerRadius(double x); - - - /** - * Return the length of this component. - * - * @return the length of this component. - */ - public double getLength(); - -} diff --git a/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java b/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java deleted file mode 100644 index 46a35143..00000000 --- a/src/net/sf/openrocket/rocketcomponent/RadiusRingComponent.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -/** - * An inner component that consists of a hollow cylindrical component. This can be - * an inner tube, tube coupler, centering ring, bulkhead etc. - * - * The properties include the inner and outer radii, length and radial position. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class RadiusRingComponent extends RingComponent implements Coaxial { - - protected double outerRadius = 0; - protected double innerRadius = 0; - - @Override - public double getOuterRadius() { - if (outerRadiusAutomatic && getParent() instanceof RadialParent) { - RocketComponent parent = getParent(); - double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x; - double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x; - pos1 = MathUtil.clamp(pos1, 0, parent.getLength()); - pos2 = MathUtil.clamp(pos2, 0, parent.getLength()); - outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1), - ((RadialParent)parent).getInnerRadius(pos2)); - } - - return outerRadius; - } - - @Override - public void setOuterRadius(double r) { - r = Math.max(r,0); - if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic()) - return; - - outerRadius = r; - outerRadiusAutomatic = false; - if (getInnerRadius() > r) { - innerRadius = r; - innerRadiusAutomatic = false; - } - - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - @Override - public double getInnerRadius() { - return innerRadius; - } - @Override - public void setInnerRadius(double r) { - r = Math.max(r,0); - if (MathUtil.equals(innerRadius, r)) - return; - - innerRadius = r; - innerRadiusAutomatic = false; - if (getOuterRadius() < r) { - outerRadius = r; - outerRadiusAutomatic = false; - } - - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - @Override - public double getThickness() { - return Math.max(getOuterRadius() - getInnerRadius(), 0); - } - @Override - public void setThickness(double thickness) { - double outer = getOuterRadius(); - - thickness = MathUtil.clamp(thickness, 0, outer); - setInnerRadius(outer - thickness); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java b/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java deleted file mode 100644 index 4156bfbe..00000000 --- a/src/net/sf/openrocket/rocketcomponent/RecoveryDevice.java +++ /dev/null @@ -1,219 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Pair; - - -/** - * RecoveryDevice is a class representing devices that slow down descent. - * Recovery devices report that they have no aerodynamic effect, since they - * are within the rocket during ascent. - * <p> - * A recovery device includes a surface material of which it is made of. - * The mass of the component is calculated based on the material and the - * area of the device from {@link #getArea()}. {@link #getComponentMass()} - * may be overridden if additional mass needs to be included. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class RecoveryDevice extends MassObject { - private static final Translator trans = Application.getTranslator(); - - public static enum DeployEvent { - //// Launch (plus NN seconds) - LAUNCH(trans.get("RecoveryDevice.DeployEvent.LAUNCH")) { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - return e.getType() == FlightEvent.Type.LAUNCH; - } - }, - //// First ejection charge of this stage - EJECTION(trans.get("RecoveryDevice.DeployEvent.EJECTION")) { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - if (e.getType() != FlightEvent.Type.EJECTION_CHARGE) - return false; - RocketComponent charge = e.getSource(); - return charge.getStageNumber() == source.getStageNumber(); - } - }, - //// Apogee - APOGEE(trans.get("RecoveryDevice.DeployEvent.APOGEE")) { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - return e.getType() == FlightEvent.Type.APOGEE; - } - }, - //// Specific altitude during descent - ALTITUDE(trans.get("RecoveryDevice.DeployEvent.ALTITUDE")) { - @SuppressWarnings("unchecked") - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - if (e.getType() != FlightEvent.Type.ALTITUDE) - return false; - - double alt = ((RecoveryDevice)source).getDeployAltitude(); - Pair<Double,Double> altitude = (Pair<Double,Double>)e.getData(); - - return (altitude.getU() >= alt) && (altitude.getV() <= alt); - } - }, - //// Never - NEVER(trans.get("RecoveryDevice.DeployEvent.NEVER")) { - @Override - public boolean isActivationEvent(FlightEvent e, RocketComponent source) { - return false; - } - } - ; - - private final String description; - - DeployEvent(String description) { - this.description = description; - } - - public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source); - - @Override - public String toString() { - return description; - } - - } - - - private DeployEvent deployEvent = DeployEvent.EJECTION; - private double deployAltitude = 200; - private double deployDelay = 0; - - private double cd = Parachute.DEFAULT_CD; - private boolean cdAutomatic = true; - - - private Material.Surface material; - - - public RecoveryDevice() { - this(Application.getPreferences().getDefaultComponentMaterial(RecoveryDevice.class, Material.Type.SURFACE)); - } - - public RecoveryDevice(Material material) { - super(); - setMaterial(material); - } - - public RecoveryDevice(double length, double radius, Material material) { - super(length, radius); - setMaterial(material); - } - - - - - public abstract double getArea(); - - public abstract double getComponentCD(double mach); - - - - public double getCD() { - return getCD(0); - } - - public double getCD(double mach) { - if (cdAutomatic) - cd = getComponentCD(mach); - return cd; - } - - public void setCD(double cd) { - if (MathUtil.equals(this.cd, cd) && !isCDAutomatic()) - return; - this.cd = cd; - this.cdAutomatic = false; - fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); - } - - - public boolean isCDAutomatic() { - return cdAutomatic; - } - - public void setCDAutomatic(boolean auto) { - if (cdAutomatic == auto) - return; - this.cdAutomatic = auto; - fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); - } - - - - public final Material getMaterial() { - return material; - } - - public final void setMaterial(Material mat) { - if (!(mat instanceof Material.Surface)) { - throw new IllegalArgumentException("Attempted to set non-surface material "+mat); - } - if (mat.equals(material)) - return; - this.material = (Material.Surface)mat; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - public DeployEvent getDeployEvent() { - return deployEvent; - } - - public void setDeployEvent(DeployEvent deployEvent) { - if (this.deployEvent == deployEvent) - return; - this.deployEvent = deployEvent; - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - } - - - public double getDeployAltitude() { - return deployAltitude; - } - - public void setDeployAltitude(double deployAltitude) { - if (MathUtil.equals(this.deployAltitude, deployAltitude)) - return; - this.deployAltitude = deployAltitude; - if (getDeployEvent() == DeployEvent.ALTITUDE) - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - else - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - public double getDeployDelay() { - return deployDelay; - } - - public void setDeployDelay(double delay) { - delay = MathUtil.max(delay, 0); - if (MathUtil.equals(this.deployDelay, delay)) - return; - this.deployDelay = delay; - fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE); - } - - - - @Override - public double getComponentMass() { - return getArea() * getMaterial().getDensity(); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ReferenceType.java b/src/net/sf/openrocket/rocketcomponent/ReferenceType.java deleted file mode 100644 index 02264ebe..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ReferenceType.java +++ /dev/null @@ -1,50 +0,0 @@ -/** - * - */ -package net.sf.openrocket.rocketcomponent; - -public enum ReferenceType { - - NOSECONE { - @Override - public double getReferenceLength(Configuration config) { - for (RocketComponent c: config) { - if (c instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent)c; - if (s.getForeRadius() >= 0.0005) - return s.getForeRadius() * 2; - if (s.getAftRadius() >= 0.0005) - return s.getAftRadius() * 2; - } - } - return Rocket.DEFAULT_REFERENCE_LENGTH; - } - }, - - MAXIMUM { - @Override - public double getReferenceLength(Configuration config) { - double r = 0; - for (RocketComponent c: config) { - if (c instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent)c; - r = Math.max(r, s.getForeRadius()); - r = Math.max(r, s.getAftRadius()); - } - } - r *= 2; - if (r < 0.001) - r = Rocket.DEFAULT_REFERENCE_LENGTH; - return r; - } - }, - - CUSTOM { - @Override - public double getReferenceLength(Configuration config) { - return config.getRocket().getCustomReferenceLength(); - } - }; - - public abstract double getReferenceLength(Configuration rocket); -} diff --git a/src/net/sf/openrocket/rocketcomponent/RingComponent.java b/src/net/sf/openrocket/rocketcomponent/RingComponent.java deleted file mode 100644 index 9e2c16bc..00000000 --- a/src/net/sf/openrocket/rocketcomponent/RingComponent.java +++ /dev/null @@ -1,221 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -/** - * An inner component that consists of a hollow cylindrical component. This can be - * an inner tube, tube coupler, centering ring, bulkhead etc. - * - * The properties include the inner and outer radii, length and radial position. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class RingComponent extends StructuralComponent implements Coaxial { - - protected boolean outerRadiusAutomatic = false; - protected boolean innerRadiusAutomatic = false; - - - private double radialDirection = 0; - private double radialPosition = 0; - - private double shiftY = 0; - private double shiftZ = 0; - - - - @Override - public abstract double getOuterRadius(); - - @Override - public abstract void setOuterRadius(double r); - - @Override - public abstract double getInnerRadius(); - - @Override - public abstract void setInnerRadius(double r); - - @Override - public abstract double getThickness(); - - public abstract void setThickness(double thickness); - - - public final boolean isOuterRadiusAutomatic() { - return outerRadiusAutomatic; - } - - // Setter is protected, subclasses may make it public - protected void setOuterRadiusAutomatic(boolean auto) { - if (auto == outerRadiusAutomatic) - return; - outerRadiusAutomatic = auto; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public final boolean isInnerRadiusAutomatic() { - return innerRadiusAutomatic; - } - - // Setter is protected, subclasses may make it public - protected void setInnerRadiusAutomatic(boolean auto) { - if (auto == innerRadiusAutomatic) - return; - innerRadiusAutomatic = auto; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - public final void setLength(double length) { - double l = Math.max(length, 0); - if (this.length == l) - return; - - this.length = l; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - /** - * Return the radial direction of displacement of the component. Direction 0 - * is equivalent to the Y-direction. - * - * @return the radial direction. - */ - public double getRadialDirection() { - return radialDirection; - } - - /** - * Set the radial direction of displacement of the component. Direction 0 - * is equivalent to the Y-direction. - * - * @param dir the radial direction. - */ - public void setRadialDirection(double dir) { - dir = MathUtil.reduce180(dir); - if (radialDirection == dir) - return; - radialDirection = dir; - shiftY = radialPosition * Math.cos(radialDirection); - shiftZ = radialPosition * Math.sin(radialDirection); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - /** - * Return the radial position of the component. The position is the distance - * of the center of the component from the center of the parent component. - * - * @return the radial position. - */ - public double getRadialPosition() { - return radialPosition; - } - - /** - * Set the radial position of the component. The position is the distance - * of the center of the component from the center of the parent component. - * - * @param pos the radial position. - */ - public void setRadialPosition(double pos) { - pos = Math.max(pos, 0); - if (radialPosition == pos) - return; - radialPosition = pos; - shiftY = radialPosition * Math.cos(radialDirection); - shiftZ = radialPosition * Math.sin(radialDirection); - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public double getRadialShiftY() { - return shiftY; - } - - public double getRadialShiftZ() { - return shiftZ; - } - - public void setRadialShift(double y, double z) { - radialPosition = Math.hypot(y, z); - radialDirection = Math.atan2(z, y); - - // Re-calculate to ensure consistency - shiftY = radialPosition * Math.cos(radialDirection); - shiftZ = radialPosition * Math.sin(radialDirection); - assert (MathUtil.equals(y, shiftY)); - assert (MathUtil.equals(z, shiftZ)); - - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - /** - * Return the number of times the component is multiplied. - */ - public int getClusterCount() { - if (this instanceof Clusterable) - return ((Clusterable) this).getClusterConfiguration().getClusterCount(); - return 1; - } - - - /** - * Shift the coordinates according to the radial position and direction. - */ - @Override - public Coordinate[] shiftCoordinates(Coordinate[] array) { - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(0, shiftY, shiftZ); - } - return array; - } - - - @Override - public Collection<Coordinate> getComponentBounds() { - List<Coordinate> bounds = new ArrayList<Coordinate>(); - addBound(bounds, 0, getOuterRadius()); - addBound(bounds, length, getOuterRadius()); - return bounds; - } - - - - @Override - public Coordinate getComponentCG() { - return new Coordinate(length / 2, 0, 0, getComponentMass()); - } - - @Override - public double getComponentMass() { - return ringMass(getOuterRadius(), getInnerRadius(), getLength(), - getMaterial().getDensity()) * getClusterCount(); - } - - - @Override - public double getLongitudinalUnitInertia() { - return ringLongitudinalUnitInertia(getOuterRadius(), getInnerRadius(), getLength()); - } - - @Override - public double getRotationalUnitInertia() { - return ringRotationalUnitInertia(getOuterRadius(), getInnerRadius()); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Rocket.java b/src/net/sf/openrocket/rocketcomponent/Rocket.java deleted file mode 100644 index 226f9527..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Rocket.java +++ /dev/null @@ -1,837 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.Collection; -import java.util.Collections; -import java.util.EventListener; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; -import java.util.UUID; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.Chars; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.StateChangeListener; -import net.sf.openrocket.util.UniqueID; - - -/** - * Base for all rocket components. This is the "starting point" for all rocket trees. - * It provides the actual implementations of several methods defined in RocketComponent - * (eg. the rocket listener lists) and the methods defined in RocketComponent call these. - * It also defines some other methods that concern the whole rocket, and helper methods - * that keep information about the program state. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class Rocket extends RocketComponent { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - public static final double DEFAULT_REFERENCE_LENGTH = 0.01; - - - /** - * List of component change listeners. - */ - private List<EventListener> listenerList = new ArrayList<EventListener>(); - - /** - * When freezeList != null, events are not dispatched but stored in the list. - * When the structure is thawed, a single combined event will be fired. - */ - private List<ComponentChangeEvent> freezeList = null; - - - private int modID; - private int massModID; - private int aeroModID; - private int treeModID; - private int functionalModID; - - - private ReferenceType refType = ReferenceType.MAXIMUM; // Set in constructor - private double customReferenceLength = DEFAULT_REFERENCE_LENGTH; - - - // The default configuration used in dialogs - private final Configuration defaultConfiguration; - - - private String designer = ""; - private String revision = ""; - - - // Motor configuration list - private ArrayList<String> motorConfigurationIDs = new ArrayList<String>(); - private HashMap<String, String> motorConfigurationNames = new HashMap<String, String>(); - { - motorConfigurationIDs.add(null); - } - - - // Does the rocket have a perfect finish (a notable amount of laminar flow) - private boolean perfectFinish = false; - - - - ///////////// Constructor ///////////// - - public Rocket() { - super(RocketComponent.Position.AFTER); - modID = UniqueID.next(); - massModID = modID; - aeroModID = modID; - treeModID = modID; - functionalModID = modID; - defaultConfiguration = new Configuration(this); - } - - - - public String getDesigner() { - checkState(); - return designer; - } - - public void setDesigner(String s) { - if (s == null) - s = ""; - designer = s; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - public String getRevision() { - checkState(); - return revision; - } - - public void setRevision(String s) { - if (s == null) - s = ""; - revision = s; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - - - /** - * Return the number of stages in this rocket. - * - * @return the number of stages in this rocket. - */ - public int getStageCount() { - checkState(); - return this.getChildCount(); - } - - - /** - * Return the non-negative modification ID of this rocket. The ID is changed - * every time any change occurs in the rocket. This can be used to check - * whether it is necessary to void cached data in cases where listeners can not - * or should not be used. - * <p> - * Three other modification IDs are also available, {@link #getMassModID()}, - * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time - * a mass change, aerodynamic change, or tree change occur. Even though the values - * of the different modification ID's may be equal, they should be treated totally - * separate. - * <p> - * Note that undo events restore the modification IDs that were in use at the - * corresponding undo level. Subsequent modifications, however, produce modIDs - * distinct from those already used. - * - * @return a unique ID number for this modification state. - */ - public int getModID() { - return modID; - } - - /** - * Return the non-negative mass modification ID of this rocket. See - * {@link #getModID()} for details. - * - * @return a unique ID number for this mass-modification state. - */ - public int getMassModID() { - return massModID; - } - - /** - * Return the non-negative aerodynamic modification ID of this rocket. See - * {@link #getModID()} for details. - * - * @return a unique ID number for this aerodynamic-modification state. - */ - public int getAerodynamicModID() { - return aeroModID; - } - - /** - * Return the non-negative tree modification ID of this rocket. See - * {@link #getModID()} for details. - * - * @return a unique ID number for this tree-modification state. - */ - public int getTreeModID() { - return treeModID; - } - - /** - * Return the non-negative functional modificationID of this rocket. - * This changes every time a functional change occurs. - * - * @return a unique ID number for this functional modification state. - */ - public int getFunctionalModID() { - return functionalModID; - } - - - - - public ReferenceType getReferenceType() { - checkState(); - return refType; - } - - public void setReferenceType(ReferenceType type) { - if (refType == type) - return; - refType = type; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - public double getCustomReferenceLength() { - checkState(); - return customReferenceLength; - } - - public void setCustomReferenceLength(double length) { - if (MathUtil.equals(customReferenceLength, length)) - return; - - this.customReferenceLength = Math.max(length, 0.001); - - if (refType == ReferenceType.CUSTOM) { - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - } - - - - - - /** - * Set whether the rocket has a perfect finish. This will affect whether the - * boundary layer is assumed to be fully turbulent or not. - * - * @param perfectFinish whether the finish is perfect. - */ - public void setPerfectFinish(boolean perfectFinish) { - if (this.perfectFinish == perfectFinish) - return; - this.perfectFinish = perfectFinish; - fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE); - } - - - - /** - * Get whether the rocket has a perfect finish. - * - * @return the perfectFinish - */ - public boolean isPerfectFinish() { - return perfectFinish; - } - - - - - - /** - * Make a deep copy of the Rocket structure. This method is exposed as public to allow - * for undo/redo system functionality. - */ - @SuppressWarnings("unchecked") - @Override - public Rocket copyWithOriginalID() { - Rocket copy = (Rocket) super.copyWithOriginalID(); - copy.motorConfigurationIDs = this.motorConfigurationIDs.clone(); - copy.motorConfigurationNames = - (HashMap<String, String>) this.motorConfigurationNames.clone(); - copy.resetListeners(); - - return copy; - } - - /** - * Load the rocket structure from the source. The method loads the fields of this - * Rocket object and copies the references to siblings from the <code>source</code>. - * The object <code>source</code> should not be used after this call, as it is in - * an illegal state! - * <p> - * This method is meant to be used in conjunction with undo/redo functionality, - * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree - * changes. - */ - @SuppressWarnings("unchecked") - public void loadFrom(Rocket r) { - - // Store list of components to invalidate after event has been fired - List<RocketComponent> toInvalidate = this.copyFrom(r); - - int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE; - if (this.massModID != r.massModID) - type |= ComponentChangeEvent.MASS_CHANGE; - if (this.aeroModID != r.aeroModID) - type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; - // Loading a rocket is always a tree change since the component objects change - type |= ComponentChangeEvent.TREE_CHANGE; - - this.modID = r.modID; - this.massModID = r.massModID; - this.aeroModID = r.aeroModID; - this.treeModID = r.treeModID; - this.functionalModID = r.functionalModID; - this.refType = r.refType; - this.customReferenceLength = r.customReferenceLength; - - this.motorConfigurationIDs = r.motorConfigurationIDs.clone(); - this.motorConfigurationNames = - (HashMap<String, String>) r.motorConfigurationNames.clone(); - this.perfectFinish = r.perfectFinish; - - String id = defaultConfiguration.getMotorConfigurationID(); - if (!this.motorConfigurationIDs.contains(id)) - defaultConfiguration.setMotorConfigurationID(null); - - this.checkComponentStructure(); - - fireComponentChangeEvent(type); - - // Invalidate obsolete components after event - for (RocketComponent c : toInvalidate) { - c.invalidate(); - } - } - - - - - /////// Implement the ComponentChangeListener lists - - /** - * Creates a new EventListenerList for this component. This is necessary when cloning - * the structure. - */ - public void resetListeners() { - // System.out.println("RESETTING LISTENER LIST of Rocket "+this); - listenerList = new ArrayList<EventListener>(); - } - - - public void printListeners() { - System.out.println("" + this + " has " + listenerList.size() + " listeners:"); - int i =0; - for( EventListener l : listenerList ) { - System.out.println(" " + (i) + ": " + l); - i++; - } - } - - @Override - public void addComponentChangeListener(ComponentChangeListener l) { - checkState(); - listenerList.add(l); - log.verbose("Added ComponentChangeListener " + l + ", current number of listeners is " + - listenerList.size()); - } - - @Override - public void removeComponentChangeListener(ComponentChangeListener l) { - listenerList.remove(l); - log.verbose("Removed ComponentChangeListener " + l + ", current number of listeners is " + - listenerList.size()); - } - - - @Override - public void addChangeListener(EventListener l) { - checkState(); - listenerList.add(l); - log.verbose("Added ChangeListener " + l + ", current number of listeners is " + - listenerList.size()); - } - - @Override - public void removeChangeListener(EventListener l) { - listenerList.remove(l); - log.verbose("Removed ChangeListener " + l + ", current number of listeners is " + - listenerList.size()); - } - - - @Override - protected void fireComponentChangeEvent(ComponentChangeEvent e) { - mutex.lock("fireComponentChangeEvent"); - try { - checkState(); - - // Update modification ID's only for normal (not undo/redo) events - if (!e.isUndoChange()) { - modID = UniqueID.next(); - if (e.isMassChange()) - massModID = modID; - if (e.isAerodynamicChange()) - aeroModID = modID; - if (e.isTreeChange()) - treeModID = modID; - if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE) - functionalModID = modID; - } - - // Check whether frozen - if (freezeList != null) { - log.debug("Rocket is in frozen state, adding event " + e + " info freeze list"); - freezeList.add(e); - return; - } - - log.debug("Firing rocket change event " + e); - - // Notify all components first - Iterator<RocketComponent> iterator = this.iterator(true); - while (iterator.hasNext()) { - iterator.next().componentChanged(e); - } - - // Notify all listeners - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] list = listenerList.toArray( new EventListener[0] ); - for ( EventListener l : list ) { - if ( l instanceof ComponentChangeListener ) { - ((ComponentChangeListener) l ).componentChanged(e); - } else if ( l instanceof StateChangeListener ) { - ((StateChangeListener) l ).stateChanged(e); - } - } - } finally { - mutex.unlock("fireComponentChangeEvent"); - } - } - - - /** - * Freezes the rocket structure from firing any events. This may be performed to - * combine several actions on the structure into a single large action. - * <code>thaw()</code> must always be called afterwards. - * - * NOTE: Always use a try/finally to ensure <code>thaw()</code> is called: - * <pre> - * Rocket r = c.getRocket(); - * try { - * r.freeze(); - * // do stuff - * } finally { - * r.thaw(); - * } - * </pre> - * - * @see #thaw() - */ - public void freeze() { - checkState(); - if (freezeList == null) { - freezeList = new LinkedList<ComponentChangeEvent>(); - log.debug("Freezing Rocket"); - } else { - Application.getExceptionHandler().handleErrorCondition("Attempting to freeze Rocket when it is already frozen, " + - "freezeList=" + freezeList); - } - } - - /** - * Thaws a frozen rocket structure and fires a combination of the events fired during - * the freeze. The event type is a combination of those fired and the source is the - * last component to have been an event source. - * - * @see #freeze() - */ - public void thaw() { - checkState(); - if (freezeList == null) { - Application.getExceptionHandler().handleErrorCondition("Attempting to thaw Rocket when it is not frozen"); - return; - } - if (freezeList.size() == 0) { - log.warn("Thawing rocket with no changes made"); - freezeList = null; - return; - } - - log.debug("Thawing rocket, freezeList=" + freezeList); - - int type = 0; - Object c = null; - for (ComponentChangeEvent e : freezeList) { - type = type | e.getType(); - c = e.getSource(); - } - freezeList = null; - - fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent) c, type)); - } - - - - - //////// Motor configurations //////// - - - /** - * Return the default configuration. This should be used in the user interface - * to ensure a consistent rocket configuration between dialogs. It should NOT - * be used in simulations not relating to the UI. - * - * @return the default {@link Configuration}. - */ - public Configuration getDefaultConfiguration() { - checkState(); - return defaultConfiguration; - } - - - /** - * Return an array of the motor configuration IDs. This array is guaranteed - * to contain the <code>null</code> ID as the first element. - * - * @return an array of the motor configuration IDs. - */ - public String[] getMotorConfigurationIDs() { - checkState(); - return motorConfigurationIDs.toArray(new String[0]); - } - - /** - * Add a new motor configuration ID to the motor configurations. The new ID - * is returned. - * - * @return the new motor configuration ID. - */ - public String newMotorConfigurationID() { - checkState(); - String id = UUID.randomUUID().toString(); - motorConfigurationIDs.add(id); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - return id; - } - - /** - * Add a specified motor configuration ID to the motor configurations. - * - * @param id the motor configuration ID. - * @return true if successful, false if the ID was already used. - */ - public boolean addMotorConfigurationID(String id) { - checkState(); - if (id == null || motorConfigurationIDs.contains(id)) - return false; - motorConfigurationIDs.add(id); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - return true; - } - - /** - * Remove a motor configuration ID from the configuration IDs. The <code>null</code> - * ID cannot be removed, and an attempt to remove it will be silently ignored. - * - * @param id the motor configuration ID to remove - */ - public void removeMotorConfigurationID(String id) { - checkState(); - if (id == null) - return; - motorConfigurationIDs.remove(id); - fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE); - } - - - /** - * Check whether <code>id</code> is a valid motor configuration ID. - * - * @param id the configuration ID. - * @return whether a motor configuration with that ID exists. - */ - public boolean isMotorConfigurationID(String id) { - checkState(); - return motorConfigurationIDs.contains(id); - } - - - - /** - * Check whether the given motor configuration ID has motors defined for it. - * - * @param id the motor configuration ID (may be invalid). - * @return whether any motors are defined for it. - */ - public boolean hasMotors(String id) { - checkState(); - if (id == null) - return false; - - Iterator<RocketComponent> iterator = this.iterator(); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - - if (c instanceof MotorMount) { - MotorMount mount = (MotorMount) c; - if (!mount.isMotorMount()) - continue; - if (mount.getMotor(id) != null) { - return true; - } - } - } - return false; - } - - - /** - * Return the user-set name of the motor configuration. If no name has been set, - * returns an empty string (not null). - * - * @param id the motor configuration id - * @return the configuration name - */ - public String getMotorConfigurationName(String id) { - checkState(); - if (!isMotorConfigurationID(id)) - return ""; - String s = motorConfigurationNames.get(id); - if (s == null) - return ""; - return s; - } - - - /** - * Set the name of the motor configuration. A name can be unset by passing - * <code>null</code> or an empty string. - * - * @param id the motor configuration id - * @param name the name for the motor configuration - */ - public void setMotorConfigurationName(String id, String name) { - checkState(); - motorConfigurationNames.put(id, name); - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - /** - * Return either the motor configuration name (if set) or its description. - * - * @param id the motor configuration ID. - * @return a textual representation of the configuration - */ - public String getMotorConfigurationNameOrDescription(String id) { - checkState(); - String name; - - name = getMotorConfigurationName(id); - if (name != null && !name.equals("")) - return name; - - return getMotorConfigurationDescription(id); - } - - /** - * Return a description for the motor configuration, generated from the motor - * designations of the components. - * - * @param id the motor configuration ID. - * @return a textual representation of the configuration - */ - @SuppressWarnings("null") - public String getMotorConfigurationDescription(String id) { - checkState(); - String name; - int motorCount = 0; - - // Generate the description - - // First iterate over each stage and store the designations of each motor - List<List<String>> list = new ArrayList<List<String>>(); - List<String> currentList = null; - - Iterator<RocketComponent> iterator = this.iterator(); - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - - if (c instanceof Stage) { - - currentList = new ArrayList<String>(); - list.add(currentList); - - } else if (c instanceof MotorMount) { - - MotorMount mount = (MotorMount) c; - Motor motor = mount.getMotor(id); - - if (mount.isMotorMount() && motor != null) { - String designation = motor.getDesignation(mount.getMotorDelay(id)); - - for (int i = 0; i < mount.getMotorCount(); i++) { - currentList.add(designation); - motorCount++; - } - } - - } - } - - if (motorCount == 0) { - //// [No motors] - return trans.get("Rocket.motorCount.Nomotor"); - } - - // Change multiple occurrences of a motor to n x motor - List<String> stages = new ArrayList<String>(); - - for (List<String> stage : list) { - String stageName = ""; - String previous = null; - int count = 0; - - Collections.sort(stage); - for (String current : stage) { - if (current.equals(previous)) { - - count++; - - } else { - - if (previous != null) { - String s = ""; - if (count > 1) { - s = "" + count + Chars.TIMES + previous; - } else { - s = previous; - } - - if (stageName.equals("")) - stageName = s; - else - stageName = stageName + "," + s; - } - - previous = current; - count = 1; - - } - } - if (previous != null) { - String s = ""; - if (count > 1) { - s = "" + count + Chars.TIMES + previous; - } else { - s = previous; - } - - if (stageName.equals("")) - stageName = s; - else - stageName = stageName + "," + s; - } - - stages.add(stageName); - } - - name = "["; - for (int i = 0; i < stages.size(); i++) { - String s = stages.get(i); - if (s.equals("")) - s = "None"; - if (i == 0) - name = name + s; - else - name = name + "; " + s; - } - name += "]"; - return name; - } - - - - //////// Obligatory component information - - - @Override - public String getComponentName() { - //// Rocket - return trans.get("Rocket.compname.Rocket"); - } - - @Override - public Coordinate getComponentCG() { - return new Coordinate(0, 0, 0, 0); - } - - @Override - public double getComponentMass() { - return 0; - } - - @Override - public double getLongitudinalUnitInertia() { - return 0; - } - - @Override - public double getRotationalUnitInertia() { - return 0; - } - - @Override - public Collection<Coordinate> getComponentBounds() { - return Collections.emptyList(); - } - - @Override - public boolean isAerodynamic() { - return false; - } - - @Override - public boolean isMassive() { - return false; - } - - @Override - public boolean allowsChildren() { - return true; - } - - /** - * Allows only <code>Stage</code> components to be added to the type Rocket. - */ - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return (Stage.class.isAssignableFrom(type)); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/RocketComponent.java b/src/net/sf/openrocket/rocketcomponent/RocketComponent.java deleted file mode 100644 index a5c8c66a..00000000 --- a/src/net/sf/openrocket/rocketcomponent/RocketComponent.java +++ /dev/null @@ -1,1893 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.ArrayDeque; -import java.util.Collection; -import java.util.Deque; -import java.util.EventListener; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.preset.ComponentPreset; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.Color; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Invalidator; -import net.sf.openrocket.util.LineStyle; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.SafetyMutex; -import net.sf.openrocket.util.UniqueID; - - -public abstract class RocketComponent implements ChangeSource, Cloneable, Iterable<RocketComponent> { - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - /* - * Text is suitable to the form - * Position relative to: <title> - */ - public enum Position { - /** Position relative to the top of the parent component. */ - //// Top of the parent component - TOP(trans.get("RocketComponent.Position.TOP")), - /** Position relative to the middle of the parent component. */ - //// Middle of the parent component - MIDDLE(trans.get("RocketComponent.Position.MIDDLE")), - /** Position relative to the bottom of the parent component. */ - //// Bottom of the parent component - BOTTOM(trans.get("RocketComponent.Position.BOTTOM")), - /** Position after the parent component (for body components). */ - //// After the parent component - AFTER(trans.get("RocketComponent.Position.AFTER")), - /** Specify an absolute X-coordinate position. */ - //// Tip of the nose cone - ABSOLUTE(trans.get("RocketComponent.Position.ABSOLUTE")); - - private String title; - - Position(String title) { - this.title = title; - } - - @Override - public String toString() { - return title; - } - } - - /** - * A safety mutex that can be used to prevent concurrent access to this component. - */ - protected SafetyMutex mutex = SafetyMutex.newInstance(); - - //////// Parent/child trees - /** - * Parent component of the current component, or null if none exists. - */ - private RocketComponent parent = null; - - /** - * List of child components of this component. - */ - private ArrayList<RocketComponent> children = new ArrayList<RocketComponent>(); - - - //////// Parameters common to all components: - - /** - * Characteristic length of the component. This is used in calculating the coordinate - * transformations and positions of other components in reference to this component. - * This may and should be used as the "true" length of the component, where applicable. - * By default it is zero, i.e. no translation. - */ - protected double length = 0; - - /** - * Positioning of this component relative to the parent component. - */ - protected Position relativePosition; - - /** - * Offset of the position of this component relative to the normal position given by - * relativePosition. By default zero, i.e. no position change. - */ - protected double position = 0; - - - // Color of the component, null means to use the default color - private Color color = null; - private LineStyle lineStyle = null; - - - // Override mass/CG - private double overrideMass = 0; - private boolean massOverriden = false; - private double overrideCGX = 0; - private boolean cgOverriden = false; - - private boolean overrideSubcomponents = false; - - - // User-given name of the component - private String name = null; - - // User-specified comment - private String comment = ""; - - // Unique ID of the component - private String id = null; - - // Preset component this component is based upon - private ComponentPreset presetComponent = null; - - - /** - * Used to invalidate the component after calling {@link #copyFrom(RocketComponent)}. - */ - private Invalidator invalidator = new Invalidator(this); - - - //// NOTE !!! All fields must be copied in the method copyFrom()! //// - - - - /** - * Default constructor. Sets the name of the component to the component's static name - * and the relative position of the component. - */ - public RocketComponent(Position relativePosition) { - // These must not fire any events, due to Rocket undo system initialization - this.name = getComponentName(); - this.relativePosition = relativePosition; - newID(); - } - - //////////// Methods that must be implemented //////////// - - - /** - * Static component name. The name may not vary of the parameters, it must be static. - */ - public abstract String getComponentName(); // Static component type name - - /** - * Return the component mass (regardless of mass overriding). - */ - public abstract double getComponentMass(); // Mass of non-overridden component - - /** - * Return the component CG and mass (regardless of CG or mass overriding). - */ - public abstract Coordinate getComponentCG(); // CG of non-overridden component - - - /** - * Return the longitudinal (around the y- or z-axis) unitary moment of inertia. - * The unitary moment of inertia is the moment of inertia with the assumption that - * the mass of the component is one kilogram. The inertia is measured in - * respect to the non-overridden CG. - * - * @return the longitudinal unitary moment of inertia of this component. - */ - public abstract double getLongitudinalUnitInertia(); - - - /** - * Return the rotational (around the x-axis) unitary moment of inertia. - * The unitary moment of inertia is the moment of inertia with the assumption that - * the mass of the component is one kilogram. The inertia is measured in - * respect to the non-overridden CG. - * - * @return the rotational unitary moment of inertia of this component. - */ - public abstract double getRotationalUnitInertia(); - - - /** - * Test whether this component allows any children components. This method must - * return true if and only if {@link #isCompatible(Class)} returns true for any - * rocket component class. - * - * @return <code>true</code> if children can be attached to this component, <code>false</code> otherwise. - */ - public abstract boolean allowsChildren(); - - /** - * Test whether the given component type can be added to this component. This type safety - * is enforced by the <code>addChild()</code> methods. The return value of this method - * may change to reflect the current state of this component (e.g. two components of some - * type cannot be placed as children). - * - * @param type The RocketComponent class type to add. - * @return Whether such a component can be added. - */ - public abstract boolean isCompatible(Class<? extends RocketComponent> type); - - - /* Non-abstract helper method */ - /** - * Test whether the given component can be added to this component. This is equivalent - * to calling <code>isCompatible(c.getClass())</code>. - * - * @param c Component to test. - * @return Whether the component can be added. - * @see #isCompatible(Class) - */ - public final boolean isCompatible(RocketComponent c) { - mutex.verify(); - return isCompatible(c.getClass()); - } - - - - /** - * Return a collection of bounding coordinates. The coordinates must be such that - * the component is fully enclosed in their convex hull. - * - * @return a collection of coordinates that bound the component. - */ - public abstract Collection<Coordinate> getComponentBounds(); - - /** - * Return true if the component may have an aerodynamic effect on the rocket. - */ - public abstract boolean isAerodynamic(); - - /** - * Return true if the component may have an effect on the rocket's mass. - */ - public abstract boolean isMassive(); - - - - - - //////////// Methods that may be overridden //////////// - - - /** - * Shift the coordinates in the array corresponding to radial movement. A component - * that has a radial position must shift the coordinates in this array suitably. - * If the component is clustered, then a new array must be returned with a - * coordinate for each cluster. - * <p> - * The default implementation simply returns the array, and thus produces no shift. - * - * @param c an array of coordinates to shift. - * @return an array of shifted coordinates. The method may modify the contents - * of the passed array and return the array itself. - */ - public Coordinate[] shiftCoordinates(Coordinate[] c) { - checkState(); - return c; - } - - - /** - * Called when any component in the tree fires a ComponentChangeEvent. This is by - * default a no-op, but subclasses may override this method to e.g. invalidate - * cached data. The overriding method *must* call - * <code>super.componentChanged(e)</code> at some point. - * - * @param e The event fired - */ - protected void componentChanged(ComponentChangeEvent e) { - // No-op - checkState(); - } - - - - - /** - * Return the user-provided name of the component, or the component base - * name if the user-provided name is empty. This can be used in the UI. - * - * @return A string describing the component. - */ - @Override - public final String toString() { - mutex.verify(); - if (name.length() == 0) - return getComponentName(); - else - return name; - } - - - /** - * Create a string describing the basic component structure from this component downwards. - * @return a string containing the rocket structure - */ - public final String toDebugString() { - mutex.lock("toDebugString"); - try { - StringBuilder sb = new StringBuilder(); - toDebugString(sb); - return sb.toString(); - } finally { - mutex.unlock("toDebugString"); - } - } - - private void toDebugString(StringBuilder sb) { - sb.append(this.getClass().getSimpleName()).append('@').append(System.identityHashCode(this)); - sb.append("[\"").append(this.getName()).append('"'); - for (RocketComponent c : this.children) { - sb.append("; "); - c.toDebugString(sb); - } - sb.append(']'); - } - - - /** - * Make a deep copy of the rocket component tree structure from this component - * downwards for copying purposes. Each component in the copy will be assigned - * a new component ID, making it a safe copy. This method does not fire any events. - * - * @return A deep copy of the structure. - */ - public final RocketComponent copy() { - RocketComponent clone = copyWithOriginalID(); - - Iterator<RocketComponent> iterator = clone.iterator(true); - while (iterator.hasNext()) { - iterator.next().newID(); - } - return clone; - } - - - - /** - * Make a deep copy of the rocket component tree structure from this component - * downwards while maintaining the component ID's. The purpose of this method is - * to allow copies to be created with the original ID's for the purpose of the - * undo/redo mechanism. This method should not be used for other purposes, - * such as copy/paste. This method does not fire any events. - * <p> - * This method must be overridden by any component that refers to mutable objects, - * or if some fields should not be copied. This should be performed by - * <code>RocketComponent c = super.copyWithOriginalID();</code> and then cloning/modifying - * the appropriate fields. - * <p> - * This is not performed as serializing/deserializing for performance reasons. - * - * @return A deep copy of the structure. - */ - protected RocketComponent copyWithOriginalID() { - mutex.lock("copyWithOriginalID"); - try { - checkState(); - RocketComponent clone; - try { - clone = (RocketComponent) this.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException encountered, report a bug!", e); - } - - // Reset the mutex - clone.mutex = SafetyMutex.newInstance(); - - // Reset all parent/child information - clone.parent = null; - clone.children = new ArrayList<RocketComponent>(); - - // Add copied children to the structure without firing events. - for (RocketComponent child : this.children) { - RocketComponent childCopy = child.copyWithOriginalID(); - // Don't use add method since it fires events - clone.children.add(childCopy); - childCopy.parent = clone; - } - - this.checkComponentStructure(); - clone.checkComponentStructure(); - - return clone; - } finally { - mutex.unlock("copyWithOriginalID"); - } - } - - - ////////////// Methods that may not be overridden //////////// - - - - ////////// Common parameter setting/getting ////////// - - /** - * Return the color of the object to use in 2D figures, or <code>null</code> - * to use the default color. - */ - public final Color getColor() { - mutex.verify(); - return color; - } - - /** - * Set the color of the object to use in 2D figures. - */ - public final void setColor(Color c) { - if ((color == null && c == null) || - (color != null && color.equals(c))) - return; - - checkState(); - this.color = c; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - public final LineStyle getLineStyle() { - mutex.verify(); - return lineStyle; - } - - public final void setLineStyle(LineStyle style) { - if (this.lineStyle == style) - return; - checkState(); - this.lineStyle = style; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - - - /** - * Get the current override mass. The mass is not necessarily in use - * at the moment. - * - * @return the override mass - */ - public final double getOverrideMass() { - mutex.verify(); - return overrideMass; - } - - /** - * Set the current override mass. The mass is not set to use by this - * method. - * - * @param m the override mass - */ - public final void setOverrideMass(double m) { - if (MathUtil.equals(m, overrideMass)) - return; - checkState(); - overrideMass = Math.max(m, 0); - if (massOverriden) - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - /** - * Return whether mass override is active for this component. This does NOT - * take into account whether a parent component is overriding the mass. - * - * @return whether the mass is overridden - */ - public final boolean isMassOverridden() { - mutex.verify(); - return massOverriden; - } - - /** - * Set whether the mass is currently overridden. - * - * @param o whether the mass is overridden - */ - public final void setMassOverridden(boolean o) { - if (massOverriden == o) { - return; - } - checkState(); - massOverriden = o; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - - /** - * Return the current override CG. The CG is not necessarily overridden. - * - * @return the override CG - */ - public final Coordinate getOverrideCG() { - mutex.verify(); - return getComponentCG().setX(overrideCGX); - } - - /** - * Return the x-coordinate of the current override CG. - * - * @return the x-coordinate of the override CG. - */ - public final double getOverrideCGX() { - mutex.verify(); - return overrideCGX; - } - - /** - * Set the current override CG to (x,0,0). - * - * @param x the x-coordinate of the override CG to set. - */ - public final void setOverrideCGX(double x) { - if (MathUtil.equals(overrideCGX, x)) - return; - checkState(); - this.overrideCGX = x; - if (isCGOverridden()) - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - else - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - /** - * Return whether the CG is currently overridden. - * - * @return whether the CG is overridden - */ - public final boolean isCGOverridden() { - mutex.verify(); - return cgOverriden; - } - - /** - * Set whether the CG is currently overridden. - * - * @param o whether the CG is overridden - */ - public final void setCGOverridden(boolean o) { - if (cgOverriden == o) { - return; - } - checkState(); - cgOverriden = o; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - /** - * Return whether the mass and/or CG override overrides all subcomponent values - * as well. The default implementation is a normal getter/setter implementation, - * however, subclasses are allowed to override this behavior if some subclass - * always or never overrides subcomponents. In this case the subclass should - * also override {@link #isOverrideSubcomponentsEnabled()} to return - * <code>false</code>. - * - * @return whether the current mass and/or CG override overrides subcomponents as well. - */ - public boolean getOverrideSubcomponents() { - mutex.verify(); - return overrideSubcomponents; - } - - - /** - * Set whether the mass and/or CG override overrides all subcomponent values - * as well. See {@link #getOverrideSubcomponents()} for details. - * - * @param override whether the mass and/or CG override overrides all subcomponent. - */ - public void setOverrideSubcomponents(boolean override) { - if (overrideSubcomponents == override) { - return; - } - checkState(); - overrideSubcomponents = override; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - /** - * Return whether the option to override all subcomponents is enabled or not. - * The default implementation returns <code>false</code> if neither mass nor - * CG is overridden, <code>true</code> otherwise. - * <p> - * This method may be overridden if the setting of overriding subcomponents - * cannot be set. - * - * @return whether the option to override subcomponents is currently enabled. - */ - public boolean isOverrideSubcomponentsEnabled() { - mutex.verify(); - return isCGOverridden() || isMassOverridden(); - } - - - - - /** - * Get the user-defined name of the component. - */ - public final String getName() { - mutex.verify(); - return name; - } - - /** - * Set the user-defined name of the component. If name==null, sets the name to - * the default name, currently the component name. - */ - public final void setName(String name) { - if (this.name.equals(name)) { - return; - } - checkState(); - if (name == null || name.matches("^\\s*$")) - this.name = getComponentName(); - else - this.name = name; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - /** - * Return the comment of the component. The component may contain multiple lines - * using \n as a newline separator. - * - * @return the comment of the component. - */ - public final String getComment() { - mutex.verify(); - return comment; - } - - /** - * Set the comment of the component. - * - * @param comment the comment of the component. - */ - public final void setComment(String comment) { - if (this.comment.equals(comment)) - return; - checkState(); - if (comment == null) - this.comment = ""; - else - this.comment = comment; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - - /** - * Return the preset component that this component is based upon. - * - * @return the preset component, or <code>null</code> if this is not based on a preset. - */ - public final ComponentPreset getPresetComponent() { - return presetComponent; - } - - /** - * Set the preset component this component is based upon and load all of the - * preset values. - * - * @param preset the preset component to load, or <code>null</code> to clear the preset. - */ - public final void loadPreset(ComponentPreset preset) { - if (presetComponent == preset) { - return; - } - - if (preset == null) { - clearPreset(); - return; - } - - if (preset.getComponentClass() != this.getClass()) { - throw new IllegalArgumentException("Attempting to load preset of type " + preset.getComponentClass() - + " into component of type " + this.getClass()); - } - - RocketComponent root = getRoot(); - final Rocket rocket; - if (root instanceof Rocket) { - rocket = (Rocket) root; - } else { - rocket = null; - } - - try { - if (rocket != null) { - rocket.freeze(); - } - - loadFromPreset(preset.getPrototype()); - - this.presetComponent = preset; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - - } finally { - if (rocket != null) { - rocket.thaw(); - } - } - } - - - /** - * Load component properties from the specified preset. The preset is guaranteed - * to be of the correct type. - * <p> - * This method should fire the appropriate events related to the changes. The rocket - * is frozen by the caller, so the events will be automatically combined. - * <p> - * This method must FIRST perform the preset loading and THEN call super.loadFromPreset(). - * This is because mass setting requires the dimensions to be set beforehand. - * - * @param preset the preset to load from - */ - protected void loadFromPreset(RocketComponent preset) { - // No-op - } - - - /** - * Clear the current component preset. This does not affect the component properties - * otherwise. - */ - public final void clearPreset() { - if (presetComponent == null) - return; - presetComponent = null; - fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE); - } - - - - /** - * Returns the unique ID of the component. - * - * @return the ID of the component. - */ - public final String getID() { - return id; - } - - /** - * Generate a new ID for this component. - */ - private final void newID() { - mutex.verify(); - this.id = UniqueID.uuid(); - } - - - - - /** - * Get the characteristic length of the component, for example the length of a body tube - * of the length of the root chord of a fin. This is used in positioning the component - * relative to its parent. - * - * If the length of a component is settable, the class must define the setter method - * itself. - */ - public final double getLength() { - mutex.verify(); - return length; - } - - /** - * Get the positioning of the component relative to its parent component. - * This is one of the enums of {@link Position}. A setter method is not provided, - * but can be provided by a subclass. - */ - public final Position getRelativePosition() { - mutex.verify(); - return relativePosition; - } - - - /** - * Set the positioning of the component relative to its parent component. - * The actual position of the component is maintained to the best ability. - * <p> - * The default implementation is of protected visibility, since many components - * do not support setting the relative position. A component that does support - * it should override this with a public method that simply calls this - * supermethod AND fire a suitable ComponentChangeEvent. - * - * @param position the relative positioning. - */ - protected void setRelativePosition(RocketComponent.Position position) { - if (this.relativePosition == position) - return; - checkState(); - - // Update position so as not to move the component - if (this.parent != null) { - double thisPos = this.toRelative(Coordinate.NUL, this.parent)[0].x; - - switch (position) { - case ABSOLUTE: - this.position = this.toAbsolute(Coordinate.NUL)[0].x; - break; - - case TOP: - this.position = thisPos; - break; - - case MIDDLE: - this.position = thisPos - (this.parent.length - this.length) / 2; - break; - - case BOTTOM: - this.position = thisPos - (this.parent.length - this.length); - break; - - default: - throw new BugException("Unknown position type: " + position); - } - } - - this.relativePosition = position; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - /** - * Determine position relative to given position argument. Note: This is a side-effect free method. No state - * is modified. - * - * @param thePosition the relative position to be used as the basis for the computation - * @param relativeTo the position is computed relative the the given component - * - * @return double position of the component relative to the parent, with respect to <code>position</code> - */ - public double asPositionValue (Position thePosition, RocketComponent relativeTo) { - double result = this.position; - if (relativeTo != null) { - double thisPos = this.toRelative(Coordinate.NUL, relativeTo)[0].x; - - switch (thePosition) { - case ABSOLUTE: - result = this.toAbsolute(Coordinate.NUL)[0].x; - break; - case TOP: - result = thisPos; - break; - case MIDDLE: - result = thisPos - (relativeTo.length - this.length) / 2; - break; - case BOTTOM: - result = thisPos - (relativeTo.length - this.length); - break; - default: - throw new BugException("Unknown position type: " + thePosition); - } - } - return result; - } - - /** - * Get the position value of the component. The exact meaning of the value is - * dependent on the current relative positioning. - * - * @return the positional value. - */ - public final double getPositionValue() { - mutex.verify(); - return position; - } - - - /** - * Set the position value of the component. The exact meaning of the value - * depends on the current relative positioning. - * <p> - * The default implementation is of protected visibility, since many components - * do not support setting the relative position. A component that does support - * it should override this with a public method that simply calls this - * supermethod AND fire a suitable ComponentChangeEvent. - * - * @param value the position value of the component. - */ - public void setPositionValue(double value) { - if (MathUtil.equals(this.position, value)) - return; - checkState(); - this.position = value; - } - - - - /////////// Coordinate changes /////////// - - /** - * Returns coordinate c in absolute coordinates. Equivalent to toComponent(c,null). - */ - public Coordinate[] toAbsolute(Coordinate c) { - checkState(); - return toRelative(c, null); - } - - - /** - * Return coordinate <code>c</code> described in the coordinate system of - * <code>dest</code>. If <code>dest</code> is <code>null</code> returns - * absolute coordinates. - * <p> - * This method returns an array of coordinates, each of which represents a - * position of the coordinate in clustered cases. The array is guaranteed - * to contain at least one element. - * <p> - * The current implementation does not support rotating components. - * - * @param c Coordinate in the component's coordinate system. - * @param dest Destination component coordinate system. - * @return an array of coordinates describing <code>c</code> in coordinates - * relative to <code>dest</code>. - */ - public final Coordinate[] toRelative(Coordinate c, RocketComponent dest) { - checkState(); - mutex.lock("toRelative"); - try { - double absoluteX = Double.NaN; - RocketComponent search = dest; - Coordinate[] array = new Coordinate[1]; - array[0] = c; - - RocketComponent component = this; - while ((component != search) && (component.parent != null)) { - - array = component.shiftCoordinates(array); - - switch (component.relativePosition) { - case TOP: - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(component.position, 0, 0); - } - break; - - case MIDDLE: - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(component.position + - (component.parent.length - component.length) / 2, 0, 0); - } - break; - - case BOTTOM: - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(component.position + - (component.parent.length - component.length), 0, 0); - } - break; - - case AFTER: - // Add length of all previous brother-components with POSITION_RELATIVE_AFTER - int index = component.parent.children.indexOf(component); - assert (index >= 0); - for (index--; index >= 0; index--) { - RocketComponent comp = component.parent.children.get(index); - double componentLength = comp.getTotalLength(); - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(componentLength, 0, 0); - } - } - for (int i = 0; i < array.length; i++) { - array[i] = array[i].add(component.position + component.parent.length, 0, 0); - } - break; - - case ABSOLUTE: - search = null; // Requires back-search if dest!=null - if (Double.isNaN(absoluteX)) { - absoluteX = component.position; - } - break; - - default: - throw new BugException("Unknown relative positioning type of component" + - component + ": " + component.relativePosition); - } - - component = component.parent; // parent != null - } - - if (!Double.isNaN(absoluteX)) { - for (int i = 0; i < array.length; i++) { - array[i] = array[i].setX(absoluteX + c.x); - } - } - - // Check whether destination has been found or whether to backtrack - // TODO: LOW: Backtracking into clustered components uses only one component - if ((dest != null) && (component != dest)) { - Coordinate[] origin = dest.toAbsolute(Coordinate.NUL); - for (int i = 0; i < array.length; i++) { - array[i] = array[i].sub(origin[0]); - } - } - - return array; - } finally { - mutex.unlock("toRelative"); - } - } - - - /** - * Recursively sum the lengths of all subcomponents that have position - * Position.AFTER. - * - * @return Sum of the lengths. - */ - private final double getTotalLength() { - checkState(); - this.checkComponentStructure(); - mutex.lock("getTotalLength"); - try { - double l = 0; - if (relativePosition == Position.AFTER) - l = length; - for (int i = 0; i < children.size(); i++) - l += children.get(i).getTotalLength(); - return l; - } finally { - mutex.unlock("getTotalLength"); - } - } - - - - /////////// Total mass and CG calculation //////////// - - /** - * Return the (possibly overridden) mass of component. - * - * @return The mass of the component or the given override mass. - */ - public final double getMass() { - mutex.verify(); - if (massOverriden) - return overrideMass; - return getComponentMass(); - } - - /** - * Return the (possibly overridden) center of gravity and mass. - * - * Returns the CG with the weight of the coordinate set to the weight of the component. - * Both CG and mass may be separately overridden. - * - * @return The CG of the component or the given override CG. - */ - public final Coordinate getCG() { - checkState(); - if (cgOverriden) - return getOverrideCG().setWeight(getMass()); - - if (massOverriden) - return getComponentCG().setWeight(getMass()); - - return getComponentCG(); - } - - - /** - * Return the longitudinal (around the y- or z-axis) moment of inertia of this component. - * The moment of inertia is scaled in reference to the (possibly overridden) mass - * and is relative to the non-overridden CG. - * - * @return the longitudinal moment of inertia of this component. - */ - public final double getLongitudinalInertia() { - checkState(); - return getLongitudinalUnitInertia() * getMass(); - } - - /** - * Return the rotational (around the y- or z-axis) moment of inertia of this component. - * The moment of inertia is scaled in reference to the (possibly overridden) mass - * and is relative to the non-overridden CG. - * - * @return the rotational moment of inertia of this component. - */ - public final double getRotationalInertia() { - checkState(); - return getRotationalUnitInertia() * getMass(); - } - - - - /////////// Children handling /////////// - - - /** - * Adds a child to the rocket component tree. The component is added to the end - * of the component's child list. This is a helper method that calls - * {@link #addChild(RocketComponent,int)}. - * - * @param component The component to add. - * @throws IllegalArgumentException if the component is already part of some - * component tree. - * @see #addChild(RocketComponent,int) - */ - public final void addChild(RocketComponent component) { - checkState(); - addChild(component, children.size()); - } - - - /** - * Adds a child to the rocket component tree. The component is added to - * the given position of the component's child list. - * <p> - * This method may be overridden to enforce more strict component addition rules. - * The tests should be performed first and then this method called. - * - * @param component The component to add. - * @param index Position to add component to. - * @throws IllegalArgumentException If the component is already part of - * some component tree. - */ - public void addChild(RocketComponent component, int index) { - checkState(); - if (component.parent != null) { - throw new IllegalArgumentException("component " + component.getComponentName() + - " is already in a tree"); - } - if (!isCompatible(component)) { - throw new IllegalStateException("Component " + component.getComponentName() + - " not currently compatible with component " + getComponentName()); - } - - children.add(index, component); - component.parent = this; - - this.checkComponentStructure(); - component.checkComponentStructure(); - - fireAddRemoveEvent(component); - } - - - /** - * Removes a child from the rocket component tree. - * - * @param n remove the n'th child. - * @throws IndexOutOfBoundsException if n is out of bounds - */ - public final void removeChild(int n) { - checkState(); - RocketComponent component = children.remove(n); - component.parent = null; - - this.checkComponentStructure(); - component.checkComponentStructure(); - - fireAddRemoveEvent(component); - } - - /** - * Removes a child from the rocket component tree. Does nothing if the component - * is not present as a child. - * - * @param component the component to remove - * @return whether the component was a child - */ - public final boolean removeChild(RocketComponent component) { - checkState(); - - component.checkComponentStructure(); - - if (children.remove(component)) { - component.parent = null; - - this.checkComponentStructure(); - component.checkComponentStructure(); - - fireAddRemoveEvent(component); - return true; - } - return false; - } - - - - - /** - * Move a child to another position. - * - * @param component the component to move - * @param index the component's new position - * @throws IllegalArgumentException If an illegal placement was attempted. - */ - public final void moveChild(RocketComponent component, int index) { - checkState(); - if (children.remove(component)) { - children.add(index, component); - - this.checkComponentStructure(); - component.checkComponentStructure(); - - fireAddRemoveEvent(component); - } - } - - - /** - * Fires an AERODYNAMIC_CHANGE, MASS_CHANGE or OTHER_CHANGE event depending on the - * type of component removed. - */ - private void fireAddRemoveEvent(RocketComponent component) { - Iterator<RocketComponent> iter = component.iterator(true); - int type = ComponentChangeEvent.TREE_CHANGE; - while (iter.hasNext()) { - RocketComponent c = iter.next(); - if (c.isAerodynamic()) - type |= ComponentChangeEvent.AERODYNAMIC_CHANGE; - if (c.isMassive()) - type |= ComponentChangeEvent.MASS_CHANGE; - } - - fireComponentChangeEvent(type); - } - - - public final int getChildCount() { - checkState(); - this.checkComponentStructure(); - return children.size(); - } - - public final RocketComponent getChild(int n) { - checkState(); - this.checkComponentStructure(); - return children.get(n); - } - - public final List<RocketComponent> getChildren() { - checkState(); - this.checkComponentStructure(); - return children.clone(); - } - - - /** - * Returns the position of the child in this components child list, or -1 if the - * component is not a child of this component. - * - * @param child The child to search for. - * @return Position in the list or -1 if not found. - */ - public final int getChildPosition(RocketComponent child) { - checkState(); - this.checkComponentStructure(); - return children.indexOf(child); - } - - /** - * Get the parent component of this component. Returns <code>null</code> if the component - * has no parent. - * - * @return The parent of this component or <code>null</code>. - */ - public final RocketComponent getParent() { - checkState(); - return parent; - } - - /** - * Get the root component of the component tree. - * - * @return The root component of the component tree. - */ - public final RocketComponent getRoot() { - checkState(); - RocketComponent gp = this; - while (gp.parent != null) - gp = gp.parent; - return gp; - } - - /** - * Returns the root Rocket component of this component tree. Throws an - * IllegalStateException if the root component is not a Rocket. - * - * @return The root Rocket component of the component tree. - * @throws IllegalStateException If the root component is not a Rocket. - */ - public final Rocket getRocket() { - checkState(); - RocketComponent r = getRoot(); - if (r instanceof Rocket) - return (Rocket) r; - throw new IllegalStateException("getRocket() called with root component " - + r.getComponentName()); - } - - - /** - * Return the Stage component that this component belongs to. Throws an - * IllegalStateException if a Stage is not in the parentage of this component. - * - * @return The Stage component this component belongs to. - * @throws IllegalStateException if a Stage component is not in the parentage. - */ - public final Stage getStage() { - checkState(); - RocketComponent c = this; - while (c != null) { - if (c instanceof Stage) - return (Stage) c; - c = c.getParent(); - } - throw new IllegalStateException("getStage() called without Stage as a parent."); - } - - /** - * Return the stage number of the stage this component belongs to. The stages - * are numbered from zero upwards. - * - * @return the stage number this component belongs to. - */ - public final int getStageNumber() { - checkState(); - if (parent == null) { - throw new IllegalArgumentException("getStageNumber() called for root component"); - } - - RocketComponent stage = this; - while (!(stage instanceof Stage)) { - stage = stage.parent; - if (stage == null || stage.parent == null) { - throw new IllegalStateException("getStageNumber() could not find parent " + - "stage."); - } - } - return stage.parent.getChildPosition(stage); - } - - - /** - * Find a component with the given ID. The component tree is searched from this component - * down (including this component) for the ID and the corresponding component is returned, - * or null if not found. - * - * @param idToFind ID to search for. - * @return The component with the ID, or null if not found. - */ - public final RocketComponent findComponent(String idToFind) { - checkState(); - Iterator<RocketComponent> iter = this.iterator(true); - while (iter.hasNext()) { - RocketComponent c = iter.next(); - if (c.getID().equals(idToFind)) - return c; - } - return null; - } - - - // TODO: Move these methods elsewhere (used only in SymmetricComponent) - public final RocketComponent getPreviousComponent() { - checkState(); - this.checkComponentStructure(); - if (parent == null) - return null; - int pos = parent.getChildPosition(this); - if (pos < 0) { - StringBuffer sb = new StringBuffer(); - sb.append("Inconsistent internal state: "); - sb.append("this=").append(this).append('[') - .append(System.identityHashCode(this)).append(']'); - sb.append(" parent.children=["); - for (int i = 0; i < parent.children.size(); i++) { - RocketComponent c = parent.children.get(i); - sb.append(c).append('[').append(System.identityHashCode(c)).append(']'); - if (i < parent.children.size() - 1) - sb.append(", "); - } - sb.append(']'); - throw new IllegalStateException(sb.toString()); - } - assert (pos >= 0); - if (pos == 0) - return parent; - RocketComponent c = parent.getChild(pos - 1); - while (c.getChildCount() > 0) - c = c.getChild(c.getChildCount() - 1); - return c; - } - - // TODO: Move these methods elsewhere (used only in SymmetricComponent) - public final RocketComponent getNextComponent() { - checkState(); - if (getChildCount() > 0) - return getChild(0); - - RocketComponent current = this; - RocketComponent nextParent = this.parent; - - while (nextParent != null) { - int pos = nextParent.getChildPosition(current); - if (pos < nextParent.getChildCount() - 1) - return nextParent.getChild(pos + 1); - - current = nextParent; - nextParent = current.parent; - } - return null; - } - - - /////////// Event handling ////////// - // - // Listener lists are provided by the root Rocket component, - // a single listener list for the whole rocket. - // - - /** - * Adds a ComponentChangeListener to the rocket tree. The listener is added to the root - * component, which must be of type Rocket (which overrides this method). Events of all - * subcomponents are sent to all listeners. - * - * @throws IllegalStateException - if the root component is not a Rocket - */ - public void addComponentChangeListener(ComponentChangeListener l) { - checkState(); - getRocket().addComponentChangeListener(l); - } - - /** - * Removes a ComponentChangeListener from the rocket tree. The listener is removed from - * the root component, which must be of type Rocket (which overrides this method). - * Does nothing if the root component is not a Rocket. (The asymmetry is so - * that listeners can always be removed just in case.) - * - * @param l Listener to remove - */ - public void removeComponentChangeListener(ComponentChangeListener l) { - if (parent != null) { - getRoot().removeComponentChangeListener(l); - } - } - - - /** - * Adds a <code>ChangeListener</code> to the rocket tree. This is identical to - * <code>addComponentChangeListener()</code> except that it uses a - * <code>ChangeListener</code>. The same events are dispatched to the - * <code>ChangeListener</code>, as <code>ComponentChangeEvent</code> is a subclass - * of <code>ChangeEvent</code>. - * - * @throws IllegalStateException - if the root component is not a <code>Rocket</code> - */ - @Override - public void addChangeListener(EventListener l) { - checkState(); - getRocket().addChangeListener(l); - } - - /** - * Removes a ChangeListener from the rocket tree. This is identical to - * removeComponentChangeListener() except it uses a ChangeListener. - * Does nothing if the root component is not a Rocket. (The asymmetry is so - * that listeners can always be removed just in case.) - * - * @param l Listener to remove - */ - @Override - public void removeChangeListener(EventListener l) { - if (this.parent != null) { - getRoot().removeChangeListener(l); - } - } - - - /** - * Fires a ComponentChangeEvent on the rocket structure. The call is passed to the - * root component, which must be of type Rocket (which overrides this method). - * Events of all subcomponents are sent to all listeners. - * - * If the component tree root is not a Rocket, the event is ignored. This is the - * case when constructing components not in any Rocket tree. In this case it - * would be impossible for the component to have listeners in any case. - * - * @param e Event to send - */ - protected void fireComponentChangeEvent(ComponentChangeEvent e) { - checkState(); - if (parent == null) { - /* Ignore if root invalid. */ - log.debug("Attempted firing event " + e + " with root " + this.getComponentName() + ", ignoring event"); - return; - } - getRoot().fireComponentChangeEvent(e); - } - - - /** - * Fires a ComponentChangeEvent of the given type. The source of the event is set to - * this component. - * - * @param type Type of event - * @see #fireComponentChangeEvent(ComponentChangeEvent) - */ - protected void fireComponentChangeEvent(int type) { - fireComponentChangeEvent(new ComponentChangeEvent(this, type)); - } - - - /** - * Checks whether this component has been invalidated and should no longer be used. - * This is a safety check that in-place replaced components are no longer used. - * All non-trivial methods (with the exception of methods simply getting a property) - * should call this method before changing or computing anything. - * - * @throws BugException if this component has been invalidated by {@link #copyFrom(RocketComponent)}. - */ - protected void checkState() { - invalidator.check(true); - mutex.verify(); - } - - - /** - * Check that the local component structure is correct. This can be called after changing - * the component structure in order to verify the integrity. - * <p> - * TODO: Remove this after the "inconsistent internal state" bug has been corrected - */ - public void checkComponentStructure() { - if (this.parent != null) { - // Test that this component is found in parent's children with == operator - if (!containsExact(this.parent.children, this)) { - throw new BugException("Inconsistent component structure detected, parent does not contain this " + - "component as a child, parent=" + parent.toDebugString() + " this=" + this.toDebugString()); - } - } - for (RocketComponent child : this.children) { - if (child.parent != this) { - throw new BugException("Inconsistent component structure detected, child does not have this component " + - "as the parent, this=" + this.toDebugString() + " child=" + child.toDebugString() + - " child.parent=" + (child.parent == null ? "null" : child.parent.toDebugString())); - } - } - } - - // Check whether the list contains exactly the searched-for component (with == operator) - private boolean containsExact(List<RocketComponent> haystack, RocketComponent needle) { - for (RocketComponent c : haystack) { - if (needle == c) { - return true; - } - } - return false; - } - - - /////////// Iterators ////////// - - /** - * Returns an iterator that iterates over all children and sub-children. - * <p> - * The iterator iterates through all children below this object, including itself if - * <code>returnSelf</code> is true. The order of the iteration is not specified - * (it may be specified in the future). - * <p> - * If an iterator iterating over only the direct children of the component is required, - * use <code>component.getChildren().iterator()</code>. - * - * TODO: HIGH: Remove this after merges have been done - * - * @param returnSelf boolean value specifying whether the component itself should be - * returned - * @return An iterator for the children and sub-children. - * @deprecated Use {@link #iterator(boolean)} instead - */ - @Deprecated - public final Iterator<RocketComponent> deepIterator(boolean returnSelf) { - return iterator(returnSelf); - } - - - /** - * Returns an iterator that iterates over all children and sub-children, including itself. - * <p> - * This method is equivalent to <code>deepIterator(true)</code>. - * - * TODO: HIGH: Remove this after merges have been done - * - * @return An iterator for this component, its children and sub-children. - * @deprecated Use {@link #iterator()} instead - */ - @Deprecated - public final Iterator<RocketComponent> deepIterator() { - return iterator(); - } - - - - /** - * Returns an iterator that iterates over all children and sub-children. - * <p> - * The iterator iterates through all children below this object, including itself if - * <code>returnSelf</code> is true. The order of the iteration is not specified - * (it may be specified in the future). - * <p> - * If an iterator iterating over only the direct children of the component is required, - * use <code>component.getChildren().iterator()</code>. - * - * @param returnSelf boolean value specifying whether the component itself should be - * returned - * @return An iterator for the children and sub-children. - */ - public final Iterator<RocketComponent> iterator(boolean returnSelf) { - checkState(); - return new RocketComponentIterator(this, returnSelf); - } - - - /** - * Returns an iterator that iterates over this component, its children and sub-children. - * <p> - * This method is equivalent to <code>iterator(true)</code>. - * - * @return An iterator for this component, its children and sub-children. - */ - @Override - public final Iterator<RocketComponent> iterator() { - return iterator(true); - } - - - - - - /** - * Compare component equality based on the ID of this component. Only the - * ID and class type is used for a basis of comparison. - */ - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (this.getClass() != obj.getClass()) - return false; - RocketComponent other = (RocketComponent) obj; - return this.id.equals(other.id); - } - - - - @Override - public int hashCode() { - return id.hashCode(); - } - - - - //////////// Helper methods for subclasses - - - - - /** - * Helper method to add rotationally symmetric bounds at the specified coordinates. - * The X-axis value is <code>x</code> and the radius at the specified position is - * <code>r</code>. - */ - protected static final void addBound(Collection<Coordinate> bounds, double x, double r) { - bounds.add(new Coordinate(x, -r, -r)); - bounds.add(new Coordinate(x, r, -r)); - bounds.add(new Coordinate(x, r, r)); - bounds.add(new Coordinate(x, -r, r)); - } - - - protected static final Coordinate ringCG(double outerRadius, double innerRadius, - double x1, double x2, double density) { - return new Coordinate((x1 + x2) / 2, 0, 0, - ringMass(outerRadius, innerRadius, x2 - x1, density)); - } - - protected static final double ringMass(double outerRadius, double innerRadius, - double length, double density) { - return Math.PI * (MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) * - length * density; - } - - protected static final double ringLongitudinalUnitInertia(double outerRadius, - double innerRadius, double length) { - // 1/12 * (3 * (r1^2 + r2^2) + h^2) - return (3 * (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) + MathUtil.pow2(length)) / 12; - } - - protected static final double ringRotationalUnitInertia(double outerRadius, - double innerRadius) { - // 1/2 * (r1^2 + r2^2) - return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) / 2; - } - - - - //////////// OTHER - - - /** - * Loads the RocketComponent fields from the given component. This method is meant - * for in-place replacement of a component. It is used with the undo/redo - * mechanism and when converting a finset into a freeform fin set. - * This component must not have a parent, otherwise this method will fail. - * <p> - * The child components in the source tree are copied into the current tree, however, - * the original components should not be used since they represent old copies of the - * components. It is recommended to invalidate them by calling {@link #invalidate()}. - * <p> - * This method returns a list of components that should be invalidated after references - * to them have been removed (for example by firing appropriate events). The list contains - * all children and sub-children of the current component and the entire component - * tree of <code>src</code>. - * - * @return a list of components that should not be used after this call. - */ - protected List<RocketComponent> copyFrom(RocketComponent src) { - checkState(); - List<RocketComponent> toInvalidate = new ArrayList<RocketComponent>(); - - if (this.parent != null) { - throw new UnsupportedOperationException("copyFrom called for non-root component, parent=" + - this.parent.toDebugString() + ", this=" + this.toDebugString()); - } - - // Add current structure to be invalidated - Iterator<RocketComponent> iterator = this.iterator(false); - while (iterator.hasNext()) { - toInvalidate.add(iterator.next()); - } - - // Remove previous components - for (RocketComponent child : this.children) { - child.parent = null; - } - this.children.clear(); - - // Copy new children to this component - for (RocketComponent c : src.children) { - RocketComponent copy = c.copyWithOriginalID(); - this.children.add(copy); - copy.parent = this; - } - - this.checkComponentStructure(); - src.checkComponentStructure(); - - // Set all parameters - this.length = src.length; - this.relativePosition = src.relativePosition; - this.position = src.position; - this.color = src.color; - this.lineStyle = src.lineStyle; - this.overrideMass = src.overrideMass; - this.massOverriden = src.massOverriden; - this.overrideCGX = src.overrideCGX; - this.cgOverriden = src.cgOverriden; - this.overrideSubcomponents = src.overrideSubcomponents; - this.name = src.name; - this.comment = src.comment; - this.id = src.id; - - // Add source components to invalidation tree - for (RocketComponent c : src) { - toInvalidate.add(c); - } - - return toInvalidate; - } - - protected void invalidate() { - invalidator.invalidate(); - } - - - ////////// Iterator implementation /////////// - - /** - * Private inner class to implement the Iterator. - * - * This iterator is fail-fast if the root of the structure is a Rocket. - */ - private static class RocketComponentIterator implements Iterator<RocketComponent> { - // Stack holds iterators which still have some components left. - private final Deque<Iterator<RocketComponent>> iteratorStack = new ArrayDeque<Iterator<RocketComponent>>(); - - private final Rocket root; - private final int treeModID; - - private final RocketComponent original; - private boolean returnSelf = false; - - // Construct iterator with component's child's iterator, if it has elements - public RocketComponentIterator(RocketComponent c, boolean returnSelf) { - - RocketComponent gp = c.getRoot(); - if (gp instanceof Rocket) { - root = (Rocket) gp; - treeModID = root.getTreeModID(); - } else { - root = null; - treeModID = -1; - } - - Iterator<RocketComponent> i = c.children.iterator(); - if (i.hasNext()) - iteratorStack.push(i); - - this.original = c; - this.returnSelf = returnSelf; - } - - @Override - public boolean hasNext() { - checkID(); - if (returnSelf) - return true; - return !iteratorStack.isEmpty(); // Elements remain if stack is not empty - } - - @Override - public RocketComponent next() { - Iterator<RocketComponent> i; - - checkID(); - - // Return original component first - if (returnSelf) { - returnSelf = false; - return original; - } - - // Peek first iterator from stack, throw exception if empty - i = iteratorStack.peek(); - if (i == null) { - throw new NoSuchElementException("No further elements in RocketComponent iterator"); - } - - // Retrieve next component of the iterator, remove iterator from stack if empty - RocketComponent c = i.next(); - if (!i.hasNext()) - iteratorStack.pop(); - - // Add iterator of component children to stack if it has children - i = c.children.iterator(); - if (i.hasNext()) - iteratorStack.push(i); - - return c; - } - - private void checkID() { - if (root != null) { - if (root.getTreeModID() != treeModID) { - throw new IllegalStateException("Rocket modified while being iterated"); - } - } - } - - @Override - public void remove() { - throw new UnsupportedOperationException("remove() not supported by " + - "RocketComponent iterator"); - } - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/RocketUtils.java b/src/net/sf/openrocket/rocketcomponent/RocketUtils.java deleted file mode 100644 index 2e4b63b8..00000000 --- a/src/net/sf/openrocket/rocketcomponent/RocketUtils.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import java.util.Collection; - -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.masscalc.MassCalculator.MassCalcType; -import net.sf.openrocket.util.Coordinate; - -public abstract class RocketUtils { - - public static double getLength(Rocket rocket) { - double length = 0; - Collection<Coordinate> bounds = rocket.getDefaultConfiguration().getBounds(); - if (!bounds.isEmpty()) { - double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY; - for (Coordinate c : bounds) { - if (c.x < minX) - minX = c.x; - if (c.x > maxX) - maxX = c.x; - } - length = maxX - minX; - } - return length; - } - - public static Coordinate getCG(Rocket rocket) { - MassCalculator massCalculator = new BasicMassCalculator(); - Coordinate cg = massCalculator.getCG(rocket.getDefaultConfiguration(), MassCalcType.LAUNCH_MASS); - return cg; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ShockCord.java b/src/net/sf/openrocket/rocketcomponent/ShockCord.java deleted file mode 100644 index 8012a87e..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ShockCord.java +++ /dev/null @@ -1,71 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; - -public class ShockCord extends MassObject { - private static final Translator trans = Application.getTranslator(); - - private Material material; - private double cordLength; - - public ShockCord() { - material = Application.getPreferences().getDefaultComponentMaterial(ShockCord.class, Material.Type.LINE); - cordLength = 0.4; - } - - - - public Material getMaterial() { - return material; - } - - public void setMaterial(Material m) { - if (m.getType() != Material.Type.LINE) - throw new BugException("Attempting to set non-linear material."); - if (material.equals(m)) - return; - this.material = m; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - public double getCordLength() { - return cordLength; - } - - public void setCordLength(double length) { - length = MathUtil.max(length, 0); - if (MathUtil.equals(length, this.length)) - return; - this.cordLength = length; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - @Override - public double getComponentMass() { - return material.getDensity() * cordLength; - } - - @Override - public String getComponentName() { - //// Shock cord - return trans.get("ShockCord.ShockCord"); - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Sleeve.java b/src/net/sf/openrocket/rocketcomponent/Sleeve.java deleted file mode 100644 index ec244ee0..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Sleeve.java +++ /dev/null @@ -1,106 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -/** - * A RingComponent that comes on top of another tube. It's defined by the inner - * radius and thickness. The inner radius can be automatic, in which case it - * takes the radius of the parent component. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Sleeve extends RingComponent { - - protected double innerRadius = 0; - protected double thickness = 0; - - - public Sleeve() { - super(); - setInnerRadiusAutomatic(true); - setThickness(0.001); - setLength(0.05); - } - - - @Override - public double getOuterRadius() { - return getInnerRadius() + thickness; - } - - @Override - public void setOuterRadius(double r) { - if (MathUtil.equals(getOuterRadius(), r)) - return; - - innerRadius = Math.max(r - thickness, 0); - if (thickness > r) - thickness = r; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - @Override - public double getInnerRadius() { - // Implement parent inner radius automation - if (isInnerRadiusAutomatic() && getParent() instanceof RadialParent) { - RocketComponent parent = getParent(); - double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x; - double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x; - pos1 = MathUtil.clamp(pos1, 0, parent.getLength()); - pos2 = MathUtil.clamp(pos2, 0, parent.getLength()); - innerRadius = Math.max(((RadialParent) parent).getOuterRadius(pos1), - ((RadialParent) parent).getOuterRadius(pos2)); - } - - return innerRadius; - } - - @Override - public void setInnerRadius(double r) { - r = Math.max(r, 0); - if (MathUtil.equals(innerRadius, r)) - return; - innerRadius = r; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - @Override - public double getThickness() { - return thickness; - } - - @Override - public void setThickness(double t) { - t = Math.max(t, 0); - if (MathUtil.equals(thickness, t)) - return; - thickness = t; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - @Override - public void setInnerRadiusAutomatic(boolean auto) { - super.setOuterRadiusAutomatic(auto); - } - - @Override - public String getComponentName() { - return "Sleeve"; - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/Stage.java b/src/net/sf/openrocket/rocketcomponent/Stage.java deleted file mode 100644 index f5079bfe..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Stage.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -public class Stage extends ComponentAssembly { - - private static final Translator trans = Application.getTranslator(); - - @Override - public String getComponentName () { - //// Stage - return trans.get("Stage.Stage"); - } - - - @Override - public boolean allowsChildren() { - return true; - } - - /** - * Check whether the given type can be added to this component. A Stage allows - * only BodyComponents to be added. - * - * @param type The RocketComponent class type to add. - * - * @return Whether such a component can be added. - */ - @Override - public boolean isCompatible (Class<? extends RocketComponent> type) { - return BodyComponent.class.isAssignableFrom(type); - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/Streamer.java b/src/net/sf/openrocket/rocketcomponent/Streamer.java deleted file mode 100644 index 8d73c457..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Streamer.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.util.MathUtil; - -public class Streamer extends RecoveryDevice { - - public static final double DEFAULT_CD = 0.6; - - public static final double MAX_COMPUTED_CD = 0.4; - - - private double stripLength; - private double stripWidth; - - - public Streamer() { - this.stripLength = 0.5; - this.stripWidth = 0.05; - } - - - public double getStripLength() { - return stripLength; - } - - public void setStripLength(double stripLength) { - if (MathUtil.equals(this.stripLength, stripLength)) - return; - this.stripLength = stripLength; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public double getStripWidth() { - return stripWidth; - } - - public void setStripWidth(double stripWidth) { - if (MathUtil.equals(this.stripWidth, stripWidth)) - return; - this.stripWidth = stripWidth; - this.length = stripWidth; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - @Override - public void setLength(double length) { - setStripWidth(length); - } - - - public double getAspectRatio() { - if (stripWidth > 0.0001) - return stripLength / stripWidth; - return 1000; - } - - public void setAspectRatio(double ratio) { - if (MathUtil.equals(getAspectRatio(), ratio)) - return; - - ratio = Math.max(ratio, 0.01); - double area = getArea(); - stripWidth = MathUtil.safeSqrt(area / ratio); - stripLength = ratio * stripWidth; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - @Override - public double getArea() { - return stripWidth * stripLength; - } - - public void setArea(double area) { - if (MathUtil.equals(getArea(), area)) - return; - - double ratio = Math.max(getAspectRatio(), 0.01); - stripWidth = MathUtil.safeSqrt(area / ratio); - stripLength = ratio * stripWidth; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - @Override - public double getComponentCD(double mach) { - double density = this.getMaterial().getDensity(); - double cd; - - cd = 0.034 * ((density + 0.025) / 0.105) * (stripLength + 1) / stripLength; - cd = MathUtil.min(cd, MAX_COMPUTED_CD); - return cd; - } - - @Override - public String getComponentName() { - return "Streamer"; - } - - @Override - public boolean allowsChildren() { - return false; - } - - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return false; - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java b/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java deleted file mode 100644 index d37af27e..00000000 --- a/src/net/sf/openrocket/rocketcomponent/StructuralComponent.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.material.Material; -import net.sf.openrocket.startup.Application; - -public abstract class StructuralComponent extends InternalComponent { - - private Material material; - - public StructuralComponent() { - super(); - material = Application.getPreferences().getDefaultComponentMaterial(this.getClass(), Material.Type.BULK); - } - - - public final Material getMaterial() { - return material; - } - - public final void setMaterial(Material mat) { - if (mat.getType() != Material.Type.BULK) { - throw new IllegalArgumentException("Attempted to set non-bulk material "+mat); - } - if (mat.equals(material)) - return; - this.material = mat; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java b/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java deleted file mode 100644 index 5dc18354..00000000 --- a/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java +++ /dev/null @@ -1,577 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -/** - * Class for an axially symmetric rocket component generated by rotating - * a function y=f(x) >= 0 around the x-axis (eg. tube, cone, etc.) - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public abstract class SymmetricComponent extends BodyComponent implements RadialParent { - public static final double DEFAULT_RADIUS = 0.025; - public static final double DEFAULT_THICKNESS = 0.002; - - private static final int DIVISIONS = 100; // No. of divisions when integrating - - protected boolean filled = false; - protected double thickness = DEFAULT_THICKNESS; - - - // Cached data, default values signify not calculated - private double wetArea = -1; - private double planArea = -1; - private double planCenter = -1; - private double volume = -1; - private double fullVolume = -1; - private double longitudinalInertia = -1; - private double rotationalInertia = -1; - private Coordinate cg = null; - - - - public SymmetricComponent() { - super(); - } - - - /** - * Return the component radius at position x. - * @param x Position on x-axis. - * @return Radius of the component at the given position, or 0 if outside - * the component. - */ - public abstract double getRadius(double x); - - @Override - public abstract double getInnerRadius(double x); - - public abstract double getForeRadius(); - - public abstract boolean isForeRadiusAutomatic(); - - public abstract double getAftRadius(); - - public abstract boolean isAftRadiusAutomatic(); - - - // Implement the Radial interface: - @Override - public final double getOuterRadius(double x) { - return getRadius(x); - } - - - @Override - public final double getRadius(double x, double theta) { - return getRadius(x); - } - - @Override - public final double getInnerRadius(double x, double theta) { - return getInnerRadius(x); - } - - - - /** - * Return the component wall thickness. - */ - public double getThickness() { - if (filled) - return Math.max(getForeRadius(), getAftRadius()); - return Math.min(thickness, Math.max(getForeRadius(), getAftRadius())); - } - - - /** - * Set the component wall thickness. Values greater than the maximum radius are not - * allowed, and will result in setting the thickness to the maximum radius. - */ - public void setThickness(double thickness) { - if ((this.thickness == thickness) && !filled) - return; - this.thickness = MathUtil.clamp(thickness, 0, Math.max(getForeRadius(), getAftRadius())); - filled = false; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - clearPreset(); - } - - - /** - * Returns whether the component is set as filled. If it is set filled, then the - * wall thickness will have no effect. - */ - public boolean isFilled() { - return filled; - } - - - /** - * Sets whether the component is set as filled. If the component is filled, then - * the wall thickness will have no effect. - */ - public void setFilled(boolean filled) { - if (this.filled == filled) - return; - this.filled = filled; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - clearPreset(); - } - - - /** - * Adds component bounds at a number of points between 0...length. - */ - @Override - public Collection<Coordinate> getComponentBounds() { - List<Coordinate> list = new ArrayList<Coordinate>(20); - for (int n = 0; n <= 5; n++) { - double x = n * length / 5; - double r = getRadius(x); - addBound(list, x, r); - } - return list; - } - - - - @Override - protected void loadFromPreset(RocketComponent preset) { - SymmetricComponent c = (SymmetricComponent) preset; - this.setThickness(c.getThickness()); - this.setFilled(c.isFilled()); - - super.loadFromPreset(preset); - } - - - - - /** - * Calculate volume of the component by integrating over the length of the component. - * The method caches the result, so subsequent calls are instant. Subclasses may - * override this method for simple shapes and use this method as necessary. - * - * @return The volume of the component. - */ - @Override - public double getComponentVolume() { - if (volume < 0) - integrate(); - return volume; - } - - - /** - * Calculate full (filled) volume of the component by integrating over the length - * of the component. The method caches the result, so subsequent calls are instant. - * Subclasses may override this method for simple shapes and use this method as - * necessary. - * - * @return The filled volume of the component. - */ - public double getFullVolume() { - if (fullVolume < 0) - integrate(); - return fullVolume; - } - - - /** - * Calculate the wetted area of the component by integrating over the length - * of the component. The method caches the result, so subsequent calls are instant. - * Subclasses may override this method for simple shapes and use this method as - * necessary. - * - * @return The wetted area of the component. - */ - public double getComponentWetArea() { - if (wetArea < 0) - integrate(); - return wetArea; - } - - - /** - * Calculate the planform area of the component by integrating over the length of - * the component. The method caches the result, so subsequent calls are instant. - * Subclasses may override this method for simple shapes and use this method as - * necessary. - * - * @return The planform area of the component. - */ - public double getComponentPlanformArea() { - if (planArea < 0) - integrate(); - return planArea; - } - - - /** - * Calculate the planform center X-coordinate of the component by integrating over - * the length of the component. The planform center is defined as - * <pre> integrate(x*2*r(x)) / planform area </pre> - * The method caches the result, so subsequent calls are instant. Subclasses may - * override this method for simple shapes and use this method as necessary. - * - * @return The planform center of the component. - */ - public double getComponentPlanformCenter() { - if (planCenter < 0) - integrate(); - return planCenter; - } - - - /** - * Calculate CG of the component by integrating over the length of the component. - * The method caches the result, so subsequent calls are instant. Subclasses may - * override this method for simple shapes and use this method as necessary. - * - * @return The CG+mass of the component. - */ - @Override - public Coordinate getComponentCG() { - if (cg == null) - integrate(); - return cg; - } - - - @Override - public double getLongitudinalUnitInertia() { - if (longitudinalInertia < 0) { - if (getComponentVolume() > 0.0000001) // == 0.1cm^3 - integrateInertiaVolume(); - else - integrateInertiaSurface(); - } - return longitudinalInertia; - } - - - @Override - public double getRotationalUnitInertia() { - if (rotationalInertia < 0) { - if (getComponentVolume() > 0.0000001) - integrateInertiaVolume(); - else - integrateInertiaSurface(); - } - return rotationalInertia; - } - - - - /** - * Performs integration over the length of the component and updates the cached variables. - */ - private void integrate() { - double x, r1, r2; - double cgx; - - // Check length > 0 - if (length <= 0) { - wetArea = 0; - planArea = 0; - planCenter = 0; - volume = 0; - cg = Coordinate.NUL; - return; - } - - - // Integrate for volume, CG, wetted area and planform area - - final double l = length / DIVISIONS; - final double pil = Math.PI * l; // PI * l - final double pil3 = Math.PI * l / 3; // PI * l/3 - r1 = getRadius(0); - x = 0; - wetArea = 0; - planArea = 0; - planCenter = 0; - fullVolume = 0; - volume = 0; - cgx = 0; - - for (int n = 1; n <= DIVISIONS; n++) { - /* - * r1 and r2 are the two radii - * x is the position of r1 - * hyp is the length of the hypotenuse from r1 to r2 - * height if the y-axis height of the component if not filled - */ - - r2 = getRadius(x + l); - final double hyp = MathUtil.hypot(r2 - r1, l); - - - // Volume differential elements - final double dV; - final double dFullV; - - dFullV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2); - if (filled || r1 < thickness || r2 < thickness) { - // Filled piece - dV = dFullV; - } else { - // Hollow piece - final double height = thickness * hyp / l; - dV = MathUtil.max(pil * height * (r1 + r2 - height), 0); - } - - // Add to the volume-related components - volume += dV; - fullVolume += dFullV; - cgx += (x + l / 2) * dV; - - // Wetted area ( * PI at the end) - wetArea += hyp * (r1 + r2); - - // Planform area & center - final double p = l * (r1 + r2); - planArea += p; - planCenter += (x + l / 2) * p; - - // Update for next iteration - r1 = r2; - x += l; - } - - wetArea *= Math.PI; - - if (planArea > 0) - planCenter /= planArea; - - if (volume < 0.0000000001) { // 0.1 mm^3 - volume = 0; - cg = new Coordinate(length / 2, 0, 0, 0); - } else { - // getComponentMass is safe now - // Use super.getComponentMass() to ensure only the transition shape mass - // is used, not the shoulders - cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass()); - } - } - - - /** - * Integrate the longitudinal and rotational inertia based on component volume. - * This method may be used only if the total volume is zero. - */ - private void integrateInertiaVolume() { - double x, r1, r2; - - final double l = length / DIVISIONS; - final double pil = Math.PI * l; // PI * l - final double pil3 = Math.PI * l / 3; // PI * l/3 - - r1 = getRadius(0); - x = 0; - longitudinalInertia = 0; - rotationalInertia = 0; - - double vol = 0; - - for (int n = 1; n <= DIVISIONS; n++) { - /* - * r1 and r2 are the two radii, outer is their average - * x is the position of r1 - * hyp is the length of the hypotenuse from r1 to r2 - * height if the y-axis height of the component if not filled - */ - r2 = getRadius(x + l); - final double outer = (r1 + r2) / 2; - - - // Volume differential elements - final double inner; - final double dV; - - if (filled || r1 < thickness || r2 < thickness) { - inner = 0; - dV = pil3 * (r1 * r1 + r1 * r2 + r2 * r2); - } else { - final double hyp = MathUtil.hypot(r2 - r1, l); - final double height = thickness * hyp / l; - dV = pil * height * (r1 + r2 - height); - inner = Math.max(outer - height, 0); - } - - rotationalInertia += dV * (pow2(outer) + pow2(inner)) / 2; - longitudinalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l)) / 12 - + pow2(x + l / 2)); - - vol += dV; - - // Update for next iteration - r1 = r2; - x += l; - } - - if (MathUtil.equals(vol, 0)) { - integrateInertiaSurface(); - return; - } - - rotationalInertia /= vol; - longitudinalInertia /= vol; - - // Shift longitudinal inertia to CG - longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0); - } - - - - /** - * Integrate the longitudinal and rotational inertia based on component surface area. - * This method may be used only if the total volume is zero. - */ - private void integrateInertiaSurface() { - double x, r1, r2; - - final double l = length / DIVISIONS; - - r1 = getRadius(0); - System.out.println(r1); - x = 0; - - longitudinalInertia = 0; - rotationalInertia = 0; - - double surface = 0; - - for (int n = 1; n <= DIVISIONS; n++) { - /* - * r1 and r2 are the two radii, outer is their average - * x is the position of r1 - * hyp is the length of the hypotenuse from r1 to r2 - * height if the y-axis height of the component if not filled - */ - r2 = getRadius(x + l); - final double hyp = MathUtil.hypot(r2 - r1, l); - final double outer = (r1 + r2) / 2; - - final double dS = hyp * (r1 + r2) * Math.PI; - - rotationalInertia += dS * pow2(outer); - longitudinalInertia += dS * ((6 * pow2(outer) + pow2(l)) / 12 + pow2(x + l / 2)); - - surface += dS; - - // Update for next iteration - r1 = r2; - x += l; - } - - if (MathUtil.equals(surface, 0)) { - longitudinalInertia = 0; - rotationalInertia = 0; - return; - } - - longitudinalInertia /= surface; - rotationalInertia /= surface; - - // Shift longitudinal inertia to CG - longitudinalInertia = Math.max(longitudinalInertia - pow2(getComponentCG().x), 0); - } - - - - - /** - * Invalidates the cached volume and CG information. - */ - @Override - protected void componentChanged(ComponentChangeEvent e) { - super.componentChanged(e); - if (!e.isOtherChange()) { - wetArea = -1; - planArea = -1; - planCenter = -1; - volume = -1; - fullVolume = -1; - longitudinalInertia = -1; - rotationalInertia = -1; - cg = null; - } - } - - - - /////////// Auto radius helper methods - - - /** - * Returns the automatic radius for this component towards the - * front of the rocket. The automatics will not search towards the - * rear of the rocket for a suitable radius. A positive return value - * indicates a preferred radius, a negative value indicates that a - * match was not found. - */ - protected abstract double getFrontAutoRadius(); - - /** - * Returns the automatic radius for this component towards the - * end of the rocket. The automatics will not search towards the - * front of the rocket for a suitable radius. A positive return value - * indicates a preferred radius, a negative value indicates that a - * match was not found. - */ - protected abstract double getRearAutoRadius(); - - - - /** - * Return the previous symmetric component, or null if none exists. - * NOTE: This method currently assumes that there are no external - * "pods". - * - * @return the previous SymmetricComponent, or null. - */ - protected final SymmetricComponent getPreviousSymmetricComponent() { - RocketComponent c; - for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) { - if (c instanceof SymmetricComponent) { - return (SymmetricComponent) c; - } - if (!(c instanceof Stage) && - (c.relativePosition == RocketComponent.Position.AFTER)) - return null; // Bad component type as "parent" - } - return null; - } - - /** - * Return the next symmetric component, or null if none exists. - * NOTE: This method currently assumes that there are no external - * "pods". - * - * @return the next SymmetricComponent, or null. - */ - protected final SymmetricComponent getNextSymmetricComponent() { - RocketComponent c; - for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) { - if (c instanceof SymmetricComponent) { - return (SymmetricComponent) c; - } - if (!(c instanceof Stage) && - (c.relativePosition == RocketComponent.Position.AFTER)) - return null; // Bad component type as "parent" - } - return null; - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java b/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java deleted file mode 100644 index 4bded707..00000000 --- a/src/net/sf/openrocket/rocketcomponent/ThicknessRingComponent.java +++ /dev/null @@ -1,82 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -/** - * An inner component that consists of a hollow cylindrical component. This can be - * an inner tube, tube coupler, centering ring, bulkhead etc. - * - * The properties include the inner and outer radii, length and radial position. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class ThicknessRingComponent extends RingComponent { - - protected double outerRadius = 0; - protected double thickness = 0; - - - @Override - public double getOuterRadius() { - if (isOuterRadiusAutomatic() && getParent() instanceof RadialParent) { - RocketComponent parent = getParent(); - double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x; - double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x; - pos1 = MathUtil.clamp(pos1, 0, parent.getLength()); - pos2 = MathUtil.clamp(pos2, 0, parent.getLength()); - outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1), - ((RadialParent)parent).getInnerRadius(pos2)); - } - - return outerRadius; - } - - - @Override - public void setOuterRadius(double r) { - r = Math.max(r,0); - if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic()) - return; - - outerRadius = r; - outerRadiusAutomatic = false; - - if (thickness > outerRadius) - thickness = outerRadius; - - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - @Override - public double getThickness() { - return Math.min(thickness, getOuterRadius()); - } - @Override - public void setThickness(double thickness) { - double outer = getOuterRadius(); - - thickness = MathUtil.clamp(thickness, 0, outer); - if (MathUtil.equals(getThickness(), thickness)) - return; - - this.thickness = thickness; - - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - @Override - public double getInnerRadius() { - return Math.max(getOuterRadius()-thickness, 0); - } - @Override - public void setInnerRadius(double r) { - r = Math.max(r,0); - setThickness(getOuterRadius() - r); - } - - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Transition.java b/src/net/sf/openrocket/rocketcomponent/Transition.java deleted file mode 100644 index f3a6f9b6..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Transition.java +++ /dev/null @@ -1,841 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import static java.lang.Math.sin; -import static net.sf.openrocket.util.MathUtil.*; - -import java.util.Collection; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - - -public class Transition extends SymmetricComponent { - private static final Translator trans = Application.getTranslator(); - private static final double CLIP_PRECISION = 0.0001; - - - private Shape type; - private double shapeParameter; - private boolean clipped; // Not to be read - use isClipped(), which may be overriden - - private double radius1, radius2; - private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic - - - private double foreShoulderRadius; - private double foreShoulderThickness; - private double foreShoulderLength; - private boolean foreShoulderCapped; - private double aftShoulderRadius; - private double aftShoulderThickness; - private double aftShoulderLength; - private boolean aftShoulderCapped; - - - // Used to cache the clip length - private double clipLength = -1; - - public Transition() { - super(); - - this.radius1 = DEFAULT_RADIUS; - this.radius2 = DEFAULT_RADIUS; - this.length = DEFAULT_RADIUS * 3; - this.autoRadius1 = true; - this.autoRadius2 = true; - - this.type = Shape.CONICAL; - this.shapeParameter = 0; - this.clipped = true; - } - - - - - //////// Fore radius //////// - - - @Override - public double getForeRadius() { - if (isForeRadiusAutomatic()) { - // Get the automatic radius from the front - double r = -1; - SymmetricComponent c = this.getPreviousSymmetricComponent(); - if (c != null) { - r = c.getFrontAutoRadius(); - } - if (r < 0) - r = DEFAULT_RADIUS; - return r; - } - return radius1; - } - - public void setForeRadius(double radius) { - if ((this.radius1 == radius) && (autoRadius1 == false)) - return; - - this.autoRadius1 = false; - this.radius1 = Math.max(radius, 0); - - if (this.thickness > this.radius1 && this.thickness > this.radius2) - this.thickness = Math.max(this.radius1, this.radius2); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - @Override - public boolean isForeRadiusAutomatic() { - return autoRadius1; - } - - public void setForeRadiusAutomatic(boolean auto) { - if (autoRadius1 == auto) - return; - - autoRadius1 = auto; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - //////// Aft radius ///////// - - @Override - public double getAftRadius() { - if (isAftRadiusAutomatic()) { - // Return the auto radius from the rear - double r = -1; - SymmetricComponent c = this.getNextSymmetricComponent(); - if (c != null) { - r = c.getRearAutoRadius(); - } - if (r < 0) - r = DEFAULT_RADIUS; - return r; - } - return radius2; - } - - - - public void setAftRadius(double radius) { - if ((this.radius2 == radius) && (autoRadius2 == false)) - return; - - this.autoRadius2 = false; - this.radius2 = Math.max(radius, 0); - - if (this.thickness > this.radius1 && this.thickness > this.radius2) - this.thickness = Math.max(this.radius1, this.radius2); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - @Override - public boolean isAftRadiusAutomatic() { - return autoRadius2; - } - - public void setAftRadiusAutomatic(boolean auto) { - if (autoRadius2 == auto) - return; - - autoRadius2 = auto; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - //// Radius automatics - - @Override - protected double getFrontAutoRadius() { - if (isAftRadiusAutomatic()) - return -1; - return getAftRadius(); - } - - - @Override - protected double getRearAutoRadius() { - if (isForeRadiusAutomatic()) - return -1; - return getForeRadius(); - } - - - - - //////// Type & shape ///////// - - public Shape getType() { - return type; - } - - public void setType(Shape type) { - if (type == null) { - throw new IllegalArgumentException("setType called with null argument"); - } - if (this.type == type) - return; - this.type = type; - this.clipped = type.isClippable(); - this.shapeParameter = type.defaultParameter(); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public double getShapeParameter() { - return shapeParameter; - } - - public void setShapeParameter(double n) { - if (shapeParameter == n) - return; - this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter()); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public boolean isClipped() { - if (!type.isClippable()) - return false; - return clipped; - } - - public void setClipped(boolean c) { - if (clipped == c) - return; - clipped = c; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public boolean isClippedEnabled() { - return type.isClippable(); - } - - public double getShapeParameterMin() { - return type.minParameter(); - } - - public double getShapeParameterMax() { - return type.maxParameter(); - } - - - //////// Shoulders //////// - - public double getForeShoulderRadius() { - return foreShoulderRadius; - } - - public void setForeShoulderRadius(double foreShoulderRadius) { - if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius)) - return; - this.foreShoulderRadius = foreShoulderRadius; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public double getForeShoulderThickness() { - return foreShoulderThickness; - } - - public void setForeShoulderThickness(double foreShoulderThickness) { - if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness)) - return; - this.foreShoulderThickness = foreShoulderThickness; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public double getForeShoulderLength() { - return foreShoulderLength; - } - - public void setForeShoulderLength(double foreShoulderLength) { - if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength)) - return; - this.foreShoulderLength = foreShoulderLength; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public boolean isForeShoulderCapped() { - return foreShoulderCapped; - } - - public void setForeShoulderCapped(boolean capped) { - if (this.foreShoulderCapped == capped) - return; - this.foreShoulderCapped = capped; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - public double getAftShoulderRadius() { - return aftShoulderRadius; - } - - public void setAftShoulderRadius(double aftShoulderRadius) { - if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius)) - return; - this.aftShoulderRadius = aftShoulderRadius; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public double getAftShoulderThickness() { - return aftShoulderThickness; - } - - public void setAftShoulderThickness(double aftShoulderThickness) { - if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness)) - return; - this.aftShoulderThickness = aftShoulderThickness; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public double getAftShoulderLength() { - return aftShoulderLength; - } - - public void setAftShoulderLength(double aftShoulderLength) { - if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength)) - return; - this.aftShoulderLength = aftShoulderLength; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - public boolean isAftShoulderCapped() { - return aftShoulderCapped; - } - - public void setAftShoulderCapped(boolean capped) { - if (this.aftShoulderCapped == capped) - return; - this.aftShoulderCapped = capped; - fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); - } - - - - - /////////// Shape implementations //////////// - - - - /** - * Return the radius at point x of the transition. - */ - @Override - public double getRadius(double x) { - if (x < 0 || x > length) - return 0; - - double r1 = getForeRadius(); - double r2 = getAftRadius(); - - if (r1 == r2) - return r1; - - if (r1 > r2) { - x = length - x; - double tmp = r1; - r1 = r2; - r2 = tmp; - } - - if (isClipped()) { - // Check clip calculation - if (clipLength < 0) - calculateClip(r1, r2); - return type.getRadius(clipLength + x, r2, clipLength + length, shapeParameter); - } else { - // Not clipped - return r1 + type.getRadius(x, r2 - r1, length, shapeParameter); - } - } - - /** - * Numerically solve clipLength from the equation - * r1 == type.getRadius(clipLength,r2,clipLength+length) - * using a binary search. It assumes getOuterRadius() to be monotonically increasing. - */ - private void calculateClip(double r1, double r2) { - double min = 0, max = length; - - if (r1 >= r2) { - double tmp = r1; - r1 = r2; - r2 = tmp; - } - - if (r1 == 0) { - clipLength = 0; - return; - } - - if (length <= 0) { - clipLength = 0; - return; - } - - // Required: - // getR(min,min+length,r2) - r1 < 0 - // getR(max,max+length,r2) - r1 > 0 - - int n = 0; - while (type.getRadius(max, r2, max + length, shapeParameter) - r1 < 0) { - min = max; - max *= 2; - n++; - if (n > 10) - break; - } - - while (true) { - clipLength = (min + max) / 2; - if ((max - min) < CLIP_PRECISION) - return; - double val = type.getRadius(clipLength, r2, clipLength + length, shapeParameter); - if (val - r1 > 0) { - max = clipLength; - } else { - min = clipLength; - } - } - } - - - @Override - public double getInnerRadius(double x) { - return Math.max(getRadius(x) - thickness, 0); - } - - - - @Override - public Collection<Coordinate> getComponentBounds() { - Collection<Coordinate> bounds = super.getComponentBounds(); - if (foreShoulderLength > 0.001) - addBound(bounds, -foreShoulderLength, foreShoulderRadius); - if (aftShoulderLength > 0.001) - addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius); - return bounds; - } - - @Override - public double getComponentMass() { - double mass = super.getComponentMass(); - if (getForeShoulderLength() > 0.001) { - final double or = getForeShoulderRadius(); - final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); - mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity()); - } - if (isForeShoulderCapped()) { - final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); - mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity()); - } - - if (getAftShoulderLength() > 0.001) { - final double or = getAftShoulderRadius(); - final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); - mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity()); - } - if (isAftShoulderCapped()) { - final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); - mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity()); - } - - return mass; - } - - @Override - public Coordinate getComponentCG() { - Coordinate cg = super.getComponentCG(); - if (getForeShoulderLength() > 0.001) { - final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); - cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0, - getMaterial().getDensity())); - } - if (isForeShoulderCapped()) { - final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0); - cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(), - getForeShoulderThickness() - getForeShoulderLength(), - getMaterial().getDensity())); - } - - if (getAftShoulderLength() > 0.001) { - final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); - cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(), - getLength() + getAftShoulderLength(), getMaterial().getDensity())); - } - if (isAftShoulderCapped()) { - final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0); - cg = cg.average(ringCG(ir, 0, - getLength() + getAftShoulderLength() - getAftShoulderThickness(), - getLength() + getAftShoulderLength(), getMaterial().getDensity())); - } - return cg; - } - - - /* - * The moments of inertia are not explicitly corrected for the shoulders. - * However, since the mass is corrected, the inertia is automatically corrected - * to very nearly the correct value. - */ - - - - /** - * Returns the name of the component ("Transition"). - */ - @Override - public String getComponentName() { - //// Transition - return trans.get("Transition.Transition"); - } - - @Override - protected void componentChanged(ComponentChangeEvent e) { - super.componentChanged(e); - clipLength = -1; - } - - /** - * Check whether the given type can be added to this component. Transitions allow any - * InternalComponents to be added. - * - * @param ctype The RocketComponent class type to add. - * @return Whether such a component can be added. - */ - @Override - public boolean isCompatible(Class<? extends RocketComponent> ctype) { - if (InternalComponent.class.isAssignableFrom(ctype)) - return true; - return false; - } - - - - /** - * An enumeration listing the possible shapes of transitions. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public static enum Shape { - - /** - * Conical shape. - */ - //// Conical - CONICAL(trans.get("Shape.Conical"), - //// A conical nose cone has a profile of a triangle. - trans.get("Shape.Conical.desc1"), - //// A conical transition has straight sides. - trans.get("Shape.Conical.desc2")) { - @Override - public double getRadius(double x, double radius, double length, double param) { - assert x >= 0; - assert x <= length; - assert radius >= 0; - return radius * x / length; - } - }, - - /** - * Ogive shape. The shape parameter is the portion of an extended tangent ogive - * that will be used. That is, for param==1 a tangent ogive will be produced, and - * for smaller values the shape straightens out into a cone at param==0. - */ - //// Ogive - OGIVE(trans.get("Shape.Ogive"), - //// An ogive nose cone has a profile that is a segment of a circle. The shape parameter value 1 produces a <b>tangent ogive</b>, which has a smooth transition to the body tube, values less than 1 produce <b>secant ogives</b>. - trans.get("Shape.Ogive.desc1"), - //// An ogive transition has a profile that is a segment of a circle. The shape parameter value 1 produces a <b>tangent ogive</b>, which has a smooth transition to the body tube at the aft end, values less than 1 produce <b>secant ogives</b>. - trans.get("Shape.Ogive.desc2")) { - @Override - public boolean usesParameter() { - return true; // Range 0...1 is default - } - - @Override - public double defaultParameter() { - return 1.0; // Tangent ogive by default - } - - @Override - public double getRadius(double x, double radius, double length, double param) { - assert x >= 0; - assert x <= length; - assert radius >= 0; - assert param >= 0; - assert param <= 1; - - // Impossible to calculate ogive for length < radius, scale instead - // TODO: LOW: secant ogive could be calculated lower - if (length < radius) { - x = x * radius / length; - length = radius; - } - - if (param < 0.001) - return CONICAL.getRadius(x, radius, length, param); - - // Radius of circle is: - double R = MathUtil.safeSqrt((pow2(length) + pow2(radius)) * - (pow2((2 - param) * length) + pow2(param * radius)) / (4 * pow2(param * radius))); - double L = length / param; - // double R = (radius + length*length/(radius*param*param))/2; - double y0 = MathUtil.safeSqrt(R * R - L * L); - return MathUtil.safeSqrt(R * R - (L - x) * (L - x)) - y0; - } - }, - - /** - * Ellipsoidal shape. - */ - //// Ellipsoid - ELLIPSOID(trans.get("Shape.Ellipsoid"), - //// An ellipsoidal nose cone has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. - trans.get("Shape.Ellipsoid.desc1"), - //// An ellipsoidal transition has a profile of a half-ellipse with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the transition is not clipped, then the profile is extended at the center by the corresponding radius. - trans.get("Shape.Ellipsoid.desc2"), true) { - @Override - public double getRadius(double x, double radius, double length, double param) { - assert x >= 0; - assert x <= length; - assert radius >= 0; - x = x * radius / length; - return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere - } - }, - - //// Power series - POWER(trans.get("Shape.Powerseries"), - trans.get("Shape.Powerseries.desc1"), - trans.get("Shape.Powerseries.desc2"), true) { - @Override - public boolean usesParameter() { // Range 0...1 - return true; - } - - @Override - public double defaultParameter() { - return 0.5; - } - - @Override - public double getRadius(double x, double radius, double length, double param) { - assert x >= 0; - assert x <= length; - assert radius >= 0; - assert param >= 0; - assert param <= 1; - if (param <= 0.00001) { - if (x <= 0.00001) - return 0; - else - return radius; - } - return radius * Math.pow(x / length, param); - } - - }, - - //// Parabolic series - PARABOLIC(trans.get("Shape.Parabolicseries"), - ////A parabolic series nose cone has a profile of a parabola. The shape parameter defines the segment of the parabola to utilize. The shape parameter 1.0 produces a <b>full parabola</b> which is tangent to the body tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a <b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone. - trans.get("Shape.Parabolicseries.desc1"), - ////A parabolic series transition has a profile of a parabola. The shape parameter defines the segment of the parabola to utilize. The shape parameter 1.0 produces a <b>full parabola</b> which is tangent to the body tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a <b>1/2 parabola</b> and 0 produces a <b>conical</b> transition. - trans.get("Shape.Parabolicseries.desc2")) { - - // In principle a parabolic transition is clippable, but the difference is - // negligible. - - @Override - public boolean usesParameter() { // Range 0...1 - return true; - } - - @Override - public double defaultParameter() { - return 1.0; - } - - @Override - public double getRadius(double x, double radius, double length, double param) { - assert x >= 0; - assert x <= length; - assert radius >= 0; - assert param >= 0; - assert param <= 1; - - return radius * ((2 * x / length - param * pow2(x / length)) / (2 - param)); - } - }, - - //// Haack series - HAACK(trans.get("Shape.Haackseries"), - //// The Haack series nose cones are designed to minimize drag. The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes drag for fixed length and diameter, while a value of 0.333 produces an <b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume. - trans.get("Shape.Haackseries.desc1"), - //// The Haack series <i>nose cones</i> are designed to minimize drag. These transition shapes are their equivalents, but do not necessarily produce optimal drag for transitions. The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, while a value of 0.333 produces an <b>LV-Haack</b> shape. - trans.get("Shape.Haackseries.desc2"), true) { - - @Override - public boolean usesParameter() { - return true; - } - - @Override - public double maxParameter() { - return 1.0 / 3.0; // Range 0...1/3 - } - - @Override - public double getRadius(double x, double radius, double length, double param) { - assert x >= 0; - assert x <= length; - assert radius >= 0; - assert param >= 0; - assert param <= 2; - - double theta = Math.acos(1 - 2 * x / length); - if (MathUtil.equals(param, 0)) { - return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI); - } - return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI); - } - }, - - // POLYNOMIAL("Smooth polynomial", - // "A polynomial is fitted such that the nose cone profile is horizontal "+ - // "at the aft end of the transition. The angle at the tip is defined by "+ - // "the shape parameter.", - // "A polynomial is fitted such that the transition profile is horizontal "+ - // "at the aft end of the transition. The angle at the fore end is defined "+ - // "by the shape parameter.") { - // @Override - // public boolean usesParameter() { - // return true; - // } - // @Override - // public double maxParameter() { - // return 3.0; // Range 0...3 - // } - // @Override - // public double defaultParameter() { - // return 0.0; - // } - // public double getRadius(double x, double radius, double length, double param) { - // assert x >= 0; - // assert x <= length; - // assert radius >= 0; - // assert param >= 0; - // assert param <= 3; - // // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x - // x = x/length; - // return radius*((((param-2)*x + (3-2*param))*x + param)*x); - // } - // } - ; - - // Privete fields of the shapes - private final String name; - private final String transitionDesc; - private final String noseconeDesc; - private final boolean canClip; - - // Non-clippable constructor - Shape(String name, String noseconeDesc, String transitionDesc) { - this(name, noseconeDesc, transitionDesc, false); - } - - // Clippable constructor - Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) { - this.name = name; - this.canClip = canClip; - this.noseconeDesc = noseconeDesc; - this.transitionDesc = transitionDesc; - } - - - /** - * Return the name of the transition shape name. - */ - public String getName() { - return name; - } - - /** - * Get a description of the Transition shape. - */ - public String getTransitionDescription() { - return transitionDesc; - } - - /** - * Get a description of the NoseCone shape. - */ - public String getNoseConeDescription() { - return noseconeDesc; - } - - /** - * Check whether the shape differs in clipped mode. The clipping should be - * enabled by default if possible. - */ - public boolean isClippable() { - return canClip; - } - - /** - * Return whether the shape uses the shape parameter. (Default false.) - */ - public boolean usesParameter() { - return false; - } - - /** - * Return the minimum value of the shape parameter. (Default 0.) - */ - public double minParameter() { - return 0.0; - } - - /** - * Return the maximum value of the shape parameter. (Default 1.) - */ - public double maxParameter() { - return 1.0; - } - - /** - * Return the default value of the shape parameter. (Default 0.) - */ - public double defaultParameter() { - return 0.0; - } - - /** - * Calculate the basic radius of a transition with the given radius, length and - * shape parameter at the point x from the tip of the component. It is assumed - * that the fore radius if zero and the aft radius is <code>radius >= 0</code>. - * Boattails are achieved by reversing the component. - * - * @param x Position from the tip of the component. - * @param radius Aft end radius >= 0. - * @param length Length of the transition >= 0. - * @param param Valid shape parameter. - * @return The basic radius at the given position. - */ - public abstract double getRadius(double x, double radius, double length, double param); - - - /** - * Returns the name of the shape (same as getName()). - */ - @Override - public String toString() { - return name; - } - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java b/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java deleted file mode 100644 index 24c773e5..00000000 --- a/src/net/sf/openrocket/rocketcomponent/TrapezoidFinSet.java +++ /dev/null @@ -1,185 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -import java.util.ArrayList; -import java.util.List; - -/** - * A set of trapezoidal fins. The root and tip chords are perpendicular to the rocket - * base line, while the leading and aft edges may be slanted. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class TrapezoidFinSet extends FinSet { - private static final Translator trans = Application.getTranslator(); - - public static final double MAX_SWEEP_ANGLE = (89 * Math.PI / 180.0); - - /* - * sweep tipChord - * | |___________ - * | / | - * | / | - * | / | height - * / | - * __________/________________|_____________ - * length - * == rootChord - */ - - // rootChord == length - private double tipChord = 0; - private double height = 0; - private double sweep = 0; - - - public TrapezoidFinSet() { - this(3, 0.05, 0.05, 0.025, 0.03); - } - - // TODO: HIGH: height=0 -> CP = NaN - public TrapezoidFinSet(int fins, double rootChord, double tipChord, double sweep, - double height) { - super(); - - this.setFinCount(fins); - this.length = rootChord; - this.tipChord = tipChord; - this.sweep = sweep; - this.height = height; - } - - - public void setFinShape(double rootChord, double tipChord, double sweep, double height, - double thickness) { - if (this.length == rootChord && this.tipChord == tipChord && this.sweep == sweep && - this.height == height && this.thickness == thickness) - return; - this.length = rootChord; - this.tipChord = tipChord; - this.sweep = sweep; - this.height = height; - this.thickness = thickness; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public double getRootChord() { - return length; - } - - public void setRootChord(double r) { - if (length == r) - return; - length = Math.max(r, 0); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - public double getTipChord() { - return tipChord; - } - - public void setTipChord(double r) { - if (tipChord == r) - return; - tipChord = Math.max(r, 0); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - /** - * Get the sweep length. - */ - public double getSweep() { - return sweep; - } - - /** - * Set the sweep length. - */ - public void setSweep(double r) { - if (sweep == r) - return; - sweep = r; - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - /** - * Get the sweep angle. This is calculated from the true sweep and height, and is not - * stored separately. - */ - public double getSweepAngle() { - if (height == 0) { - if (sweep > 0) - return Math.PI / 2; - if (sweep < 0) - return -Math.PI / 2; - return 0; - } - return Math.atan(sweep / height); - } - - /** - * Sets the sweep by the sweep angle. The sweep is calculated and set by this method, - * and the angle itself is not stored. - */ - public void setSweepAngle(double r) { - if (r > MAX_SWEEP_ANGLE) - r = MAX_SWEEP_ANGLE; - if (r < -MAX_SWEEP_ANGLE) - r = -MAX_SWEEP_ANGLE; - double sweep = Math.tan(r) * height; - if (Double.isNaN(sweep) || Double.isInfinite(sweep)) - return; - setSweep(sweep); - } - - public double getHeight() { - return height; - } - - public void setHeight(double r) { - if (height == r) - return; - height = Math.max(r, 0); - fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); - } - - - - /** - * Returns the geometry of a trapezoidal fin. - */ - @Override - public Coordinate[] getFinPoints() { - List<Coordinate> list = new ArrayList<Coordinate>(4); - - list.add(Coordinate.NUL); - list.add(new Coordinate(sweep, height)); - if (tipChord > 0.0001) { - list.add(new Coordinate(sweep + tipChord, height)); - } - list.add(new Coordinate(MathUtil.max(length, 0.0001), 0)); - - return list.toArray(new Coordinate[list.size()]); - } - - /** - * Returns the span of a trapezoidal fin. - */ - @Override - public double getSpan() { - return height; - } - - - @Override - public String getComponentName() { - //// Trapezoidal fin set - return trans.get("TrapezoidFinSet.TrapezoidFinSet"); - } - -} diff --git a/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java b/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java deleted file mode 100644 index 5e22ec0c..00000000 --- a/src/net/sf/openrocket/rocketcomponent/TubeCoupler.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.sf.openrocket.rocketcomponent; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - - -public class TubeCoupler extends ThicknessRingComponent implements RadialParent { - private static final Translator trans = Application.getTranslator(); - - public TubeCoupler() { - setOuterRadiusAutomatic(true); - setThickness(0.002); - setLength(0.06); - } - - - // Make setter visible - @Override - public void setOuterRadiusAutomatic(boolean auto) { - super.setOuterRadiusAutomatic(auto); - } - - - @Override - public String getComponentName() { - //// Tube coupler - return trans.get("TubeCoupler.TubeCoupler"); - } - - @Override - public boolean allowsChildren() { - return true; - } - - /** - * Allow all InternalComponents to be added to this component. - */ - @Override - public boolean isCompatible(Class<? extends RocketComponent> type) { - return InternalComponent.class.isAssignableFrom(type); - } - - - @Override - public double getInnerRadius(double x) { - return getInnerRadius(); - } - - - @Override - public double getOuterRadius(double x) { - return getOuterRadius(); - } -} diff --git a/src/net/sf/openrocket/rocketcomponent/Visitable.java b/src/net/sf/openrocket/rocketcomponent/Visitable.java deleted file mode 100644 index 5f46b7ff..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Visitable.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Visitable.java - */ -package net.sf.openrocket.rocketcomponent; - -/** - * This interface describes a portion of the Visitor pattern, using generics to assure type-safety. - * The elements of the concrete object hierarchy are only visitable by an associated hierarchy of visitors, - * while these visitors are only able to visit the elements of that hierarchy. - * - * The key concept regarding the Visitor pattern is to realize that Java will only "discriminate" the type of an - * object being called, not the type of an object being passed. - * - * In order for the type of two objects to be determinable to the JVM, each object must be the receiver of an - * invocation. Here, when accept is called on a Visitable, the concrete type of the Visitable becomes "known" but the - * concrete type of the argument is still unknown. <code>visit</code> is then called on the parameter object, passing - * the Visitable back, which has type and identity. Flow of control has now been 'double-dispatched' such that the - * type (and identity) of both objects are known. - * - * Specifically, this interface is to be implemented by every class in the RocketComponent hierarchy that - * can be visited AND which are sufficiently specialized from their super class. If they only provide - * constraints to their superclass (such as TubeCoupler), then the implementation of this interface at - * the superclass level is sufficient. - * - * Admittedly, the syntax is a bit contorted here, as it is necessarily self-referential for type-safety. - * - * <V> The visitor type - * <T> The visitable (the concrete class that implements this interface) - */ -public interface Visitable<V extends Visitor<V, T>, T extends Visitable<V, T>> { - - /** - * Any class in the hierarchy that allows itself to be visited will implement this method. The normal - * behavior is that the visitor will invoke this method of a Visitable, passing itself. The Visitable - * turns around calls the Visitor back. This idiom is also known as 'double-dispatching'. - * - * @param visitor the visitor that will be called back - */ - public void accept(V visitor); - -} diff --git a/src/net/sf/openrocket/rocketcomponent/Visitor.java b/src/net/sf/openrocket/rocketcomponent/Visitor.java deleted file mode 100644 index edb39f2b..00000000 --- a/src/net/sf/openrocket/rocketcomponent/Visitor.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Visitor.java - */ -package net.sf.openrocket.rocketcomponent; - -/** - * This interface describes a portion of the Visitor pattern, using generics to assure type-safety. - * The elements of the concrete object hierarchy are only visitable by an associated hierarchy of visitors, - * while these visitors are only able to visit the elements of that hierarchy. - * - * The key concept regarding the Visitor pattern is to realize that Java will only "discriminate" the type of an - * object being called, not the type of an object being passed. - * - * In order for the type of two objects to be determinable to the JVM, each object must be the receiver of an - * invocation. Here, when accept is called on a Visitable, the concrete type of the Visitable becomes "known" but the - * concrete type of the argument is still unknown. <code>visit</code> is then called on the parameter object, passing - * the Visitable back, which has type and identity. Flow of control has now been 'double-dispatched' such that the - * type (and identity) of both objects are known. - * - * Specifically, this interface is to be implemented by every class in the RocketComponent hierarchy that - * can be visited AND which are sufficiently specialized from their super class. If they only provide - * constraints to their superclass (such as TubeCoupler), then the implementation of this interface at - * the superclass level is sufficient. - * - * Admittedly, the syntax is a bit contorted here, as it is necessarily self-referential for type-safety. - * - * <V> The visitor type (the concrete class that implements this interface) - * <T> The visitable - */ -public interface Visitor<V extends Visitor<V, T>, T extends Visitable<V, T>> { - - /** - * The callback method. This method is the 2nd leg of the double-dispatch, having been invoked from a - * corresponding <code>accept</code>. - * - * @param visitable the instance of the Visitable (the target of what is being visiting) - */ - void visit(T visitable); -} \ No newline at end of file diff --git a/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java b/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java deleted file mode 100644 index 85be9120..00000000 --- a/src/net/sf/openrocket/simulation/AbstractSimulationStepper.java +++ /dev/null @@ -1,232 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.motor.MotorId; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.motor.MotorInstanceConfiguration; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Quaternion; - -public abstract class AbstractSimulationStepper implements SimulationStepper { - - /** - * Compute the atmospheric conditions, allowing listeners to override. - * - * @param status the simulation status - * @return the atmospheric conditions to use - * @throws SimulationException if a listener throws SimulationException - */ - protected AtmosphericConditions modelAtmosphericConditions(SimulationStatus status) throws SimulationException { - AtmosphericConditions conditions; - - // Call pre-listener - conditions = SimulationListenerHelper.firePreAtmosphericModel(status); - if (conditions != null) { - return conditions; - } - - // Compute conditions - double altitude = status.getRocketPosition().z + status.getSimulationConditions().getLaunchSite().getAltitude(); - conditions = status.getSimulationConditions().getAtmosphericModel().getConditions(altitude); - - // Call post-listener - conditions = SimulationListenerHelper.firePostAtmosphericModel(status, conditions); - - checkNaN(conditions.getPressure()); - checkNaN(conditions.getTemperature()); - - return conditions; - } - - - - /** - * Compute the wind to use, allowing listeners to override. - * - * @param status the simulation status - * @return the wind conditions to use - * @throws SimulationException if a listener throws SimulationException - */ - protected Coordinate modelWindVelocity(SimulationStatus status) throws SimulationException { - Coordinate wind; - - // Call pre-listener - wind = SimulationListenerHelper.firePreWindModel(status); - if (wind != null) { - return wind; - } - - // Compute conditions - double altitude = status.getRocketPosition().z + status.getSimulationConditions().getLaunchSite().getAltitude(); - wind = status.getSimulationConditions().getWindModel().getWindVelocity(status.getSimulationTime(), altitude); - - // Call post-listener - wind = SimulationListenerHelper.firePostWindModel(status, wind); - - checkNaN(wind); - - return wind; - } - - - - /** - * Compute the gravity to use, allowing listeners to override. - * - * @param status the simulation status - * @return the gravitational acceleration to use - * @throws SimulationException if a listener throws SimulationException - */ - protected double modelGravity(SimulationStatus status) throws SimulationException { - double gravity; - - // Call pre-listener - gravity = SimulationListenerHelper.firePreGravityModel(status); - if (!Double.isNaN(gravity)) { - return gravity; - } - - // Compute conditions - gravity = status.getSimulationConditions().getGravityModel().getGravity(status.getRocketWorldPosition()); - - // Call post-listener - gravity = SimulationListenerHelper.firePostGravityModel(status, gravity); - - checkNaN(gravity); - - return gravity; - } - - - - /** - * Compute the mass data to use, allowing listeners to override. - * - * @param status the simulation status - * @return the mass data to use - * @throws SimulationException if a listener throws SimulationException - */ - protected MassData calculateMassData(SimulationStatus status) throws SimulationException { - MassData mass; - Coordinate cg; - double longitudinalInertia, rotationalInertia; - - // Call pre-listener - mass = SimulationListenerHelper.firePreMassCalculation(status); - if (mass != null) { - return mass; - } - - MassCalculator calc = status.getSimulationConditions().getMassCalculator(); - cg = calc.getCG(status.getConfiguration(), status.getMotorConfiguration()); - longitudinalInertia = calc.getLongitudinalInertia(status.getConfiguration(), status.getMotorConfiguration()); - rotationalInertia = calc.getRotationalInertia(status.getConfiguration(), status.getMotorConfiguration()); - mass = new MassData(cg, longitudinalInertia, rotationalInertia); - - // Call post-listener - mass = SimulationListenerHelper.firePostMassCalculation(status, mass); - - checkNaN(mass.getCG()); - checkNaN(mass.getLongitudinalInertia()); - checkNaN(mass.getRotationalInertia()); - - return mass; - } - - - - - - /** - * Calculate the average thrust produced by the motors in the current configuration, allowing - * listeners to override. The average is taken between <code>status.time</code> and - * <code>status.time + timestep</code>. - * <p> - * TODO: HIGH: This method does not take into account any moments generated by off-center motors. - * - * @param status the current simulation status. - * @param timestep the time step of the current iteration. - * @param acceleration the current (approximate) acceleration - * @param atmosphericConditions the current atmospheric conditions - * @param stepMotors whether to step the motors forward or work on a clone object - * @return the average thrust during the time step. - */ - protected double calculateThrust(SimulationStatus status, double timestep, - double acceleration, AtmosphericConditions atmosphericConditions, - boolean stepMotors) throws SimulationException { - double thrust; - - // Pre-listeners - thrust = SimulationListenerHelper.firePreThrustCalculation(status); - if (!Double.isNaN(thrust)) { - return thrust; - } - - Configuration configuration = status.getConfiguration(); - - // Iterate over the motors and calculate combined thrust - MotorInstanceConfiguration mic = status.getMotorConfiguration(); - if (!stepMotors) { - mic = mic.clone(); - } - mic.step(status.getSimulationTime() + timestep, acceleration, atmosphericConditions); - thrust = 0; - for (MotorId id : mic.getMotorIDs()) { - if (configuration.isComponentActive((RocketComponent) mic.getMotorMount(id))) { - MotorInstance motor = mic.getMotorInstance(id); - thrust += motor.getThrust(); - } - } - - // Post-listeners - thrust = SimulationListenerHelper.firePostThrustCalculation(status, thrust); - - checkNaN(thrust); - - return thrust; - } - - - /** - * Check that the provided value is not NaN. - * - * @param d the double value to check. - * @throws BugException if the value is NaN. - */ - protected void checkNaN(double d) { - if (Double.isNaN(d)) { - throw new BugException("Simulation resulted in not-a-number (NaN) value, please report a bug."); - } - } - - /** - * Check that the provided coordinate is not NaN. - * - * @param c the coordinate value to check. - * @throws BugException if the value is NaN. - */ - protected void checkNaN(Coordinate c) { - if (c.isNaN()) { - throw new BugException("Simulation resulted in not-a-number (NaN) value, please report a bug, c=" + c); - } - } - - - /** - * Check that the provided quaternion is not NaN. - * - * @param q the quaternion value to check. - * @throws BugException if the value is NaN. - */ - protected void checkNaN(Quaternion q) { - if (q.isNaN()) { - throw new BugException("Simulation resulted in not-a-number (NaN) value, please report a bug, q=" + q); - } - } -} diff --git a/src/net/sf/openrocket/simulation/AccelerationData.java b/src/net/sf/openrocket/simulation/AccelerationData.java deleted file mode 100644 index 31d19ba9..00000000 --- a/src/net/sf/openrocket/simulation/AccelerationData.java +++ /dev/null @@ -1,91 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Quaternion; - -public final class AccelerationData { - - private Coordinate linearAccelerationRC; - private Coordinate rotationalAccelerationRC; - private Coordinate linearAccelerationWC; - private Coordinate rotationalAccelerationWC; - // Rotates from rocket coordinates to world coordinates - private final Quaternion rotation; - - - public AccelerationData(Coordinate linearAccelerationRC, Coordinate rotationalAccelerationRC, - Coordinate linearAccelerationWC, Coordinate rotationalAccelerationWC, - Quaternion rotation) { - - if ((linearAccelerationRC == null && linearAccelerationWC == null) || - (rotationalAccelerationRC == null && rotationalAccelerationWC == null) || - rotation == null) { - throw new IllegalArgumentException("Parameter is null: " + - " linearAccelerationRC=" + linearAccelerationRC + - " linearAccelerationWC=" + linearAccelerationWC + - " rotationalAccelerationRC=" + rotationalAccelerationRC + - " rotationalAccelerationWC=" + rotationalAccelerationWC + - " rotation=" + rotation); - } - this.linearAccelerationRC = linearAccelerationRC; - this.rotationalAccelerationRC = rotationalAccelerationRC; - this.linearAccelerationWC = linearAccelerationWC; - this.rotationalAccelerationWC = rotationalAccelerationWC; - this.rotation = rotation; - } - - - - - public Coordinate getLinearAccelerationRC() { - if (linearAccelerationRC == null) { - linearAccelerationRC = rotation.invRotate(linearAccelerationWC); - } - return linearAccelerationRC; - } - - public Coordinate getRotationalAccelerationRC() { - if (rotationalAccelerationRC == null) { - rotationalAccelerationRC = rotation.invRotate(rotationalAccelerationWC); - } - return rotationalAccelerationRC; - } - - public Coordinate getLinearAccelerationWC() { - if (linearAccelerationWC == null) { - linearAccelerationWC = rotation.rotate(linearAccelerationRC); - } - return linearAccelerationWC; - } - - public Coordinate getRotationalAccelerationWC() { - if (rotationalAccelerationWC == null) { - rotationalAccelerationWC = rotation.rotate(rotationalAccelerationRC); - } - return rotationalAccelerationWC; - } - - public Quaternion getRotation() { - return rotation; - } - - - - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof AccelerationData)) - return false; - AccelerationData other = (AccelerationData) obj; - return (this.getLinearAccelerationRC().equals(other.getLinearAccelerationRC()) && - this.getRotationalAccelerationRC().equals(other.getRotationalAccelerationRC())); - } - - @Override - public int hashCode() { - return getLinearAccelerationRC().hashCode() ^ getRotationalAccelerationRC().hashCode(); - } - -} diff --git a/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java deleted file mode 100644 index 002fd62c..00000000 --- a/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ /dev/null @@ -1,572 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; - -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorId; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.motor.MotorInstanceConfiguration; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.simulation.exception.MotorIgnitionException; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.exception.SimulationLaunchException; -import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Pair; -import net.sf.openrocket.util.Quaternion; - - -public class BasicEventSimulationEngine implements SimulationEngine { - - private static final LogHelper log = Application.getLogger(); - - // TODO: MEDIUM: Allow selecting steppers - private SimulationStepper flightStepper = new RK4SimulationStepper(); - private SimulationStepper landingStepper = new BasicLandingStepper(); - - private SimulationStepper currentStepper; - - private SimulationStatus status; - - - @Override - public FlightData simulate(SimulationConditions simulationConditions) throws SimulationException { - Set<MotorId> motorBurntOut = new HashSet<MotorId>(); - - // Set up flight data - FlightData flightData = new FlightData(); - - // Set up rocket configuration - Configuration configuration = setupConfiguration(simulationConditions); - MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration); - if (motorConfiguration.getMotorIDs().isEmpty()) { - throw new MotorIgnitionException("No motors defined in the simulation."); - } - - // Initialize the simulation - currentStepper = flightStepper; - status = initialStatus(configuration, motorConfiguration, simulationConditions, flightData); - status = currentStepper.initialize(status); - - - SimulationListenerHelper.fireStartSimulation(status); - // Get originating position (in case listener has modified launch position) - Coordinate origin = status.getRocketPosition(); - Coordinate originVelocity = status.getRocketVelocity(); - - try { - double maxAlt = Double.NEGATIVE_INFINITY; - - // Start the simulation - while (handleEvents()) { - - // Take the step - double oldAlt = status.getRocketPosition().z; - - if (SimulationListenerHelper.firePreStep(status)) { - // Step at most to the next event - double maxStepTime = Double.MAX_VALUE; - FlightEvent nextEvent = status.getEventQueue().peek(); - if (nextEvent != null) { - maxStepTime = MathUtil.max(nextEvent.getTime() - status.getSimulationTime(), 0.001); - } - log.verbose("BasicEventSimulationEngine: Taking simulation step at t=" + status.getSimulationTime()); - currentStepper.step(status, maxStepTime); - } - SimulationListenerHelper.firePostStep(status); - - - // Check for NaN values in the simulation status - checkNaN(); - - // Add altitude event - addEvent(new FlightEvent(FlightEvent.Type.ALTITUDE, status.getSimulationTime(), - status.getConfiguration().getRocket(), - new Pair<Double, Double>(oldAlt, status.getRocketPosition().z))); - - if (status.getRocketPosition().z > maxAlt) { - maxAlt = status.getRocketPosition().z; - } - - - // Position relative to start location - Coordinate relativePosition = status.getRocketPosition().sub(origin); - - // Add appropriate events - if (!status.isLiftoff()) { - - // Avoid sinking into ground before liftoff - if (relativePosition.z < 0) { - status.setRocketPosition(origin); - status.setRocketVelocity(originVelocity); - } - // Detect lift-off - if (relativePosition.z > 0.02) { - addEvent(new FlightEvent(FlightEvent.Type.LIFTOFF, status.getSimulationTime())); - } - - } else { - - // Check ground hit after liftoff - if (status.getRocketPosition().z < 0) { - status.setRocketPosition(status.getRocketPosition().setZ(0)); - addEvent(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.getSimulationTime())); - addEvent(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); - } - - } - - // Check for launch guide clearance - if (!status.isLaunchRodCleared() && - relativePosition.length() > status.getSimulationConditions().getLaunchRodLength()) { - addEvent(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.getSimulationTime(), null)); - } - - - // Check for apogee - if (!status.isApogeeReached() && status.getRocketPosition().z < maxAlt - 0.01) { - addEvent(new FlightEvent(FlightEvent.Type.APOGEE, status.getSimulationTime(), - status.getConfiguration().getRocket())); - } - - - // Check for burnt out motors - for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) { - MotorInstance motor = status.getMotorConfiguration().getMotorInstance(motorId); - if (!motor.isActive() && motorBurntOut.add(motorId)) { - addEvent(new FlightEvent(FlightEvent.Type.BURNOUT, status.getSimulationTime(), - (RocketComponent) status.getMotorConfiguration().getMotorMount(motorId), motorId)); - } - } - - } - - } catch (SimulationException e) { - SimulationListenerHelper.fireEndSimulation(status, e); - throw e; - } - - SimulationListenerHelper.fireEndSimulation(status, null); - - flightData.addBranch(status.getFlightData()); - - if (!flightData.getWarningSet().isEmpty()) { - log.info("Warnings at the end of simulation: " + flightData.getWarningSet()); - } - - // TODO: HIGH: Simulate branches - return flightData; - } - - - - private SimulationStatus initialStatus(Configuration configuration, - MotorInstanceConfiguration motorConfiguration, - SimulationConditions simulationConditions, FlightData flightData) { - - SimulationStatus init = new SimulationStatus(); - init.setSimulationConditions(simulationConditions); - init.setConfiguration(configuration); - init.setMotorConfiguration(motorConfiguration); - - init.setSimulationTime(0); - init.setPreviousTimeStep(simulationConditions.getTimeStep()); - init.setRocketPosition(Coordinate.NUL); - init.setRocketVelocity(Coordinate.NUL); - init.setRocketWorldPosition(simulationConditions.getLaunchSite()); - - // Initialize to roll angle with least stability w.r.t. the wind - Quaternion o; - FlightConditions cond = new FlightConditions(configuration); - simulationConditions.getAerodynamicCalculator().getWorstCP(configuration, cond, null); - double angle = -cond.getTheta() - simulationConditions.getLaunchRodDirection(); - o = Quaternion.rotation(new Coordinate(0, 0, angle)); - - // Launch rod angle and direction - o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, simulationConditions.getLaunchRodAngle(), 0))); - o = o.multiplyLeft(Quaternion.rotation(new Coordinate(0, 0, simulationConditions.getLaunchRodDirection()))); - - init.setRocketOrientationQuaternion(o); - init.setRocketRotationVelocity(Coordinate.NUL); - - - /* - * Calculate the effective launch rod length taking into account launch lugs. - * If no lugs are found, assume a tower launcher of full length. - */ - double length = simulationConditions.getLaunchRodLength(); - double lugPosition = Double.NaN; - for (RocketComponent c : configuration) { - if (c instanceof LaunchLug) { - double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x; - if (Double.isNaN(lugPosition) || pos > lugPosition) { - lugPosition = pos; - } - } - } - if (!Double.isNaN(lugPosition)) { - double maxX = 0; - for (Coordinate c : configuration.getBounds()) { - if (c.x > maxX) - maxX = c.x; - } - if (maxX >= lugPosition) { - length = Math.max(0, length - (maxX - lugPosition)); - } - } - init.setEffectiveLaunchRodLength(length); - - - - init.setSimulationStartWallTime(System.nanoTime()); - - init.setMotorIgnited(false); - init.setLiftoff(false); - init.setLaunchRodCleared(false); - init.setApogeeReached(false); - - init.getEventQueue().add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulationConditions.getRocket())); - - init.setFlightData(new FlightDataBranch("MAIN", FlightDataType.TYPE_TIME)); - init.setWarnings(flightData.getWarningSet()); - - return init; - } - - - - /** - * Create a rocket configuration from the launch conditions. - * - * @param simulation the launch conditions. - * @return a rocket configuration with all stages attached. - */ - private Configuration setupConfiguration(SimulationConditions simulation) { - Configuration configuration = new Configuration(simulation.getRocket()); - configuration.setAllStages(); - configuration.setMotorConfigurationID(simulation.getMotorConfigurationID()); - - return configuration; - } - - - - /** - * Create a new motor instance configuration for the rocket configuration. - * - * @param configuration the rocket configuration. - * @return a new motor instance configuration with all motors in place. - */ - private MotorInstanceConfiguration setupMotorConfiguration(Configuration configuration) { - MotorInstanceConfiguration motors = new MotorInstanceConfiguration(); - final String motorId = configuration.getMotorConfigurationID(); - - Iterator<MotorMount> iterator = configuration.motorIterator(); - while (iterator.hasNext()) { - MotorMount mount = iterator.next(); - RocketComponent component = (RocketComponent) mount; - Motor motor = mount.getMotor(motorId); - - if (motor != null) { - Coordinate[] positions = component.toAbsolute(mount.getMotorPosition(motorId)); - for (int i = 0; i < positions.length; i++) { - Coordinate position = positions[i]; - MotorId id = new MotorId(component.getID(), i + 1); - motors.addMotor(id, motor.getInstance(), mount, position); - } - } - } - return motors; - } - - /** - * Handles events occurring during the flight from the event queue. - * Each event that has occurred before or at the current simulation time is - * processed. Suitable events are also added to the flight data. - */ - private boolean handleEvents() throws SimulationException { - boolean ret = true; - FlightEvent event; - - for (event = nextEvent(); event != null; event = nextEvent()) { - - // Call simulation listeners, allow aborting event handling - if (!SimulationListenerHelper.fireHandleFlightEvent(status, event)) { - continue; - } - - if (event.getType() != FlightEvent.Type.ALTITUDE) { - log.verbose("BasicEventSimulationEngine: Handling event " + event); - } - - if (event.getType() == FlightEvent.Type.IGNITION) { - MotorMount mount = (MotorMount) event.getSource(); - MotorId motorId = (MotorId) event.getData(); - MotorInstance instance = status.getMotorConfiguration().getMotorInstance(motorId); - if (!SimulationListenerHelper.fireMotorIgnition(status, motorId, mount, instance)) { - continue; - } - } - - if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) { - RecoveryDevice device = (RecoveryDevice) event.getSource(); - if (!SimulationListenerHelper.fireRecoveryDeviceDeployment(status, device)) { - continue; - } - } - - - - // Check for motor ignition events, add ignition events to queue - for (MotorId id : status.getMotorConfiguration().getMotorIDs()) { - MotorMount mount = status.getMotorConfiguration().getMotorMount(id); - RocketComponent component = (RocketComponent) mount; - - if (mount.getIgnitionEvent().isActivationEvent(event, component)) { - addEvent(new FlightEvent(FlightEvent.Type.IGNITION, - status.getSimulationTime() + mount.getIgnitionDelay(), - component, id)); - } - } - - - // Check for recovery device deployment, add events to queue - Iterator<RocketComponent> rci = status.getConfiguration().iterator(); - while (rci.hasNext()) { - RocketComponent c = rci.next(); - if (!(c instanceof RecoveryDevice)) - continue; - if (((RecoveryDevice) c).getDeployEvent().isActivationEvent(event, c)) { - // Delay event by at least 1ms to allow stage separation to occur first - addEvent(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT, - event.getTime() + Math.max(0.001, ((RecoveryDevice) c).getDeployDelay()), c)); - } - } - - - // Handle event - switch (event.getType()) { - - case LAUNCH: { - status.getFlightData().addEvent(event); - break; - } - - case IGNITION: { - // Ignite the motor - MotorMount mount = (MotorMount) event.getSource(); - RocketComponent component = (RocketComponent) mount; - MotorId motorId = (MotorId) event.getData(); - MotorInstanceConfiguration config = status.getMotorConfiguration(); - config.setMotorIgnitionTime(motorId, event.getTime()); - status.setMotorIgnited(true); - status.getFlightData().addEvent(event); - - // Add stage separation event if appropriate - int n = component.getStageNumber(); - if (n < component.getRocket().getStageCount() - 1) { - if (status.getConfiguration().isStageActive(n + 1)) { - addEvent(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, event.getTime(), - component.getStage())); - } - } - break; - } - - case LIFTOFF: { - // Mark lift-off as occurred - status.setLiftoff(true); - status.getFlightData().addEvent(event); - break; - } - - case LAUNCHROD: { - // Mark launch rod as cleared - status.setLaunchRodCleared(true); - status.getFlightData().addEvent(event); - break; - } - - case BURNOUT: { - // If motor burnout occurs without lift-off, abort - if (!status.isLiftoff()) { - throw new SimulationLaunchException("Motor burnout without liftoff."); - } - // Add ejection charge event - String id = status.getConfiguration().getMotorConfigurationID(); - MotorMount mount = (MotorMount) event.getSource(); - double delay = mount.getMotorDelay(id); - if (delay != Motor.PLUGGED) { - addEvent(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE, status.getSimulationTime() + delay, - event.getSource(), event.getData())); - } - status.getFlightData().addEvent(event); - break; - } - - case EJECTION_CHARGE: { - status.getFlightData().addEvent(event); - break; - } - - case STAGE_SEPARATION: { - // TODO: HIGH: Store lower stages to be simulated later - RocketComponent stage = event.getSource(); - int n = stage.getStageNumber(); - status.getConfiguration().setToStage(n); - status.getFlightData().addEvent(event); - break; - } - - case APOGEE: - // Mark apogee as reached - status.setApogeeReached(true); - status.getFlightData().addEvent(event); - break; - - case RECOVERY_DEVICE_DEPLOYMENT: - RocketComponent c = event.getSource(); - int n = c.getStageNumber(); - // Ignore event if stage not active - if (status.getConfiguration().isStageActive(n)) { - // TODO: HIGH: Check stage activeness for other events as well? - - // Check whether any motor in the active stages is active anymore - for (MotorId motorId : status.getMotorConfiguration().getMotorIDs()) { - int stage = ((RocketComponent) status.getMotorConfiguration(). - getMotorMount(motorId)).getStageNumber(); - if (!status.getConfiguration().isStageActive(stage)) - continue; - if (!status.getMotorConfiguration().getMotorInstance(motorId).isActive()) - continue; - status.getWarnings().add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING); - } - - // Check for launch rod - if (!status.isLaunchRodCleared()) { - status.getWarnings().add(Warning.fromString("Recovery device device deployed while on " + - "the launch guide.")); - } - - // Check current velocity - if (status.getRocketVelocity().length() > 20) { - // TODO: LOW: Custom warning. - status.getWarnings().add(Warning.fromString("Recovery device deployment at high " + - "speed (" - + UnitGroup.UNITS_VELOCITY.toStringUnit(status.getRocketVelocity().length()) - + ").")); - } - - status.setLiftoff(true); - status.getDeployedRecoveryDevices().add((RecoveryDevice) c); - - this.currentStepper = this.landingStepper; - this.status = currentStepper.initialize(status); - - status.getFlightData().addEvent(event); - } - break; - - case GROUND_HIT: - status.getFlightData().addEvent(event); - break; - - case SIMULATION_END: - ret = false; - status.getFlightData().addEvent(event); - break; - - case ALTITUDE: - break; - } - - } - - - // If no motor has ignited, abort - if (!status.isMotorIgnited()) { - throw new MotorIgnitionException("No motors ignited."); - } - - return ret; - } - - - /** - * Add a flight event to the event queue unless a listener aborts adding it. - * - * @param event the event to add to the queue. - */ - private void addEvent(FlightEvent event) throws SimulationException { - if (SimulationListenerHelper.fireAddFlightEvent(status, event)) { - status.getEventQueue().add(event); - } - } - - - - /** - * Return the next flight event to handle, or null if no more events should be handled. - * This method jumps the simulation time forward in case no motors have been ignited. - * The flight event is removed from the event queue. - * - * @param status the simulation status - * @return the flight event to handle, or null - */ - private FlightEvent nextEvent() { - EventQueue queue = status.getEventQueue(); - FlightEvent event = queue.peek(); - if (event == null) - return null; - - // Jump to event if no motors have been ignited - if (!status.isMotorIgnited() && event.getTime() > status.getSimulationTime()) { - status.setSimulationTime(event.getTime()); - } - if (event.getTime() <= status.getSimulationTime()) { - return queue.poll(); - } else { - return null; - } - } - - - - private void checkNaN() throws SimulationException { - double d = 0; - boolean b = false; - d += status.getSimulationTime(); - d += status.getPreviousTimeStep(); - b |= status.getRocketPosition().isNaN(); - b |= status.getRocketVelocity().isNaN(); - b |= status.getRocketOrientationQuaternion().isNaN(); - b |= status.getRocketRotationVelocity().isNaN(); - d += status.getEffectiveLaunchRodLength(); - - if (Double.isNaN(d) || b) { - log.error("Simulation resulted in NaN value:" + - " simulationTime=" + status.getSimulationTime() + - " previousTimeStep=" + status.getPreviousTimeStep() + - " rocketPosition=" + status.getRocketPosition() + - " rocketVelocity=" + status.getRocketVelocity() + - " rocketOrientationQuaternion=" + status.getRocketOrientationQuaternion() + - " rocketRotationVelocity=" + status.getRocketRotationVelocity() + - " effectiveLaunchRodLength=" + status.getEffectiveLaunchRodLength()); - throw new SimulationException("Simulation resulted in not-a-number (NaN) value, please report a bug."); - } - } - - -} diff --git a/src/net/sf/openrocket/simulation/BasicLandingStepper.java b/src/net/sf/openrocket/simulation/BasicLandingStepper.java deleted file mode 100644 index dc67e853..00000000 --- a/src/net/sf/openrocket/simulation/BasicLandingStepper.java +++ /dev/null @@ -1,138 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.GeodeticComputationStrategy; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.WorldCoordinate; - -public class BasicLandingStepper extends AbstractSimulationStepper { - - private static final double RECOVERY_TIME_STEP = 0.5; - - @Override - public SimulationStatus initialize(SimulationStatus status) throws SimulationException { - return status; - } - - @Override - public void step(SimulationStatus status, double maxTimeStep) throws SimulationException { - double totalCD = 0; - double refArea = status.getConfiguration().getReferenceArea(); - - // Get the atmospheric conditions - AtmosphericConditions atmosphere = modelAtmosphericConditions(status); - - //// Local wind speed and direction - Coordinate windSpeed = modelWindVelocity(status); - Coordinate airSpeed = status.getRocketVelocity().add(windSpeed); - - // Get total CD - double mach = airSpeed.length() / atmosphere.getMachSpeed(); - for (RecoveryDevice c : status.getDeployedRecoveryDevices()) { - totalCD += c.getCD(mach) * c.getArea() / refArea; - } - - // Compute drag force - double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2()); - double dragForce = totalCD * dynP * refArea; - MassData massData = calculateMassData(status); - double mass = massData.getCG().weight; - - - // Compute drag acceleration - Coordinate linearAcceleration; - if (airSpeed.length() > 0.001) { - linearAcceleration = airSpeed.normalize().multiply(-dragForce / mass); - } else { - linearAcceleration = Coordinate.NUL; - } - - // Add effect of gravity - double gravity = modelGravity(status); - linearAcceleration = linearAcceleration.sub(0, 0, gravity); - - - // Add coriolis acceleration - Coordinate coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation().getCoriolisAcceleration( - status.getRocketWorldPosition(), status.getRocketVelocity()); - linearAcceleration = linearAcceleration.add(coriolisAcceleration); - - - - // Select time step - double timeStep = MathUtil.min(0.5 / linearAcceleration.length(), RECOVERY_TIME_STEP); - - // Perform Euler integration - status.setRocketPosition(status.getRocketPosition().add(status.getRocketVelocity().multiply(timeStep)). - add(linearAcceleration.multiply(MathUtil.pow2(timeStep) / 2))); - status.setRocketVelocity(status.getRocketVelocity().add(linearAcceleration.multiply(timeStep))); - status.setSimulationTime(status.getSimulationTime() + timeStep); - - - // Update the world coordinate - WorldCoordinate w = status.getSimulationConditions().getLaunchSite(); - w = status.getSimulationConditions().getGeodeticComputation().addCoordinate(w, status.getRocketPosition()); - status.setRocketWorldPosition(w); - - - // Store data - FlightDataBranch data = status.getFlightData(); - boolean extra = status.getSimulationConditions().isCalculateExtras(); - data.addPoint(); - - data.setValue(FlightDataType.TYPE_TIME, status.getSimulationTime()); - data.setValue(FlightDataType.TYPE_ALTITUDE, status.getRocketPosition().z); - data.setValue(FlightDataType.TYPE_POSITION_X, status.getRocketPosition().x); - data.setValue(FlightDataType.TYPE_POSITION_Y, status.getRocketPosition().y); - if (extra) { - data.setValue(FlightDataType.TYPE_POSITION_XY, - MathUtil.hypot(status.getRocketPosition().x, status.getRocketPosition().y)); - data.setValue(FlightDataType.TYPE_POSITION_DIRECTION, - Math.atan2(status.getRocketPosition().y, status.getRocketPosition().x)); - - data.setValue(FlightDataType.TYPE_VELOCITY_XY, - MathUtil.hypot(status.getRocketVelocity().x, status.getRocketVelocity().y)); - data.setValue(FlightDataType.TYPE_ACCELERATION_XY, - MathUtil.hypot(linearAcceleration.x, linearAcceleration.y)); - - data.setValue(FlightDataType.TYPE_ACCELERATION_TOTAL, linearAcceleration.length()); - - double Re = airSpeed.length() * - status.getConfiguration().getLength() / - atmosphere.getKinematicViscosity(); - data.setValue(FlightDataType.TYPE_REYNOLDS_NUMBER, Re); - } - - - data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); - data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); - if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { - data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, coriolisAcceleration.length()); - } - - - data.setValue(FlightDataType.TYPE_VELOCITY_Z, status.getRocketVelocity().z); - data.setValue(FlightDataType.TYPE_ACCELERATION_Z, linearAcceleration.z); - - data.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, airSpeed.length()); - data.setValue(FlightDataType.TYPE_MACH_NUMBER, mach); - - data.setValue(FlightDataType.TYPE_MASS, mass); - - data.setValue(FlightDataType.TYPE_THRUST_FORCE, 0); - data.setValue(FlightDataType.TYPE_DRAG_FORCE, dragForce); - - data.setValue(FlightDataType.TYPE_WIND_VELOCITY, windSpeed.length()); - data.setValue(FlightDataType.TYPE_AIR_TEMPERATURE, atmosphere.getTemperature()); - data.setValue(FlightDataType.TYPE_AIR_PRESSURE, atmosphere.getPressure()); - data.setValue(FlightDataType.TYPE_SPEED_OF_SOUND, atmosphere.getMachSpeed()); - - data.setValue(FlightDataType.TYPE_TIME_STEP, timeStep); - data.setValue(FlightDataType.TYPE_COMPUTATION_TIME, - (System.nanoTime() - status.getSimulationStartWallTime()) / 1000000000.0); - } - -} diff --git a/src/net/sf/openrocket/simulation/EventQueue.java b/src/net/sf/openrocket/simulation/EventQueue.java deleted file mode 100644 index 1ac9fb82..00000000 --- a/src/net/sf/openrocket/simulation/EventQueue.java +++ /dev/null @@ -1,65 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.PriorityQueue; - -import net.sf.openrocket.util.Monitorable; - -/** - * A sorted queue of FlightEvent objects. This queue maintains the events in time order - * and also keeps a modification count for the queue. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class EventQueue extends PriorityQueue<FlightEvent> implements Monitorable { - - private int modID = 0; - - public EventQueue() { - super(); - } - - public EventQueue(PriorityQueue<? extends FlightEvent> c) { - super(c); - } - - @Override - public boolean add(FlightEvent e) { - modID++; - return super.add(e); - } - - @Override - public void clear() { - modID++; - super.clear(); - } - - @Override - public boolean offer(FlightEvent e) { - modID++; - return super.offer(e); - } - - @Override - public FlightEvent poll() { - modID++; - return super.poll(); - } - - @Override - public boolean remove(Object o) { - modID++; - return super.remove(o); - } - - public int getModID() { - return modID; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // TODO Auto-generated method stub - return super.clone(); - } - -} diff --git a/src/net/sf/openrocket/simulation/FlightData.java b/src/net/sf/openrocket/simulation/FlightData.java deleted file mode 100644 index 839c6584..00000000 --- a/src/net/sf/openrocket/simulation/FlightData.java +++ /dev/null @@ -1,309 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Mutable; - -/** - * A collection of various flight data. This is the result of a simulation, or importing - * data into the software. The data includes: - * <ul> - * <li>A number of generally interesting values of a simulation, such as max. altitude and velocity - * <li>A number (or zero) of flight data branches containing the actual data - * <li>A WarningSet including warnings that occurred during simulation - * </ul> - * <p> - * A FlightData object can be made immutable by calling {@link #immute()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FlightData { - private static final LogHelper log = Application.getLogger(); - - /** - * An immutable FlightData object with NaN data. - */ - public static final FlightData NaN_DATA; - static { - FlightData data = new FlightData(); - data.immute(); - NaN_DATA = data; - } - - private Mutable mutable = new Mutable(); - - private final ArrayList<FlightDataBranch> branches = new ArrayList<FlightDataBranch>(); - - private final WarningSet warnings = new WarningSet(); - - private double maxAltitude = Double.NaN; - private double maxVelocity = Double.NaN; - private double maxAcceleration = Double.NaN; - private double maxMachNumber = Double.NaN; - private double timeToApogee = Double.NaN; - private double flightTime = Double.NaN; - private double groundHitVelocity = Double.NaN; - private double launchRodVelocity = Double.NaN; - - - /** - * Create a FlightData object with no content. The resulting object is mutable. - */ - public FlightData() { - - } - - - /** - * Construct a FlightData object with no data branches but the specified - * summary information. The resulting object is mutable. - * - * @param maxAltitude maximum altitude. - * @param maxVelocity maximum velocity. - * @param maxAcceleration maximum acceleration. - * @param maxMachNumber maximum Mach number. - * @param timeToApogee time to apogee. - * @param flightTime total flight time. - * @param groundHitVelocity ground hit velocity. - * @param launchRodVelocity TODO - */ - public FlightData(double maxAltitude, double maxVelocity, double maxAcceleration, - double maxMachNumber, double timeToApogee, double flightTime, - double groundHitVelocity, double launchRodVelocity) { - this.maxAltitude = maxAltitude; - this.maxVelocity = maxVelocity; - this.maxAcceleration = maxAcceleration; - this.maxMachNumber = maxMachNumber; - this.timeToApogee = timeToApogee; - this.flightTime = flightTime; - this.groundHitVelocity = groundHitVelocity; - this.launchRodVelocity = launchRodVelocity; - } - - - /** - * Create a FlightData object with the specified branches. The resulting object is mutable. - * - * @param branches the branches. - */ - public FlightData(FlightDataBranch... branches) { - this(); - - for (FlightDataBranch b : branches) - this.addBranch(b); - - calculateIntrestingValues(); - } - - - - - /** - * Returns the warning set associated with this object. This WarningSet cannot be - * set, so simulations must use this warning set to store their warnings. - * The returned WarningSet should not be modified otherwise. - * - * @return the warnings generated during this simulation. - */ - public WarningSet getWarningSet() { - return warnings; - } - - - public void addBranch(FlightDataBranch branch) { - mutable.check(); - - branch.immute(); - branches.add(branch); - - if (branches.size() == 1) { - calculateIntrestingValues(); - } - } - - public int getBranchCount() { - return branches.size(); - } - - public FlightDataBranch getBranch(int n) { - return branches.get(n); - } - - - - public double getMaxAltitude() { - return maxAltitude; - } - - public double getMaxVelocity() { - return maxVelocity; - } - - /** - * NOTE: This value only takes into account flight phase. - */ - public double getMaxAcceleration() { - return maxAcceleration; - } - - public double getMaxMachNumber() { - return maxMachNumber; - } - - public double getTimeToApogee() { - return timeToApogee; - } - - public double getFlightTime() { - return flightTime; - } - - public double getGroundHitVelocity() { - return groundHitVelocity; - } - - public double getLaunchRodVelocity() { - return launchRodVelocity; - } - - - - /** - * Calculate the max. altitude/velocity/acceleration, time to apogee, flight time - * and ground hit velocity. - */ - private void calculateIntrestingValues() { - if (branches.isEmpty()) - return; - - FlightDataBranch branch = branches.get(0); - maxAltitude = branch.getMaximum(FlightDataType.TYPE_ALTITUDE); - maxVelocity = branch.getMaximum(FlightDataType.TYPE_VELOCITY_TOTAL); - maxMachNumber = branch.getMaximum(FlightDataType.TYPE_MACH_NUMBER); - - flightTime = branch.getLast(FlightDataType.TYPE_TIME); - if (branch.getLast(FlightDataType.TYPE_ALTITUDE) < 10) { - groundHitVelocity = branch.getLast(FlightDataType.TYPE_VELOCITY_TOTAL); - } else { - groundHitVelocity = Double.NaN; - } - - - // Time to apogee - List<Double> time = branch.get(FlightDataType.TYPE_TIME); - List<Double> altitude = branch.get(FlightDataType.TYPE_ALTITUDE); - - if (time == null || altitude == null) { - timeToApogee = Double.NaN; - maxAcceleration = Double.NaN; - return; - } - int index = 0; - for (Double alt : altitude) { - if (alt != null) { - if (MathUtil.equals(alt, maxAltitude)) - break; - } - - index++; - } - if (index < time.size()) - timeToApogee = time.get(index); - else - timeToApogee = Double.NaN; - - - // Launch rod velocity - eventloop: for (FlightEvent event : branch.getEvents()) { - if (event.getType() == FlightEvent.Type.LAUNCHROD) { - double t = event.getTime(); - List<Double> velocity = branch.get(FlightDataType.TYPE_VELOCITY_TOTAL); - if (velocity == null) { - break; - } - for (int i = 0; i < velocity.size(); i++) { - if (time.get(i) >= t) { - launchRodVelocity = velocity.get(i); - break eventloop; - } - } - } - } - - // Max. acceleration (must be after apogee time) - if (branch.get(FlightDataType.TYPE_ACCELERATION_TOTAL) != null) { - maxAcceleration = calculateMaxAcceleration(); - } else { - maxAcceleration = Double.NaN; - } - - log.debug("Computed flight values:" + - " maxAltitude=" + maxAltitude + - " maxVelocity=" + maxVelocity + - " maxAcceleration=" + maxAcceleration + - " maxMachNumber=" + maxMachNumber + - " timeToApogee=" + timeToApogee + - " flightTime=" + flightTime + - " groundHitVelocity=" + groundHitVelocity + - " launchRodVelocity=" + launchRodVelocity); - } - - - public void immute() { - mutable.immute(); - warnings.immute(); - for (FlightDataBranch b : branches) { - b.immute(); - } - } - - - public boolean isMutable() { - return mutable.isMutable(); - } - - - - /** - * Find the maximum acceleration before apogee. - */ - private double calculateMaxAcceleration() { - - // End check at first recovery device deployment - double endTime = Double.MAX_VALUE; - - FlightDataBranch branch = this.getBranch(0); - for (FlightEvent event : branch.getEvents()) { - if (event.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) { - if (event.getTime() < endTime) { - endTime = event.getTime(); - } - } - } - - List<Double> time = branch.get(FlightDataType.TYPE_TIME); - List<Double> acceleration = branch.get(FlightDataType.TYPE_ACCELERATION_TOTAL); - - if (time == null || acceleration == null) { - return Double.NaN; - } - - double max = 0; - - for (int i = 0; i < time.size(); i++) { - if (time.get(i) >= endTime) { - break; - } - double a = acceleration.get(i); - if (a > max) - max = a; - } - - return max; - } -} diff --git a/src/net/sf/openrocket/simulation/FlightDataBranch.java b/src/net/sf/openrocket/simulation/FlightDataBranch.java deleted file mode 100644 index f3eb3d9c..00000000 --- a/src/net/sf/openrocket/simulation/FlightDataBranch.java +++ /dev/null @@ -1,255 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import net.sf.openrocket.util.ArrayList; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.Mutable; - -/** - * A single branch of flight data. The data is ordered based on some variable, typically time. - * It also contains flight events that have occurred during simulation. - * <p> - * After instantiating a FlightDataBranch data and new variable types can be added to the branch. - * A new data point (a value for each variable defined) is created using {@link #addPoint()} after - * which the value for each variable type can be set using {@link #setValue(FlightDataType, double)}. - * Each variable type does NOT have to be set, unset values will default to NaN. New variable types - * not defined in the constructor can be added using {@link #setValue(FlightDataType, double)}, they - * will be created and all previous values will be set to NaN. - * <p> - * After populating a FlightDataBranch object it can be made immutable by calling {@link #immute()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FlightDataBranch implements Monitorable { - - /** The name of this flight data branch. */ - private final String branchName; - - private final Map<FlightDataType, ArrayList<Double>> values = - new LinkedHashMap<FlightDataType, ArrayList<Double>>(); - - private final Map<FlightDataType, Double> maxValues = new HashMap<FlightDataType, Double>(); - private final Map<FlightDataType, Double> minValues = new HashMap<FlightDataType, Double>(); - - - private final ArrayList<FlightEvent> events = new ArrayList<FlightEvent>(); - - private Mutable mutable = new Mutable(); - - private int modID = 0; - - /** - * Sole constructor. Defines the name of the FlightDataBranch and at least one variable type. - * - * @param name the name of this FlightDataBranch. - * @param types data types to include (must include at least one type). - */ - public FlightDataBranch(String name, FlightDataType... types) { - if (types.length == 0) { - throw new IllegalArgumentException("Must specify at least one data type."); - } - - this.branchName = name; - - for (FlightDataType t : types) { - if (values.containsKey(t)) { - throw new IllegalArgumentException("Value type " + t + " specified multiple " + - "times in constructor."); - } - - values.put(t, new ArrayList<Double>()); - minValues.put(t, Double.NaN); - maxValues.put(t, Double.NaN); - } - } - - - - /** - * Adds a new point into the data branch. The value for all types is set to NaN by default. - * - * @throws IllegalStateException if this object has been made immutable. - */ - public void addPoint() { - mutable.check(); - - for (FlightDataType t : values.keySet()) { - values.get(t).add(Double.NaN); - } - modID++; - } - - - /** - * Set the value for a specific data type at the latest point. New variable types can be - * added to the FlightDataBranch transparently. - * - * @param type the variable to set. - * @param value the value to set. - * @throws IllegalStateException if this object has been made immutable. - */ - public void setValue(FlightDataType type, double value) { - mutable.check(); - - ArrayList<Double> list = values.get(type); - if (list == null) { - - list = new ArrayList<Double>(); - int n = getLength(); - for (int i = 0; i < n; i++) { - list.add(Double.NaN); - } - values.put(type, list); - minValues.put(type, value); - maxValues.put(type, value); - - } - list.set(list.size() - 1, value); - double min = minValues.get(type); - double max = maxValues.get(type); - - if (Double.isNaN(min) || (value < min)) { - minValues.put(type, value); - } - if (Double.isNaN(max) || (value > max)) { - maxValues.put(type, value); - } - modID++; - } - - - /** - * Return the branch name. - */ - public String getBranchName() { - return branchName; - } - - /** - * Return the variable types included in this branch. The types are sorted in their - * natural order. - */ - public FlightDataType[] getTypes() { - FlightDataType[] array = values.keySet().toArray(new FlightDataType[0]); - Arrays.sort(array); - return array; - } - - /** - * Return the number of data points in this branch. - */ - public int getLength() { - for (FlightDataType t : values.keySet()) { - return values.get(t).size(); - } - return 0; - } - - /** - * Return an array of values for the specified variable type. - * - * @param type the variable type. - * @return a list of the variable values, or <code>null</code> if - * the variable type hasn't been added to this branch. - */ - public List<Double> get(FlightDataType type) { - ArrayList<Double> list = values.get(type); - if (list == null) - return null; - return list.clone(); - } - - /** - * Return the last value of the specified type in the branch, or NaN if the type is - * unavailable. - * - * @param type the parameter type. - * @return the last value in this branch, or NaN. - */ - public double getLast(FlightDataType type) { - ArrayList<Double> list = values.get(type); - if (list == null || list.isEmpty()) - return Double.NaN; - return list.get(list.size() - 1); - } - - /** - * Return the minimum value of the specified type in the branch, or NaN if the type - * is unavailable. - * - * @param type the parameter type. - * @return the minimum value in this branch, or NaN. - */ - public double getMinimum(FlightDataType type) { - Double v = minValues.get(type); - if (v == null) - return Double.NaN; - return v; - } - - /** - * Return the maximum value of the specified type in the branch, or NaN if the type - * is unavailable. - * - * @param type the parameter type. - * @return the maximum value in this branch, or NaN. - */ - public double getMaximum(FlightDataType type) { - Double v = maxValues.get(type); - if (v == null) - return Double.NaN; - return v; - } - - - /** - * Add a flight event to this branch. - * - * @param event the event to add. - * @throws IllegalStateException if this branch has been made immutable. - */ - public void addEvent(FlightEvent event) { - mutable.check(); - events.add(event.resetSourceAndData()); - modID++; - } - - - /** - * Return the list of events. - * - * @return the list of events during the flight. - */ - public List<FlightEvent> getEvents() { - return events.clone(); - } - - - /** - * Make this FlightDataBranch immutable. Any calls to the set methods that would - * modify this object will after this call throw an <code>IllegalStateException</code>. - */ - public void immute() { - mutable.immute(); - } - - - /** - * Return whether this branch is still mutable. - */ - public boolean isMutable() { - return mutable.isMutable(); - } - - - @Override - public int getModID() { - return modID; - } - -} diff --git a/src/net/sf/openrocket/simulation/FlightDataType.java b/src/net/sf/openrocket/simulation/FlightDataType.java deleted file mode 100644 index 26862420..00000000 --- a/src/net/sf/openrocket/simulation/FlightDataType.java +++ /dev/null @@ -1,260 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.HashMap; -import java.util.Map; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.unit.UnitGroup; - -/** - * A class defining a storable simulation variable type. This class defined numerous ready - * types, and allows also creating new types with any name. When retrieving types based on - * a name, you should use {@link #getType(String, UnitGroup)} to return the default unit type, - * or a new type if the name does not currently exist. - * <p> - * Each type has a type name (description), a unit group and a priority. The type is identified - * purely by its name case-insensitively. The unit group provides the units for the type. - * The priority is used to order the types. The pre-existing types are defined specific priority - * numbers, and other types have a default priority number that is after all other types. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FlightDataType implements Comparable<FlightDataType> { - private static final Translator trans = Application.getTranslator(); - - /** Priority of custom-created variables */ - private static final int DEFAULT_PRIORITY = 999; - - /** List of existing types. MUST BE DEFINED BEFORE ANY TYPES!! */ - private static final Map<String, FlightDataType> EXISTING_TYPES = new HashMap<String, FlightDataType>(); - - - - //// Time - public static final FlightDataType TYPE_TIME = newType(trans.get("FlightDataType.TYPE_TIME"), UnitGroup.UNITS_FLIGHT_TIME, 1); - - - //// Vertical position and motion - //// Altitude - public static final FlightDataType TYPE_ALTITUDE = newType(trans.get("FlightDataType.TYPE_ALTITUDE"), UnitGroup.UNITS_DISTANCE, 10); - //// Vertical velocity - public static final FlightDataType TYPE_VELOCITY_Z = newType(trans.get("FlightDataType.TYPE_VELOCITY_Z"), UnitGroup.UNITS_VELOCITY, 11); - //// Vertical acceleration - public static final FlightDataType TYPE_ACCELERATION_Z = newType(trans.get("FlightDataType.TYPE_ACCELERATION_Z"), UnitGroup.UNITS_ACCELERATION, 12); - - - //// Total motion - //// Total velocity - public static final FlightDataType TYPE_VELOCITY_TOTAL = newType(trans.get("FlightDataType.TYPE_VELOCITY_TOTAL"), UnitGroup.UNITS_VELOCITY, 20); - //// Total acceleration - public static final FlightDataType TYPE_ACCELERATION_TOTAL = newType(trans.get("FlightDataType.TYPE_ACCELERATION_TOTAL"), UnitGroup.UNITS_ACCELERATION, 21); - - - //// Lateral position and motion - //// Position upwind - public static final FlightDataType TYPE_POSITION_X = newType(trans.get("FlightDataType.TYPE_POSITION_X"), UnitGroup.UNITS_DISTANCE, 30); - //// Position parallel to wind - public static final FlightDataType TYPE_POSITION_Y = newType(trans.get("FlightDataType.TYPE_POSITION_Y"), UnitGroup.UNITS_DISTANCE, 31); - //// Lateral distance - public static final FlightDataType TYPE_POSITION_XY = newType(trans.get("FlightDataType.TYPE_POSITION_XY"), UnitGroup.UNITS_DISTANCE, 32); - //// Lateral direction - public static final FlightDataType TYPE_POSITION_DIRECTION = newType(trans.get("FlightDataType.TYPE_POSITION_DIRECTION"), UnitGroup.UNITS_ANGLE, 33); - //// Lateral velocity - public static final FlightDataType TYPE_VELOCITY_XY = newType(trans.get("FlightDataType.TYPE_VELOCITY_XY"), UnitGroup.UNITS_VELOCITY, 34); - //// Lateral acceleration - public static final FlightDataType TYPE_ACCELERATION_XY = newType(trans.get("FlightDataType.TYPE_ACCELERATION_XY"), UnitGroup.UNITS_ACCELERATION, 35); - //// Latitude - public static final FlightDataType TYPE_LATITUDE = newType(trans.get("FlightDataType.TYPE_LATITUDE"), UnitGroup.UNITS_ANGLE, 36); - //// Longitude - public static final FlightDataType TYPE_LONGITUDE = newType(trans.get("FlightDataType.TYPE_LONGITUDE"), UnitGroup.UNITS_ANGLE, 37); - - //// Angular motion - //// Angle of attack - public static final FlightDataType TYPE_AOA = newType(trans.get("FlightDataType.TYPE_AOA"), UnitGroup.UNITS_ANGLE, 40); - //// Roll rate - public static final FlightDataType TYPE_ROLL_RATE = newType(trans.get("FlightDataType.TYPE_ROLL_RATE"), UnitGroup.UNITS_ROLL, 41); - //// Pitch rate - public static final FlightDataType TYPE_PITCH_RATE = newType(trans.get("FlightDataType.TYPE_PITCH_RATE"), UnitGroup.UNITS_ROLL, 42); - //// Yaw rate - public static final FlightDataType TYPE_YAW_RATE = newType(trans.get("FlightDataType.TYPE_YAW_RATE"), UnitGroup.UNITS_ROLL, 43); - - - //// Stability information - //// Mass - public static final FlightDataType TYPE_MASS = newType(trans.get("FlightDataType.TYPE_MASS"), UnitGroup.UNITS_MASS, 50); - //// Longitudinal moment of inertia - public static final FlightDataType TYPE_LONGITUDINAL_INERTIA = newType(trans.get("FlightDataType.TYPE_LONGITUDINAL_INERTIA"), UnitGroup.UNITS_INERTIA, 51); - //// Rotational moment of inertia - public static final FlightDataType TYPE_ROTATIONAL_INERTIA = newType(trans.get("FlightDataType.TYPE_ROTATIONAL_INERTIA"), UnitGroup.UNITS_INERTIA, 52); - //// CP location - public static final FlightDataType TYPE_CP_LOCATION = newType(trans.get("FlightDataType.TYPE_CP_LOCATION"), UnitGroup.UNITS_LENGTH, 53); - //// CG location - public static final FlightDataType TYPE_CG_LOCATION = newType(trans.get("FlightDataType.TYPE_CG_LOCATION"), UnitGroup.UNITS_LENGTH, 54); - //// Stability margin calibers - public static final FlightDataType TYPE_STABILITY = newType(trans.get("FlightDataType.TYPE_STABILITY"), UnitGroup.UNITS_COEFFICIENT, 55); - - - //// Characteristic numbers - //// Mach number - public static final FlightDataType TYPE_MACH_NUMBER = newType(trans.get("FlightDataType.TYPE_MACH_NUMBER"), UnitGroup.UNITS_COEFFICIENT, 60); - //// Reynolds number - public static final FlightDataType TYPE_REYNOLDS_NUMBER = newType(trans.get("FlightDataType.TYPE_REYNOLDS_NUMBER"), UnitGroup.UNITS_COEFFICIENT, 61); - - - //// Thrust and drag - //// Thrust - public static final FlightDataType TYPE_THRUST_FORCE = newType(trans.get("FlightDataType.TYPE_THRUST_FORCE"), UnitGroup.UNITS_FORCE, 70); - //// Drag force - public static final FlightDataType TYPE_DRAG_FORCE = newType(trans.get("FlightDataType.TYPE_DRAG_FORCE"), UnitGroup.UNITS_FORCE, 71); - //// Drag coefficient - public static final FlightDataType TYPE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 72); - //// Axial drag coefficient - public static final FlightDataType TYPE_AXIAL_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_AXIAL_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 73); - - - //// Component drag coefficients - //// Friction drag coefficient - public static final FlightDataType TYPE_FRICTION_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_FRICTION_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 80); - //// Pressure drag coefficient - public static final FlightDataType TYPE_PRESSURE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_PRESSURE_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 81); - //// Base drag coefficient - public static final FlightDataType TYPE_BASE_DRAG_COEFF = newType(trans.get("FlightDataType.TYPE_BASE_DRAG_COEFF"), UnitGroup.UNITS_COEFFICIENT, 82); - - - //// Other coefficients - //// Normal force coefficient - public static final FlightDataType TYPE_NORMAL_FORCE_COEFF = newType(trans.get("FlightDataType.TYPE_NORMAL_FORCE_COEFF"), UnitGroup.UNITS_COEFFICIENT, 90); - //// Pitch moment coefficient - public static final FlightDataType TYPE_PITCH_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_PITCH_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 91); - //// Yaw moment coefficient - public static final FlightDataType TYPE_YAW_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_YAW_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 92); - //// Side force coefficient - public static final FlightDataType TYPE_SIDE_FORCE_COEFF = newType(trans.get("FlightDataType.TYPE_SIDE_FORCE_COEFF"), UnitGroup.UNITS_COEFFICIENT, 93); - //// Roll moment coefficient - public static final FlightDataType TYPE_ROLL_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 94); - //// Roll forcing coefficient - public static final FlightDataType TYPE_ROLL_FORCING_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_FORCING_COEFF"), UnitGroup.UNITS_COEFFICIENT, 95); - //// Roll damping coefficient - public static final FlightDataType TYPE_ROLL_DAMPING_COEFF = newType(trans.get("FlightDataType.TYPE_ROLL_DAMPING_COEFF"), UnitGroup.UNITS_COEFFICIENT, 96); - - //// Pitch damping coefficient - public static final FlightDataType TYPE_PITCH_DAMPING_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 97); - //// Yaw damping coefficient - public static final FlightDataType TYPE_YAW_DAMPING_MOMENT_COEFF = newType(trans.get("FlightDataType.TYPE_YAW_DAMPING_MOMENT_COEFF"), UnitGroup.UNITS_COEFFICIENT, 98); - - //// Coriolis acceleration - public static final FlightDataType TYPE_CORIOLIS_ACCELERATION = newType(trans.get("FlightDataType.TYPE_CORIOLIS_ACCELERATION"), UnitGroup.UNITS_ACCELERATION, 99); - - - //// Reference length + area - //// Reference length - public static final FlightDataType TYPE_REFERENCE_LENGTH = newType(trans.get("FlightDataType.TYPE_REFERENCE_LENGTH"), UnitGroup.UNITS_LENGTH, 100); - //// Reference area - public static final FlightDataType TYPE_REFERENCE_AREA = newType(trans.get("FlightDataType.TYPE_REFERENCE_AREA"), UnitGroup.UNITS_AREA, 101); - - - //// Orientation - //// Vertical orientation (zenith) - public static final FlightDataType TYPE_ORIENTATION_THETA = newType(trans.get("FlightDataType.TYPE_ORIENTATION_THETA"), UnitGroup.UNITS_ANGLE, 106); - //// Lateral orientation (azimuth) - public static final FlightDataType TYPE_ORIENTATION_PHI = newType(trans.get("FlightDataType.TYPE_ORIENTATION_PHI"), UnitGroup.UNITS_ANGLE, 107); - - - //// Atmospheric conditions - //// Wind velocity - public static final FlightDataType TYPE_WIND_VELOCITY = newType(trans.get("FlightDataType.TYPE_WIND_VELOCITY"), UnitGroup.UNITS_VELOCITY, 110); - //// Air temperature - public static final FlightDataType TYPE_AIR_TEMPERATURE = newType(trans.get("FlightDataType.TYPE_AIR_TEMPERATURE"), UnitGroup.UNITS_TEMPERATURE, 111); - //// Air pressure - public static final FlightDataType TYPE_AIR_PRESSURE = newType(trans.get("FlightDataType.TYPE_AIR_PRESSURE"), UnitGroup.UNITS_PRESSURE, 112); - //// Speed of sound - public static final FlightDataType TYPE_SPEED_OF_SOUND = newType(trans.get("FlightDataType.TYPE_SPEED_OF_SOUND"), UnitGroup.UNITS_VELOCITY, 113); - - //// Simulation information - //// Simulation time step - public static final FlightDataType TYPE_TIME_STEP = newType(trans.get("FlightDataType.TYPE_TIME_STEP"), UnitGroup.UNITS_TIME_STEP, 200); - //// Computation time - public static final FlightDataType TYPE_COMPUTATION_TIME = newType(trans.get("FlightDataType.TYPE_COMPUTATION_TIME"), UnitGroup.UNITS_SHORT_TIME, 201); - - - - /** - * Return a {@link FlightDataType} based on a string description. This returns known data types - * if possible, or a new type otherwise. - * - * @param s the string description of the type. - * @param u the unit group the new type should belong to if a new group is created. - * @return a data type. - */ - public static synchronized FlightDataType getType(String s, UnitGroup u) { - FlightDataType type = EXISTING_TYPES.get(s.toLowerCase()); - if (type != null) { - return type; - } - type = newType(s, u, DEFAULT_PRIORITY); - return type; - } - - /** - * Used while initializing the class. - */ - private static synchronized FlightDataType newType(String s, UnitGroup u, int priority) { - FlightDataType type = new FlightDataType(s, u, priority); - EXISTING_TYPES.put(s.toLowerCase(), type); - return type; - } - - - private final String name; - private final UnitGroup units; - private final int priority; - private final int hashCode; - - - private FlightDataType(String typeName, UnitGroup units, int priority) { - if (typeName == null) - throw new IllegalArgumentException("typeName is null"); - if (units == null) - throw new IllegalArgumentException("units is null"); - this.name = typeName; - this.units = units; - this.priority = priority; - this.hashCode = this.name.toLowerCase().hashCode(); - } - - - - - public String getName() { - return name; - } - - public UnitGroup getUnitGroup() { - return units; - } - - @Override - public String toString() { - return name; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof FlightDataType)) - return false; - return this.name.equalsIgnoreCase(((FlightDataType) other).name); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public int compareTo(FlightDataType o) { - if (this.priority != o.priority) - return this.priority - o.priority; - return this.name.compareToIgnoreCase(o.name); - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/simulation/FlightEvent.java b/src/net/sf/openrocket/simulation/FlightEvent.java deleted file mode 100644 index ead84eec..00000000 --- a/src/net/sf/openrocket/simulation/FlightEvent.java +++ /dev/null @@ -1,171 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.startup.Application; - - -/** - * A class that defines an event during the flight of a rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class FlightEvent implements Comparable<FlightEvent> { - private static final Translator trans = Application.getTranslator(); - - /** - * The type of the flight event. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public enum Type { - /** - * Rocket launch. - */ - //// Launch - LAUNCH(trans.get("FlightEvent.Type.LAUNCH")), - /** - * Ignition of a motor. Source is the motor mount the motor of which has ignited, - * and the data is the MotorId of the motor instance. - */ - //// Motor ignition - IGNITION(trans.get("FlightEvent.Type.IGNITION")), - /** - * When the motor has lifted off the ground. - */ - //// Lift-off - LIFTOFF(trans.get("FlightEvent.Type.LIFTOFF")), - /** - * Launch rod has been cleared. - */ - //// Launch rod clearance - LAUNCHROD(trans.get("FlightEvent.Type.LAUNCHROD")), - /** - * Burnout of a motor. Source is the motor mount the motor of which has burnt out, - * and the data is the MotorId of the motor instance. - */ - //// Motor burnout - BURNOUT(trans.get("FlightEvent.Type.BURNOUT")), - /** - * Ejection charge of a motor fired. Source is the motor mount the motor of - * which has exploded its ejection charge, and data is the MotorId of the motor instance. - */ - //// Ejection charge - EJECTION_CHARGE(trans.get("FlightEvent.Type.EJECTION_CHARGE")), - /** - * Separation of a stage. Source is the stage which has separated all lower stages. - */ - //// Stage separation - STAGE_SEPARATION(trans.get("FlightEvent.Type.STAGE_SEPARATION")), - /** - * Apogee has been reached. - */ - //// Apogee - APOGEE(trans.get("FlightEvent.Type.APOGEE")), - /** - * Opening of a recovery device. Source is the RecoveryComponent which has opened. - */ - //// Recovery device deployment - RECOVERY_DEVICE_DEPLOYMENT(trans.get("FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT")), - /** - * Ground has been hit after flight. - */ - //// Ground hit - GROUND_HIT(trans.get("FlightEvent.Type.GROUND_HIT")), - - /** - * End of simulation. Placing this to the queue will end the simulation. - */ - //// Simulation end - SIMULATION_END(trans.get("FlightEvent.Type.SIMULATION_END")), - - /** - * A change in altitude has occurred. Data is a <code>Pair<Double,Double></code> - * which contains the old and new altitudes. - */ - //// Altitude change - ALTITUDE(trans.get("FlightEvent.Type.ALTITUDE")); - - private final String name; - - private Type(String name) { - this.name = name; - } - - @Override - public String toString() { - return name; - } - } - - private final Type type; - private final double time; - private final RocketComponent source; - private final Object data; - - - public FlightEvent(Type type, double time) { - this(type, time, null); - } - - public FlightEvent(Type type, double time, RocketComponent source) { - this(type, time, source, null); - } - - public FlightEvent(Type type, double time, RocketComponent source, Object data) { - this.type = type; - this.time = time; - this.source = source; - this.data = data; - } - - - - public Type getType() { - return type; - } - - public double getTime() { - return time; - } - - public RocketComponent getSource() { - return source; - } - - public Object getData() { - return data; - } - - - /** - * Return a new FlightEvent with the same information as the current event - * but with <code>null</code> source. This is used to avoid memory leakage by - * retaining references to obsolete components. - * - * @return a new FlightEvent with same type, time and data. - */ - public FlightEvent resetSourceAndData() { - return new FlightEvent(type, time, null, null); - } - - - /** - * Compares this event to another event depending on the event time. Secondary - * sorting is performed based on the event type ordinal. - */ - @Override - public int compareTo(FlightEvent o) { - if (this.time < o.time) - return -1; - if (this.time > o.time) - return 1; - - return this.type.ordinal() - o.type.ordinal(); - } - - @Override - public String toString() { - return "FlightEvent[type=" + type.name() + ",time=" + time + ",source=" + source + "]"; - } -} diff --git a/src/net/sf/openrocket/simulation/MassData.java b/src/net/sf/openrocket/simulation/MassData.java deleted file mode 100644 index 1d910df0..00000000 --- a/src/net/sf/openrocket/simulation/MassData.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -/** - * An immutable value object containing the mass data of a rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MassData { - - private final Coordinate cg; - private final double longitudinalInertia; - private final double rotationalInertia; - - - public MassData(Coordinate cg, double longitudinalInertia, double rotationalInertia) { - if (cg == null) { - throw new IllegalArgumentException("cg is null"); - } - this.cg = cg; - this.longitudinalInertia = longitudinalInertia; - this.rotationalInertia = rotationalInertia; - } - - - - - public Coordinate getCG() { - return cg; - } - - public double getLongitudinalInertia() { - return longitudinalInertia; - } - - public double getRotationalInertia() { - return rotationalInertia; - } - - - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof MassData)) - return false; - - MassData other = (MassData) obj; - return (this.cg.equals(other.cg) && MathUtil.equals(this.longitudinalInertia, other.longitudinalInertia) && - MathUtil.equals(this.rotationalInertia, other.rotationalInertia)); - } - - - @Override - public int hashCode() { - return (int) (cg.hashCode() ^ Double.doubleToLongBits(longitudinalInertia) ^ Double.doubleToLongBits(rotationalInertia)); - } - - - @Override - public String toString() { - return "MassData [cg=" + cg + ", longitudinalInertia=" + longitudinalInertia - + ", rotationalInertia=" + rotationalInertia + "]"; - } - -} diff --git a/src/net/sf/openrocket/simulation/RK4SimulationStatus.java b/src/net/sf/openrocket/simulation/RK4SimulationStatus.java deleted file mode 100644 index 505ff77c..00000000 --- a/src/net/sf/openrocket/simulation/RK4SimulationStatus.java +++ /dev/null @@ -1,74 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.util.Coordinate; - -public class RK4SimulationStatus extends SimulationStatus { - private Coordinate launchRodDirection; - - private double previousAcceleration = 0; - private AtmosphericConditions previousAtmosphericConditions; - - // Used for determining when to store aerodynamic computation warnings: - private double maxZVelocity = 0; - private double startWarningTime = -1; - - - public void setLaunchRodDirection(Coordinate launchRodDirection) { - this.launchRodDirection = launchRodDirection; - } - - - public Coordinate getLaunchRodDirection() { - return launchRodDirection; - } - - - - public double getPreviousAcceleration() { - return previousAcceleration; - } - - - public void setPreviousAcceleration(double previousAcceleration) { - this.previousAcceleration = previousAcceleration; - } - - - public AtmosphericConditions getPreviousAtmosphericConditions() { - return previousAtmosphericConditions; - } - - - public void setPreviousAtmosphericConditions( - AtmosphericConditions previousAtmosphericConditions) { - this.previousAtmosphericConditions = previousAtmosphericConditions; - } - - - public double getMaxZVelocity() { - return maxZVelocity; - } - - - public void setMaxZVelocity(double maxZVelocity) { - this.maxZVelocity = maxZVelocity; - } - - - public double getStartWarningTime() { - return startWarningTime; - } - - - public void setStartWarningTime(double startWarningTime) { - this.startWarningTime = startWarningTime; - } - - - @Override - public RK4SimulationStatus clone() { - return (RK4SimulationStatus) super.clone(); - } - -} diff --git a/src/net/sf/openrocket/simulation/RK4SimulationStepper.java b/src/net/sf/openrocket/simulation/RK4SimulationStepper.java deleted file mode 100644 index 6b7aad9d..00000000 --- a/src/net/sf/openrocket/simulation/RK4SimulationStepper.java +++ /dev/null @@ -1,725 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.Arrays; -import java.util.Random; - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.simulation.exception.SimulationCalculationException; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.SimulationListenerHelper; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.GeodeticComputationStrategy; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Quaternion; -import net.sf.openrocket.util.Rotation2D; -import net.sf.openrocket.util.WorldCoordinate; - -public class RK4SimulationStepper extends AbstractSimulationStepper { - - private static final LogHelper log = Application.getLogger(); - private static final Translator trans = Application.getTranslator(); - - - /** Random value with which to XOR the random seed value */ - private static final int SEED_RANDOMIZATION = 0x23E3A01F; - - - /** - * A recommended reasonably accurate time step. - */ - public static final double RECOMMENDED_TIME_STEP = 0.05; - - /** - * A recommended maximum angle step value. - */ - public static final double RECOMMENDED_ANGLE_STEP = 3 * Math.PI / 180; - - /** - * A random amount that is added to pitch and yaw coefficients, plus or minus. - */ - public static final double PITCH_YAW_RANDOM = 0.0005; - - /** - * Maximum roll step allowed. This is selected as an uneven division of the full - * circle so that the simulation will sample the most wind directions - */ - private static final double MAX_ROLL_STEP_ANGLE = 2 * 28.32 * Math.PI / 180; - // private static final double MAX_ROLL_STEP_ANGLE = 8.32 * Math.PI/180; - - private static final double MAX_ROLL_RATE_CHANGE = 2 * Math.PI / 180; - private static final double MAX_PITCH_CHANGE = 4 * Math.PI / 180; - - private static final double MIN_TIME_STEP = 0.001; - - - private Random random; - - - - - @Override - public RK4SimulationStatus initialize(SimulationStatus original) { - - RK4SimulationStatus status = new RK4SimulationStatus(); - - status.copyFrom(original); - - SimulationConditions sim = original.getSimulationConditions(); - - status.setLaunchRodDirection(new Coordinate( - Math.sin(sim.getLaunchRodAngle()) * Math.cos(sim.getLaunchRodDirection()), - Math.sin(sim.getLaunchRodAngle()) * Math.sin(sim.getLaunchRodDirection()), - Math.cos(sim.getLaunchRodAngle()) - )); - - this.random = new Random(original.getSimulationConditions().getRandomSeed() ^ SEED_RANDOMIZATION); - - return status; - } - - - - - @Override - public void step(SimulationStatus simulationStatus, double maxTimeStep) throws SimulationException { - - RK4SimulationStatus status = (RK4SimulationStatus) simulationStatus; - DataStore store = new DataStore(); - - //////// Perform RK4 integration: //////// - - RK4SimulationStatus status2; - RK4Parameters k1, k2, k3, k4; - - /* - * Start with previous time step which is used to compute the initial thrust estimate. - * Don't make it longer than maxTimeStep, but at least MIN_TIME_STEP. - */ - store.timestep = status.getPreviousTimeStep(); - store.timestep = MathUtil.max(MathUtil.min(store.timestep, maxTimeStep), MIN_TIME_STEP); - checkNaN(store.timestep); - - /* - * Compute the initial thrust estimate. This is used for the first time step computation. - */ - store.thrustForce = calculateThrust(status, store.timestep, status.getPreviousAcceleration(), - status.getPreviousAtmosphericConditions(), false); - - - /* - * Perform RK4 integration. Decide the time step length after the first step. - */ - - //// First position, k1 = f(t, y) - - k1 = computeParameters(status, store); - - /* - * Select the actual time step to use. It is the minimum of the following: - * dt[0]: the user-specified time step (or 1/5th of it if still on the launch rod) - * dt[1]: the value of maxTimeStep - * dt[2]: the maximum pitch step angle limit - * dt[3]: the maximum roll step angle limit - * dt[4]: the maximum roll rate change limit - * dt[5]: the maximum pitch change limit - * dt[6]: 1/10th of the launch rod length if still on the launch rod - * dt[7]: 1.50 times the previous time step - * - * The limits #5 and #6 are required since near the steady-state roll rate the roll rate - * may oscillate significantly even between the sub-steps of the RK4 integration. - * - * The step is still at least 1/20th of the user-selected time step. - */ - double[] dt = new double[8]; - Arrays.fill(dt, Double.MAX_VALUE); - - dt[0] = status.getSimulationConditions().getTimeStep(); - dt[1] = maxTimeStep; - dt[2] = status.getSimulationConditions().getMaximumAngleStep() / store.lateralPitchRate; - dt[3] = Math.abs(MAX_ROLL_STEP_ANGLE / store.flightConditions.getRollRate()); - dt[4] = Math.abs(MAX_ROLL_RATE_CHANGE / store.rollAcceleration); - dt[5] = Math.abs(MAX_PITCH_CHANGE / store.lateralPitchAcceleration); - if (!status.isLaunchRodCleared()) { - dt[0] /= 5.0; - dt[6] = status.getSimulationConditions().getLaunchRodLength() / k1.v.length() / 10; - } - dt[7] = 1.5 * status.getPreviousTimeStep(); - - store.timestep = Double.MAX_VALUE; - int limitingValue = -1; - for (int i = 0; i < dt.length; i++) { - if (dt[i] < store.timestep) { - store.timestep = dt[i]; - limitingValue = i; - } - } - - double minTimeStep = status.getSimulationConditions().getTimeStep() / 20; - if (store.timestep < minTimeStep) { - log.verbose("Too small time step " + store.timestep + " (limiting factor " + limitingValue + "), using " + - minTimeStep + " instead."); - store.timestep = minTimeStep; - } else { - log.verbose("Selected time step " + store.timestep + " (limiting factor " + limitingValue + ")"); - } - checkNaN(store.timestep); - - /* - * Compute the correct thrust for this time step. If the original thrust estimate differs more - * than 10% from the true value then recompute the RK4 step 1. The 10% error in step 1 is - * diminished by it affecting only 1/6th of the total, so it's an acceptable error. - */ - double thrustEstimate = store.thrustForce; - store.thrustForce = calculateThrust(status, store.timestep, store.longitudinalAcceleration, - store.atmosphericConditions, true); - double thrustDiff = Math.abs(store.thrustForce - thrustEstimate); - // Log if difference over 1%, recompute if over 10% - if (thrustDiff > 0.01 * thrustEstimate) { - if (thrustDiff > 0.1 * thrustEstimate + 0.001) { - log.debug("Thrust estimate differs from correct value by " + - (Math.rint(1000 * (thrustDiff + 0.000001) / thrustEstimate) / 10.0) + "%," + - " estimate=" + thrustEstimate + - " correct=" + store.thrustForce + - " timestep=" + store.timestep + - ", recomputing k1 parameters"); - k1 = computeParameters(status, store); - } else { - log.verbose("Thrust estimate differs from correct value by " + - (Math.rint(1000 * (thrustDiff + 0.000001) / thrustEstimate) / 10.0) + "%," + - " estimate=" + thrustEstimate + - " correct=" + store.thrustForce + - " timestep=" + store.timestep + - ", error acceptable"); - } - } - - // Store data - // TODO: MEDIUM: Store acceleration etc of entire RK4 step, store should be cloned or something... - storeData(status, store); - - - //// Second position, k2 = f(t + h/2, y + k1*h/2) - - status2 = status.clone(); - status2.setSimulationTime(status.getSimulationTime() + store.timestep / 2); - status2.setRocketPosition(status.getRocketPosition().add(k1.v.multiply(store.timestep / 2))); - status2.setRocketVelocity(status.getRocketVelocity().add(k1.a.multiply(store.timestep / 2))); - status2.setRocketOrientationQuaternion(status.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(k1.rv.multiply(store.timestep / 2)))); - status2.setRocketRotationVelocity(status.getRocketRotationVelocity().add(k1.ra.multiply(store.timestep / 2))); - - k2 = computeParameters(status2, store); - - - //// Third position, k3 = f(t + h/2, y + k2*h/2) - - status2 = status.clone(); - status2.setSimulationTime(status.getSimulationTime() + store.timestep / 2); - status2.setRocketPosition(status.getRocketPosition().add(k2.v.multiply(store.timestep / 2))); - status2.setRocketVelocity(status.getRocketVelocity().add(k2.a.multiply(store.timestep / 2))); - status2.setRocketOrientationQuaternion(status2.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(k2.rv.multiply(store.timestep / 2)))); - status2.setRocketRotationVelocity(status.getRocketRotationVelocity().add(k2.ra.multiply(store.timestep / 2))); - - k3 = computeParameters(status2, store); - - - //// Fourth position, k4 = f(t + h, y + k3*h) - - status2 = status.clone(); - status2.setSimulationTime(status.getSimulationTime() + store.timestep); - status2.setRocketPosition(status.getRocketPosition().add(k3.v.multiply(store.timestep))); - status2.setRocketVelocity(status.getRocketVelocity().add(k3.a.multiply(store.timestep))); - status2.setRocketOrientationQuaternion(status2.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(k3.rv.multiply(store.timestep)))); - status2.setRocketRotationVelocity(status.getRocketRotationVelocity().add(k3.ra.multiply(store.timestep))); - - k4 = computeParameters(status2, store); - - - //// Sum all together, y(n+1) = y(n) + h*(k1 + 2*k2 + 2*k3 + k4)/6 - - - - Coordinate deltaV, deltaP, deltaR, deltaO; - deltaV = k2.a.add(k3.a).multiply(2).add(k1.a).add(k4.a).multiply(store.timestep / 6); - deltaP = k2.v.add(k3.v).multiply(2).add(k1.v).add(k4.v).multiply(store.timestep / 6); - deltaR = k2.ra.add(k3.ra).multiply(2).add(k1.ra).add(k4.ra).multiply(store.timestep / 6); - deltaO = k2.rv.add(k3.rv).multiply(2).add(k1.rv).add(k4.rv).multiply(store.timestep / 6); - - - - status.setRocketVelocity(status.getRocketVelocity().add(deltaV)); - status.setRocketPosition(status.getRocketPosition().add(deltaP)); - status.setRocketRotationVelocity(status.getRocketRotationVelocity().add(deltaR)); - status.setRocketOrientationQuaternion(status.getRocketOrientationQuaternion().multiplyLeft(Quaternion.rotation(deltaO)).normalizeIfNecessary()); - - WorldCoordinate w = status.getSimulationConditions().getLaunchSite(); - w = status.getSimulationConditions().getGeodeticComputation().addCoordinate(w, status.getRocketPosition()); - status.setRocketWorldPosition(w); - - status.setSimulationTime(status.getSimulationTime() + store.timestep); - - status.setPreviousTimeStep(store.timestep); - - // Verify that values don't run out of range - if (status.getRocketVelocity().length2() > 1e18 || - status.getRocketPosition().length2() > 1e18 || - status.getRocketRotationVelocity().length2() > 1e18) { - throw new SimulationCalculationException(trans.get("error.valuesTooLarge")); - } - } - - - - - - private RK4Parameters computeParameters(RK4SimulationStatus status, DataStore dataStore) - throws SimulationException { - RK4Parameters params = new RK4Parameters(); - - // if (dataStore == null) { - // dataStore = new DataStore(); - // } - - calculateAcceleration(status, dataStore); - params.a = dataStore.linearAcceleration; - params.ra = dataStore.angularAcceleration; - params.v = status.getRocketVelocity(); - params.rv = status.getRocketRotationVelocity(); - - checkNaN(params.a); - checkNaN(params.ra); - checkNaN(params.v); - checkNaN(params.rv); - - return params; - } - - - - - - /** - * Calculate the linear and angular acceleration at the given status. The results - * are stored in the fields {@link #linearAcceleration} and {@link #angularAcceleration}. - * - * @param status the status of the rocket. - * @throws SimulationException - */ - private void calculateAcceleration(RK4SimulationStatus status, DataStore store) throws SimulationException { - - // Call pre-listeners - store.accelerationData = SimulationListenerHelper.firePreAccelerationCalculation(status); - if (store.accelerationData != null) { - return; - } - - // Compute the forces affecting the rocket - calculateForces(status, store); - - // Calculate mass data - store.massData = calculateMassData(status); - - - // Calculate the forces from the aerodynamic coefficients - - double dynP = (0.5 * store.flightConditions.getAtmosphericConditions().getDensity() * - MathUtil.pow2(store.flightConditions.getVelocity())); - double refArea = store.flightConditions.getRefArea(); - double refLength = store.flightConditions.getRefLength(); - - - // Linear forces in rocket coordinates - store.dragForce = store.forces.getCaxial() * dynP * refArea; - double fN = store.forces.getCN() * dynP * refArea; - double fSide = store.forces.getCside() * dynP * refArea; - - double forceZ = store.thrustForce - store.dragForce; - - store.linearAcceleration = new Coordinate(-fN / store.massData.getCG().weight, - -fSide / store.massData.getCG().weight, - forceZ / store.massData.getCG().weight); - - store.linearAcceleration = store.thetaRotation.rotateZ(store.linearAcceleration); - - // Convert into rocket world coordinates - store.linearAcceleration = status.getRocketOrientationQuaternion().rotate(store.linearAcceleration); - - // add effect of gravity - store.gravity = modelGravity(status); - store.linearAcceleration = store.linearAcceleration.sub(0, 0, store.gravity); - - // add effect of Coriolis acceleration - store.coriolisAcceleration = status.getSimulationConditions().getGeodeticComputation() - .getCoriolisAcceleration(status.getRocketWorldPosition(), status.getRocketVelocity()); - store.linearAcceleration = store.linearAcceleration.add(store.coriolisAcceleration); - - // If still on the launch rod, project acceleration onto launch rod direction and - // set angular acceleration to zero. - if (!status.isLaunchRodCleared()) { - - store.linearAcceleration = status.getLaunchRodDirection().multiply( - store.linearAcceleration.dot(status.getLaunchRodDirection())); - store.angularAcceleration = Coordinate.NUL; - store.rollAcceleration = 0; - store.lateralPitchAcceleration = 0; - - } else { - - // Shift moments to CG - double Cm = store.forces.getCm() - store.forces.getCN() * store.massData.getCG().x / refLength; - double Cyaw = store.forces.getCyaw() - store.forces.getCside() * store.massData.getCG().x / refLength; - - // Compute moments - double momX = -Cyaw * dynP * refArea * refLength; - double momY = Cm * dynP * refArea * refLength; - double momZ = store.forces.getCroll() * dynP * refArea * refLength; - - // Compute acceleration in rocket coordinates - store.angularAcceleration = new Coordinate(momX / store.massData.getLongitudinalInertia(), - momY / store.massData.getLongitudinalInertia(), - momZ / store.massData.getRotationalInertia()); - - store.rollAcceleration = store.angularAcceleration.z; - // TODO: LOW: This should be hypot, but does it matter? - store.lateralPitchAcceleration = MathUtil.max(Math.abs(store.angularAcceleration.x), - Math.abs(store.angularAcceleration.y)); - - store.angularAcceleration = store.thetaRotation.rotateZ(store.angularAcceleration); - - // Convert to world coordinates - store.angularAcceleration = status.getRocketOrientationQuaternion().rotate(store.angularAcceleration); - - } - - // Call post-listeners - store.accelerationData = SimulationListenerHelper.firePostAccelerationCalculation(status, store.accelerationData); - } - - - /** - * Calculate the aerodynamic forces into the data store. This method also handles - * whether to include aerodynamic computation warnings or not. - */ - private void calculateForces(RK4SimulationStatus status, DataStore store) throws SimulationException { - - // Call pre-listeners - store.forces = SimulationListenerHelper.firePreAerodynamicCalculation(status); - if (store.forces != null) { - return; - } - - // Compute flight conditions - calculateFlightConditions(status, store); - - /* - * Check whether to store warnings or not. Warnings are ignored when on the - * launch rod or 0.25 seconds after departure, and when the velocity has dropped - * below 20% of the max. velocity. - */ - WarningSet warnings = status.getWarnings(); - status.setMaxZVelocity(MathUtil.max(status.getMaxZVelocity(), status.getRocketVelocity().z)); - - if (!status.isLaunchRodCleared()) { - warnings = null; - } else { - if (status.getRocketVelocity().z < 0.2 * status.getMaxZVelocity()) - warnings = null; - if (status.getStartWarningTime() < 0) - status.setStartWarningTime(status.getSimulationTime() + 0.25); - } - if (status.getSimulationTime() < status.getStartWarningTime()) - warnings = null; - - - // Calculate aerodynamic forces - store.forces = status.getSimulationConditions().getAerodynamicCalculator() - .getAerodynamicForces(status.getConfiguration(), store.flightConditions, warnings); - - - // Add very small randomization to yaw & pitch moments to prevent over-perfect flight - // TODO: HIGH: This should rather be performed as a listener - store.forces.setCm(store.forces.getCm() + (PITCH_YAW_RANDOM * 2 * (random.nextDouble() - 0.5))); - store.forces.setCyaw(store.forces.getCyaw() + (PITCH_YAW_RANDOM * 2 * (random.nextDouble() - 0.5))); - - - // Call post-listeners - store.forces = SimulationListenerHelper.firePostAerodynamicCalculation(status, store.forces); - } - - - - /** - * Calculate and return the flight conditions for the current rocket status. - * Listeners can override these if necessary. - * <p> - * Additionally the fields thetaRotation and lateralPitchRate are defined in - * the data store, and can be used after calling this method. - */ - private void calculateFlightConditions(RK4SimulationStatus status, DataStore store) - throws SimulationException { - - // Call pre listeners, allow complete override - store.flightConditions = SimulationListenerHelper.firePreFlightConditions( - status); - if (store.flightConditions != null) { - // Compute the store values - store.thetaRotation = new Rotation2D(store.flightConditions.getTheta()); - store.lateralPitchRate = Math.hypot(store.flightConditions.getPitchRate(), store.flightConditions.getYawRate()); - return; - } - - - - //// Atmospheric conditions - AtmosphericConditions atmosphere = modelAtmosphericConditions(status); - store.flightConditions = new FlightConditions(status.getConfiguration()); - store.flightConditions.setAtmosphericConditions(atmosphere); - - - //// Local wind speed and direction - Coordinate windSpeed = modelWindVelocity(status); - Coordinate airSpeed = status.getRocketVelocity().add(windSpeed); - airSpeed = status.getRocketOrientationQuaternion().invRotate(airSpeed); - - - // Lateral direction: - double len = MathUtil.hypot(airSpeed.x, airSpeed.y); - if (len > 0.0001) { - store.thetaRotation = new Rotation2D(airSpeed.y / len, airSpeed.x / len); - store.flightConditions.setTheta(Math.atan2(airSpeed.y, airSpeed.x)); - } else { - store.thetaRotation = Rotation2D.ID; - store.flightConditions.setTheta(0); - } - - double velocity = airSpeed.length(); - store.flightConditions.setVelocity(velocity); - if (velocity > 0.01) { - // aoa must be calculated from the monotonous cosine - // sine can be calculated by a simple division - store.flightConditions.setAOA(Math.acos(airSpeed.z / velocity), len / velocity); - } else { - store.flightConditions.setAOA(0); - } - - - // Roll, pitch and yaw rate - Coordinate rot = status.getRocketOrientationQuaternion().invRotate(status.getRocketRotationVelocity()); - rot = store.thetaRotation.invRotateZ(rot); - - store.flightConditions.setRollRate(rot.z); - if (len < 0.001) { - store.flightConditions.setPitchRate(0); - store.flightConditions.setYawRate(0); - store.lateralPitchRate = 0; - } else { - store.flightConditions.setPitchRate(rot.y); - store.flightConditions.setYawRate(rot.x); - // TODO: LOW: set this as power of two? - store.lateralPitchRate = MathUtil.hypot(rot.x, rot.y); - } - - - // Call post listeners - FlightConditions c = SimulationListenerHelper.firePostFlightConditions( - status, store.flightConditions); - if (c != store.flightConditions) { - // Listeners changed the values, recalculate data store - store.flightConditions = c; - store.thetaRotation = new Rotation2D(store.flightConditions.getTheta()); - store.lateralPitchRate = Math.hypot(store.flightConditions.getPitchRate(), store.flightConditions.getYawRate()); - } - - } - - - - private void storeData(RK4SimulationStatus status, DataStore store) { - - FlightDataBranch data = status.getFlightData(); - boolean extra = status.getSimulationConditions().isCalculateExtras(); - - data.addPoint(); - data.setValue(FlightDataType.TYPE_TIME, status.getSimulationTime()); - data.setValue(FlightDataType.TYPE_ALTITUDE, status.getRocketPosition().z); - data.setValue(FlightDataType.TYPE_POSITION_X, status.getRocketPosition().x); - data.setValue(FlightDataType.TYPE_POSITION_Y, status.getRocketPosition().y); - - data.setValue(FlightDataType.TYPE_LATITUDE, status.getRocketWorldPosition().getLatitudeRad()); - data.setValue(FlightDataType.TYPE_LONGITUDE, status.getRocketWorldPosition().getLongitudeRad()); - if (status.getSimulationConditions().getGeodeticComputation() != GeodeticComputationStrategy.FLAT) { - data.setValue(FlightDataType.TYPE_CORIOLIS_ACCELERATION, store.coriolisAcceleration.length()); - } - - if (extra) { - data.setValue(FlightDataType.TYPE_POSITION_XY, - MathUtil.hypot(status.getRocketPosition().x, status.getRocketPosition().y)); - data.setValue(FlightDataType.TYPE_POSITION_DIRECTION, - Math.atan2(status.getRocketPosition().y, status.getRocketPosition().x)); - - data.setValue(FlightDataType.TYPE_VELOCITY_XY, - MathUtil.hypot(status.getRocketVelocity().x, status.getRocketVelocity().y)); - - if (store.linearAcceleration != null) { - data.setValue(FlightDataType.TYPE_ACCELERATION_XY, - MathUtil.hypot(store.linearAcceleration.x, store.linearAcceleration.y)); - - data.setValue(FlightDataType.TYPE_ACCELERATION_TOTAL, store.linearAcceleration.length()); - } - - if (store.flightConditions != null) { - double Re = (store.flightConditions.getVelocity() * - status.getConfiguration().getLength() / - store.flightConditions.getAtmosphericConditions().getKinematicViscosity()); - data.setValue(FlightDataType.TYPE_REYNOLDS_NUMBER, Re); - } - } - - data.setValue(FlightDataType.TYPE_VELOCITY_Z, status.getRocketVelocity().z); - if (store.linearAcceleration != null) { - data.setValue(FlightDataType.TYPE_ACCELERATION_Z, store.linearAcceleration.z); - } - - if (store.flightConditions != null) { - data.setValue(FlightDataType.TYPE_VELOCITY_TOTAL, status.getRocketVelocity().length()); - data.setValue(FlightDataType.TYPE_MACH_NUMBER, store.flightConditions.getMach()); - } - - if (store.massData != null) { - data.setValue(FlightDataType.TYPE_CG_LOCATION, store.massData.getCG().x); - } - if (status.isLaunchRodCleared()) { - // Don't include CP and stability with huge launch AOA - if (store.forces != null) { - data.setValue(FlightDataType.TYPE_CP_LOCATION, store.forces.getCP().x); - } - if (store.forces != null && store.flightConditions != null && store.massData != null) { - data.setValue(FlightDataType.TYPE_STABILITY, - (store.forces.getCP().x - store.massData.getCG().x) / store.flightConditions.getRefLength()); - } - } - if (store.massData != null) { - data.setValue(FlightDataType.TYPE_MASS, store.massData.getCG().weight); - data.setValue(FlightDataType.TYPE_LONGITUDINAL_INERTIA, store.massData.getLongitudinalInertia()); - data.setValue(FlightDataType.TYPE_ROTATIONAL_INERTIA, store.massData.getRotationalInertia()); - } - - data.setValue(FlightDataType.TYPE_THRUST_FORCE, store.thrustForce); - data.setValue(FlightDataType.TYPE_DRAG_FORCE, store.dragForce); - - if (status.isLaunchRodCleared() && store.forces != null) { - if (store.massData != null && store.flightConditions != null) { - data.setValue(FlightDataType.TYPE_PITCH_MOMENT_COEFF, - store.forces.getCm() - store.forces.getCN() * store.massData.getCG().x / store.flightConditions.getRefLength()); - data.setValue(FlightDataType.TYPE_YAW_MOMENT_COEFF, - store.forces.getCyaw() - store.forces.getCside() * store.massData.getCG().x / store.flightConditions.getRefLength()); - } - data.setValue(FlightDataType.TYPE_NORMAL_FORCE_COEFF, store.forces.getCN()); - data.setValue(FlightDataType.TYPE_SIDE_FORCE_COEFF, store.forces.getCside()); - data.setValue(FlightDataType.TYPE_ROLL_MOMENT_COEFF, store.forces.getCroll()); - data.setValue(FlightDataType.TYPE_ROLL_FORCING_COEFF, store.forces.getCrollForce()); - data.setValue(FlightDataType.TYPE_ROLL_DAMPING_COEFF, store.forces.getCrollDamp()); - data.setValue(FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF, - store.forces.getPitchDampingMoment()); - } - - if (store.forces != null) { - data.setValue(FlightDataType.TYPE_DRAG_COEFF, store.forces.getCD()); - data.setValue(FlightDataType.TYPE_AXIAL_DRAG_COEFF, store.forces.getCaxial()); - data.setValue(FlightDataType.TYPE_FRICTION_DRAG_COEFF, store.forces.getFrictionCD()); - data.setValue(FlightDataType.TYPE_PRESSURE_DRAG_COEFF, store.forces.getPressureCD()); - data.setValue(FlightDataType.TYPE_BASE_DRAG_COEFF, store.forces.getBaseCD()); - } - - if (store.flightConditions != null) { - data.setValue(FlightDataType.TYPE_REFERENCE_LENGTH, store.flightConditions.getRefLength()); - data.setValue(FlightDataType.TYPE_REFERENCE_AREA, store.flightConditions.getRefArea()); - - data.setValue(FlightDataType.TYPE_PITCH_RATE, store.flightConditions.getPitchRate()); - data.setValue(FlightDataType.TYPE_YAW_RATE, store.flightConditions.getYawRate()); - data.setValue(FlightDataType.TYPE_ROLL_RATE, store.flightConditions.getRollRate()); - - data.setValue(FlightDataType.TYPE_AOA, store.flightConditions.getAOA()); - } - - - if (extra) { - Coordinate c = status.getRocketOrientationQuaternion().rotateZ(); - double theta = Math.atan2(c.z, MathUtil.hypot(c.x, c.y)); - double phi = Math.atan2(c.y, c.x); - if (phi < -(Math.PI - 0.0001)) - phi = Math.PI; - data.setValue(FlightDataType.TYPE_ORIENTATION_THETA, theta); - data.setValue(FlightDataType.TYPE_ORIENTATION_PHI, phi); - } - - data.setValue(FlightDataType.TYPE_WIND_VELOCITY, store.windSpeed); - - if (store.flightConditions != null) { - data.setValue(FlightDataType.TYPE_AIR_TEMPERATURE, - store.flightConditions.getAtmosphericConditions().getTemperature()); - data.setValue(FlightDataType.TYPE_AIR_PRESSURE, - store.flightConditions.getAtmosphericConditions().getPressure()); - data.setValue(FlightDataType.TYPE_SPEED_OF_SOUND, - store.flightConditions.getAtmosphericConditions().getMachSpeed()); - } - - - data.setValue(FlightDataType.TYPE_TIME_STEP, store.timestep); - data.setValue(FlightDataType.TYPE_COMPUTATION_TIME, - (System.nanoTime() - status.getSimulationStartWallTime()) / 1000000000.0); - } - - - - - private static class RK4Parameters { - /** Linear acceleration */ - public Coordinate a; - /** Linear velocity */ - public Coordinate v; - /** Rotational acceleration */ - public Coordinate ra; - /** Rotational velocity */ - public Coordinate rv; - } - - private static class DataStore { - public double timestep = Double.NaN; - - public AccelerationData accelerationData; - - public AtmosphericConditions atmosphericConditions; - - public FlightConditions flightConditions; - - public double longitudinalAcceleration = Double.NaN; - - public MassData massData; - - public Coordinate coriolisAcceleration; - - public Coordinate linearAcceleration; - public Coordinate angularAcceleration; - - // set by calculateFlightConditions and calculateAcceleration: - public AerodynamicForces forces; - public double windSpeed = Double.NaN; - public double gravity = Double.NaN; - public double thrustForce = Double.NaN; - public double dragForce = Double.NaN; - public double lateralPitchRate = Double.NaN; - - public double rollAcceleration = Double.NaN; - public double lateralPitchAcceleration = Double.NaN; - - public Rotation2D thetaRotation; - - } - -} diff --git a/src/net/sf/openrocket/simulation/SimulationConditions.java b/src/net/sf/openrocket/simulation/SimulationConditions.java deleted file mode 100644 index f2412717..00000000 --- a/src/net/sf/openrocket/simulation/SimulationConditions.java +++ /dev/null @@ -1,293 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.aerodynamics.AerodynamicCalculator; -import net.sf.openrocket.masscalc.MassCalculator; -import net.sf.openrocket.models.atmosphere.AtmosphericModel; -import net.sf.openrocket.models.gravity.GravityModel; -import net.sf.openrocket.models.wind.WindModel; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.simulation.listeners.SimulationListener; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.GeodeticComputationStrategy; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.WorldCoordinate; - -/** - * A holder class for the simulation conditions. These include conditions that do not change - * during the flight of a rocket, for example launch rod parameters, atmospheric models, - * aerodynamic calculators etc. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationConditions implements Monitorable, Cloneable { - - private Rocket rocket; - private String motorID = null; - - - private double launchRodLength = 1; - - /** Launch rod angle >= 0, radians from vertical */ - private double launchRodAngle = 0; - - /** Launch rod direction, 0 = upwind, PI = downwind. */ - private double launchRodDirection = 0; - - // TODO: Depreciate these and use worldCoordinate only. - //private double launchAltitude = 0; - //private double launchLatitude = 45; - //private double launchLongitude = 0; - private WorldCoordinate launchSite = new WorldCoordinate(0, 0, 0); - private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL; - - - private WindModel windModel; - private AtmosphericModel atmosphericModel; - private GravityModel gravityModel; - - private AerodynamicCalculator aerodynamicCalculator; - private MassCalculator massCalculator; - - - private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP; - private double maximumAngleStep = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP; - - /* Whether to calculate additional data or only primary simulation figures */ - private boolean calculateExtras = true; - - - private List<SimulationListener> simulationListeners = new ArrayList<SimulationListener>(); - - - private int randomSeed = 0; - - private int modID = 0; - private int modIDadd = 0; - - - - - public AerodynamicCalculator getAerodynamicCalculator() { - return aerodynamicCalculator; - } - - - public void setAerodynamicCalculator(AerodynamicCalculator aerodynamicCalculator) { - if (this.aerodynamicCalculator != null) - this.modIDadd += this.aerodynamicCalculator.getModID(); - this.modID++; - this.aerodynamicCalculator = aerodynamicCalculator; - } - - public MassCalculator getMassCalculator() { - return massCalculator; - } - - - public void setMassCalculator(MassCalculator massCalculator) { - if (this.massCalculator != null) - this.modIDadd += this.massCalculator.getModID(); - this.modID++; - this.massCalculator = massCalculator; - } - - - public Rocket getRocket() { - return rocket; - } - - - public void setRocket(Rocket rocket) { - if (this.rocket != null) - this.modIDadd += this.rocket.getModID(); - this.modID++; - this.rocket = rocket; - } - - - public String getMotorConfigurationID() { - return motorID; - } - - - public void setMotorConfigurationID(String motorID) { - this.motorID = motorID; - this.modID++; - } - - - public double getLaunchRodLength() { - return launchRodLength; - } - - - public void setLaunchRodLength(double launchRodLength) { - this.launchRodLength = launchRodLength; - this.modID++; - } - - - public double getLaunchRodAngle() { - return launchRodAngle; - } - - - public void setLaunchRodAngle(double launchRodAngle) { - this.launchRodAngle = launchRodAngle; - this.modID++; - } - - - public double getLaunchRodDirection() { - return launchRodDirection; - } - - - public void setLaunchRodDirection(double launchRodDirection) { - this.launchRodDirection = launchRodDirection; - this.modID++; - } - - - public WorldCoordinate getLaunchSite() { - return this.launchSite; - } - - public void setLaunchSite(WorldCoordinate site) { - if (this.launchSite.equals(site)) - return; - this.launchSite = site; - this.modID++; - } - - - public GeodeticComputationStrategy getGeodeticComputation() { - return geodeticComputation; - } - - public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) { - if (this.geodeticComputation == geodeticComputation) - return; - if (geodeticComputation == null) { - throw new IllegalArgumentException("strategy cannot be null"); - } - this.geodeticComputation = geodeticComputation; - this.modID++; - } - - - public WindModel getWindModel() { - return windModel; - } - - - public void setWindModel(WindModel windModel) { - if (this.windModel != null) - this.modIDadd += this.windModel.getModID(); - this.modID++; - this.windModel = windModel; - } - - - public AtmosphericModel getAtmosphericModel() { - return atmosphericModel; - } - - - public void setAtmosphericModel(AtmosphericModel atmosphericModel) { - if (this.atmosphericModel != null) - this.modIDadd += this.atmosphericModel.getModID(); - this.modID++; - this.atmosphericModel = atmosphericModel; - } - - - public GravityModel getGravityModel() { - return gravityModel; - } - - - public void setGravityModel(GravityModel gravityModel) { - //if (this.gravityModel != null) - // this.modIDadd += this.gravityModel.getModID(); - this.modID++; - this.gravityModel = gravityModel; - } - - - public double getTimeStep() { - return timeStep; - } - - - public void setTimeStep(double timeStep) { - this.timeStep = timeStep; - this.modID++; - } - - - public double getMaximumAngleStep() { - return maximumAngleStep; - } - - - public void setMaximumAngleStep(double maximumAngle) { - this.maximumAngleStep = maximumAngle; - this.modID++; - } - - - public boolean isCalculateExtras() { - return calculateExtras; - } - - - public void setCalculateExtras(boolean calculateExtras) { - this.calculateExtras = calculateExtras; - this.modID++; - } - - - - public int getRandomSeed() { - return randomSeed; - } - - - public void setRandomSeed(int randomSeed) { - this.randomSeed = randomSeed; - this.modID++; - } - - - - - // TODO: HIGH: Make cleaner - public List<SimulationListener> getSimulationListenerList() { - return simulationListeners; - } - - - @Override - public int getModID() { - //return (modID + modIDadd + rocket.getModID() + windModel.getModID() + atmosphericModel.getModID() + - // gravityModel.getModID() + aerodynamicCalculator.getModID() + massCalculator.getModID()); - return (modID + modIDadd + rocket.getModID() + windModel.getModID() + atmosphericModel.getModID() + - aerodynamicCalculator.getModID() + massCalculator.getModID()); - } - - - @Override - public SimulationConditions clone() { - try { - // TODO: HIGH: Deep clone models - return (SimulationConditions) super.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException(e); - } - } - -} diff --git a/src/net/sf/openrocket/simulation/SimulationEngine.java b/src/net/sf/openrocket/simulation/SimulationEngine.java deleted file mode 100644 index b70e82bc..00000000 --- a/src/net/sf/openrocket/simulation/SimulationEngine.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.simulation.exception.SimulationException; - -/** - * A simulation engine that controls the flow of a simulation. This typically maintains - * flight events and related actions, while continuously calling a SimulationStepper to - * move the rocket forward step by step. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface SimulationEngine { - - /** - * Simulate the flight of a rocket. - * - * @param simulation the simulation conditions which to simulate. - * @return a FlightData object containing the simulated data. - * @throws SimulationException if an error occurs during simulation - */ - public FlightData simulate(SimulationConditions simulation) - throws SimulationException; - -} diff --git a/src/net/sf/openrocket/simulation/SimulationOptions.java b/src/net/sf/openrocket/simulation/SimulationOptions.java deleted file mode 100644 index 7734ec5d..00000000 --- a/src/net/sf/openrocket/simulation/SimulationOptions.java +++ /dev/null @@ -1,538 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.ArrayList; -import java.util.EventListener; -import java.util.EventObject; -import java.util.List; -import java.util.Random; - -import net.sf.openrocket.aerodynamics.BarrowmanCalculator; -import net.sf.openrocket.masscalc.BasicMassCalculator; -import net.sf.openrocket.models.atmosphere.AtmosphericModel; -import net.sf.openrocket.models.atmosphere.ExtendedISAModel; -import net.sf.openrocket.models.gravity.GravityModel; -import net.sf.openrocket.models.gravity.WGSGravityModel; -import net.sf.openrocket.models.wind.PinkNoiseWindModel; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.ChangeSource; -import net.sf.openrocket.util.GeodeticComputationStrategy; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.StateChangeListener; -import net.sf.openrocket.util.Utils; -import net.sf.openrocket.util.WorldCoordinate; - -/** - * A class holding simulation options in basic parameter form and which functions - * as a ChangeSource. A SimulationConditions instance is generated from this class - * using {@link #toSimulationConditions()}. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationOptions implements ChangeSource, Cloneable { - - public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI / 3; - - /** - * The ISA standard atmosphere. - */ - private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel(); - - - private final Rocket rocket; - private String motorID = null; - - - /* - * NOTE: When adding/modifying parameters, they must also be added to the - * equals and copyFrom methods!! - */ - - // TODO: HIGH: Fetch default values from Prefs! - - private double launchRodLength = 1; - - /** Launch rod angle > 0, radians from vertical */ - private double launchRodAngle = 0; - - /** Launch rod direction, 0 = upwind, PI = downwind. */ - private double launchRodDirection = 0; - - - private double windAverage = 2.0; - private double windTurbulence = 0.1; - - - /* - * SimulationOptions maintains the launch site parameters as separate double values, - * and converts them into a WorldCoordinate when converting to SimulationConditions. - */ - private double launchAltitude = 0; - private double launchLatitude = 45; - private double launchLongitude = 0; - private GeodeticComputationStrategy geodeticComputation = GeodeticComputationStrategy.SPHERICAL; - - private boolean useISA = true; - private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE; - private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE; - - - private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP; - private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP; - - private int randomSeed = new Random().nextInt(); - - private boolean calculateExtras = true; - - - private List<EventListener> listeners = new ArrayList<EventListener>(); - - - - public SimulationOptions(Rocket rocket) { - this.rocket = rocket; - } - - - public Rocket getRocket() { - return rocket; - } - - - public String getMotorConfigurationID() { - return motorID; - } - - /** - * Set the motor configuration ID. This must be a valid motor configuration ID of - * the rocket, otherwise the configuration is set to <code>null</code>. - * - * @param id the configuration to set. - */ - public void setMotorConfigurationID(String id) { - if (id != null) - id = id.intern(); - if (!rocket.isMotorConfigurationID(id)) - id = null; - if (id == motorID) - return; - motorID = id; - fireChangeEvent(); - } - - - public double getLaunchRodLength() { - return launchRodLength; - } - - public void setLaunchRodLength(double launchRodLength) { - if (MathUtil.equals(this.launchRodLength, launchRodLength)) - return; - this.launchRodLength = launchRodLength; - fireChangeEvent(); - } - - - public double getLaunchRodAngle() { - return launchRodAngle; - } - - public void setLaunchRodAngle(double launchRodAngle) { - launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE); - if (MathUtil.equals(this.launchRodAngle, launchRodAngle)) - return; - this.launchRodAngle = launchRodAngle; - fireChangeEvent(); - } - - - public double getLaunchRodDirection() { - return launchRodDirection; - } - - public void setLaunchRodDirection(double launchRodDirection) { - launchRodDirection = MathUtil.reduce180(launchRodDirection); - if (MathUtil.equals(this.launchRodDirection, launchRodDirection)) - return; - this.launchRodDirection = launchRodDirection; - fireChangeEvent(); - } - - - - public double getWindSpeedAverage() { - return windAverage; - } - - public void setWindSpeedAverage(double windAverage) { - if (MathUtil.equals(this.windAverage, windAverage)) - return; - this.windAverage = MathUtil.max(windAverage, 0); - fireChangeEvent(); - } - - - public double getWindSpeedDeviation() { - return windAverage * windTurbulence; - } - - public void setWindSpeedDeviation(double windDeviation) { - if (windAverage < 0.1) { - windAverage = 0.1; - } - setWindTurbulenceIntensity(windDeviation / windAverage); - } - - - /** - * Return the wind turbulence intensity (standard deviation / average). - * - * @return the turbulence intensity - */ - public double getWindTurbulenceIntensity() { - return windTurbulence; - } - - /** - * Set the wind standard deviation to match the given turbulence intensity. - * - * @param intensity the turbulence intensity - */ - public void setWindTurbulenceIntensity(double intensity) { - // Does not check equality so that setWindSpeedDeviation can be sure of event firing - this.windTurbulence = intensity; - fireChangeEvent(); - } - - - - - - public double getLaunchAltitude() { - return launchAltitude; - } - - public void setLaunchAltitude(double altitude) { - if (MathUtil.equals(this.launchAltitude, altitude)) - return; - this.launchAltitude = altitude; - fireChangeEvent(); - } - - - public double getLaunchLatitude() { - return launchLatitude; - } - - public void setLaunchLatitude(double launchLatitude) { - launchLatitude = MathUtil.clamp(launchLatitude, -90, 90); - if (MathUtil.equals(this.launchLatitude, launchLatitude)) - return; - this.launchLatitude = launchLatitude; - fireChangeEvent(); - } - - public double getLaunchLongitude() { - return launchLongitude; - } - - public void setLaunchLongitude(double launchLongitude) { - launchLongitude = MathUtil.clamp(launchLongitude, -180, 180); - if (MathUtil.equals(this.launchLongitude, launchLongitude)) - return; - this.launchLongitude = launchLongitude; - fireChangeEvent(); - } - - - public GeodeticComputationStrategy getGeodeticComputation() { - return geodeticComputation; - } - - public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) { - if (this.geodeticComputation == geodeticComputation) - return; - if (geodeticComputation == null) { - throw new IllegalArgumentException("strategy cannot be null"); - } - this.geodeticComputation = geodeticComputation; - fireChangeEvent(); - } - - - public boolean isISAAtmosphere() { - return useISA; - } - - public void setISAAtmosphere(boolean isa) { - if (isa == useISA) - return; - useISA = isa; - fireChangeEvent(); - } - - - public double getLaunchTemperature() { - return launchTemperature; - } - - - - public void setLaunchTemperature(double launchTemperature) { - if (MathUtil.equals(this.launchTemperature, launchTemperature)) - return; - this.launchTemperature = launchTemperature; - fireChangeEvent(); - } - - - - public double getLaunchPressure() { - return launchPressure; - } - - - - public void setLaunchPressure(double launchPressure) { - if (MathUtil.equals(this.launchPressure, launchPressure)) - return; - this.launchPressure = launchPressure; - fireChangeEvent(); - } - - - /** - * Returns an atmospheric model corresponding to the launch conditions. The - * atmospheric models may be shared between different calls. - * - * @return an AtmosphericModel object. - */ - private AtmosphericModel getAtmosphericModel() { - if (useISA) { - return ISA_ATMOSPHERIC_MODEL; - } - return new ExtendedISAModel(getLaunchAltitude(), launchTemperature, launchPressure); - } - - - public double getTimeStep() { - return timeStep; - } - - public void setTimeStep(double timeStep) { - if (MathUtil.equals(this.timeStep, timeStep)) - return; - this.timeStep = timeStep; - fireChangeEvent(); - } - - public double getMaximumStepAngle() { - return maximumAngle; - } - - public void setMaximumStepAngle(double maximumAngle) { - maximumAngle = MathUtil.clamp(maximumAngle, 1 * Math.PI / 180, 20 * Math.PI / 180); - if (MathUtil.equals(this.maximumAngle, maximumAngle)) - return; - this.maximumAngle = maximumAngle; - fireChangeEvent(); - } - - - - public boolean getCalculateExtras() { - return calculateExtras; - } - - - - public void setCalculateExtras(boolean calculateExtras) { - if (this.calculateExtras == calculateExtras) - return; - this.calculateExtras = calculateExtras; - fireChangeEvent(); - } - - - - public int getRandomSeed() { - return randomSeed; - } - - public void setRandomSeed(int randomSeed) { - if (this.randomSeed == randomSeed) { - return; - } - this.randomSeed = randomSeed; - /* - * This does not fire an event since we don't want to invalidate simulation results - * due to changing the seed value. This needs to be revisited if the user is ever - * allowed to select the seed value. - */ - // fireChangeEvent(); - } - - /** - * Randomize the random seed value. - */ - public void randomizeSeed() { - this.randomSeed = new Random().nextInt(); - // fireChangeEvent(); - } - - - - @Override - public SimulationOptions clone() { - try { - SimulationOptions copy = (SimulationOptions) super.clone(); - copy.listeners = new ArrayList<EventListener>(); - return copy; - } catch (CloneNotSupportedException e) { - throw new BugException(e); - } - } - - - public void copyFrom(SimulationOptions src) { - - if (this.rocket == src.rocket) { - - this.motorID = src.motorID; - - } else { - - if (src.rocket.hasMotors(src.motorID)) { - // Try to find a matching motor ID - String motorDesc = src.rocket.getMotorConfigurationDescription(src.motorID); - String matchID = null; - - for (String id : this.rocket.getMotorConfigurationIDs()) { - if (motorDesc.equals(this.rocket.getMotorConfigurationDescription(id))) { - matchID = id; - break; - } - } - - this.motorID = matchID; - } else { - this.motorID = null; - } - } - - this.launchAltitude = src.launchAltitude; - this.launchLatitude = src.launchLatitude; - this.launchLongitude = src.launchLongitude; - this.launchPressure = src.launchPressure; - this.launchRodAngle = src.launchRodAngle; - this.launchRodDirection = src.launchRodDirection; - this.launchRodLength = src.launchRodLength; - this.launchTemperature = src.launchTemperature; - this.maximumAngle = src.maximumAngle; - this.timeStep = src.timeStep; - this.windAverage = src.windAverage; - this.windTurbulence = src.windTurbulence; - this.calculateExtras = src.calculateExtras; - this.randomSeed = src.randomSeed; - - fireChangeEvent(); - } - - - - /** - * Compares whether the two simulation conditions are equal. The two are considered - * equal if the rocket, motor id and all variables are equal. - */ - @Override - public boolean equals(Object other) { - if (!(other instanceof SimulationOptions)) - return false; - SimulationOptions o = (SimulationOptions) other; - return ((this.rocket == o.rocket) && - Utils.equals(this.motorID, o.motorID) && - MathUtil.equals(this.launchAltitude, o.launchAltitude) && - MathUtil.equals(this.launchLatitude, o.launchLatitude) && - MathUtil.equals(this.launchLongitude, o.launchLongitude) && - MathUtil.equals(this.launchPressure, o.launchPressure) && - MathUtil.equals(this.launchRodAngle, o.launchRodAngle) && - MathUtil.equals(this.launchRodDirection, o.launchRodDirection) && - MathUtil.equals(this.launchRodLength, o.launchRodLength) && - MathUtil.equals(this.launchTemperature, o.launchTemperature) && - MathUtil.equals(this.maximumAngle, o.maximumAngle) && - MathUtil.equals(this.timeStep, o.timeStep) && - MathUtil.equals(this.windAverage, o.windAverage) && - MathUtil.equals(this.windTurbulence, o.windTurbulence) && - this.calculateExtras == o.calculateExtras && this.randomSeed == o.randomSeed); - } - - /** - * Hashcode method compatible with {@link #equals(Object)}. - */ - @Override - public int hashCode() { - if (motorID == null) - return rocket.hashCode(); - return rocket.hashCode() + motorID.hashCode(); - } - - @Override - public void addChangeListener(EventListener listener) { - listeners.add(listener); - } - - @Override - public void removeChangeListener(EventListener listener) { - listeners.remove(listener); - } - - private final EventObject event = new EventObject(this); - - private void fireChangeEvent() { - - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] list = listeners.toArray(new EventListener[0]); - for (EventListener l : list) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(event); - } - } - } - - - // TODO: HIGH: Clean up - public SimulationConditions toSimulationConditions() { - SimulationConditions conditions = new SimulationConditions(); - - conditions.setRocket((Rocket) getRocket().copy()); - conditions.setMotorConfigurationID(getMotorConfigurationID()); - conditions.setLaunchRodLength(getLaunchRodLength()); - conditions.setLaunchRodAngle(getLaunchRodAngle()); - conditions.setLaunchRodDirection(getLaunchRodDirection()); - conditions.setLaunchSite(new WorldCoordinate(getLaunchLatitude(), getLaunchLongitude(), getLaunchAltitude())); - conditions.setGeodeticComputation(getGeodeticComputation()); - conditions.setRandomSeed(randomSeed); - - PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed); - windModel.setAverage(getWindSpeedAverage()); - windModel.setStandardDeviation(getWindSpeedDeviation()); - conditions.setWindModel(windModel); - - conditions.setAtmosphericModel(getAtmosphericModel()); - - GravityModel gravityModel = new WGSGravityModel(); - - conditions.setGravityModel(gravityModel); - - conditions.setAerodynamicCalculator(new BarrowmanCalculator()); - conditions.setMassCalculator(new BasicMassCalculator()); - - conditions.setTimeStep(getTimeStep()); - conditions.setMaximumAngleStep(getMaximumStepAngle()); - - conditions.setCalculateExtras(getCalculateExtras()); - - return conditions; - } - -} diff --git a/src/net/sf/openrocket/simulation/SimulationStatus.java b/src/net/sf/openrocket/simulation/SimulationStatus.java deleted file mode 100644 index 9a39cffd..00000000 --- a/src/net/sf/openrocket/simulation/SimulationStatus.java +++ /dev/null @@ -1,389 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import net.sf.openrocket.aerodynamics.WarningSet; -import net.sf.openrocket.motor.MotorInstanceConfiguration; -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.Monitorable; -import net.sf.openrocket.util.MonitorableSet; -import net.sf.openrocket.util.Quaternion; -import net.sf.openrocket.util.WorldCoordinate; - -/** - * A holder class for the dynamic status during the rocket's flight. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationStatus implements Cloneable, Monitorable { - - /* - * NOTE! All fields must be added to copyFrom() method!! - */ - - private SimulationConditions simulationConditions; - private Configuration configuration; - private MotorInstanceConfiguration motorConfiguration; - private FlightDataBranch flightData; - - private double time; - - private double previousTimeStep; - - private Coordinate position; - private WorldCoordinate worldPosition; - private Coordinate velocity; - - private Quaternion orientation; - private Coordinate rotationVelocity; - - private double effectiveLaunchRodLength; - - - /** Nanosecond time when the simulation was started. */ - private long simulationStartWallTime = Long.MIN_VALUE; - - - /** Set to true when a motor has ignited. */ - private boolean motorIgnited = false; - - /** Set to true when the rocket has risen from the ground. */ - private boolean liftoff = false; - - /** Set to true when the launch rod has been cleared. */ - private boolean launchRodCleared = false; - - /** Set to true when apogee has been detected. */ - private boolean apogeeReached = false; - - /** Contains a list of deployed recovery devices. */ - private MonitorableSet<RecoveryDevice> deployedRecoveryDevices = new MonitorableSet<RecoveryDevice>(); - - /** The flight event queue */ - private final EventQueue eventQueue = new EventQueue(); - - private WarningSet warnings; - - /** Available for special purposes by the listeners. */ - private final Map<String, Object> extraData = new HashMap<String, Object>(); - - - private int modID = 0; - private int modIDadd = 0; - - - public void setSimulationTime(double time) { - this.time = time; - this.modID++; - } - - - public double getSimulationTime() { - return time; - } - - - public void setConfiguration(Configuration configuration) { - if (this.configuration != null) - this.modIDadd += this.configuration.getModID(); - this.modID++; - this.configuration = configuration; - } - - - public Configuration getConfiguration() { - return configuration; - } - - - public void setMotorConfiguration(MotorInstanceConfiguration motorConfiguration) { - if (this.motorConfiguration != null) - this.modIDadd += this.motorConfiguration.getModID(); - this.modID++; - this.motorConfiguration = motorConfiguration; - } - - - public MotorInstanceConfiguration getMotorConfiguration() { - return motorConfiguration; - } - - - public void setFlightData(FlightDataBranch flightData) { - if (this.flightData != null) - this.modIDadd += this.flightData.getModID(); - this.modID++; - this.flightData = flightData; - } - - - public FlightDataBranch getFlightData() { - return flightData; - } - - - public double getPreviousTimeStep() { - return previousTimeStep; - } - - - public void setPreviousTimeStep(double previousTimeStep) { - this.previousTimeStep = previousTimeStep; - this.modID++; - } - - - public void setRocketPosition(Coordinate position) { - this.position = position; - this.modID++; - } - - - public Coordinate getRocketPosition() { - return position; - } - - public void setRocketWorldPosition(WorldCoordinate wc) { - this.worldPosition = wc; - this.modID++; - } - - public WorldCoordinate getRocketWorldPosition() { - return worldPosition; - } - - public void setRocketVelocity(Coordinate velocity) { - this.velocity = velocity; - this.modID++; - } - - - public Coordinate getRocketVelocity() { - return velocity; - } - - - - - - public Quaternion getRocketOrientationQuaternion() { - return orientation; - } - - - public void setRocketOrientationQuaternion(Quaternion orientation) { - this.orientation = orientation; - this.modID++; - } - - - public Coordinate getRocketRotationVelocity() { - return rotationVelocity; - } - - - public void setRocketRotationVelocity(Coordinate rotation) { - this.rotationVelocity = rotation; - } - - - public void setEffectiveLaunchRodLength(double effectiveLaunchRodLength) { - this.effectiveLaunchRodLength = effectiveLaunchRodLength; - this.modID++; - } - - - public double getEffectiveLaunchRodLength() { - return effectiveLaunchRodLength; - } - - - public void setSimulationStartWallTime(long simulationStartWallTime) { - this.simulationStartWallTime = simulationStartWallTime; - this.modID++; - } - - - public long getSimulationStartWallTime() { - return simulationStartWallTime; - } - - - public void setMotorIgnited(boolean motorIgnited) { - this.motorIgnited = motorIgnited; - this.modID++; - } - - - public boolean isMotorIgnited() { - return motorIgnited; - } - - - public void setLiftoff(boolean liftoff) { - this.liftoff = liftoff; - this.modID++; - } - - - public boolean isLiftoff() { - return liftoff; - } - - - public void setLaunchRodCleared(boolean launchRod) { - this.launchRodCleared = launchRod; - this.modID++; - } - - - public boolean isLaunchRodCleared() { - return launchRodCleared; - } - - - public void setApogeeReached(boolean apogeeReached) { - this.apogeeReached = apogeeReached; - this.modID++; - } - - - public boolean isApogeeReached() { - return apogeeReached; - } - - - public Set<RecoveryDevice> getDeployedRecoveryDevices() { - return deployedRecoveryDevices; - } - - - public void setWarnings(WarningSet warnings) { - if (this.warnings != null) - this.modIDadd += this.warnings.getModID(); - this.modID++; - this.warnings = warnings; - } - - - public WarningSet getWarnings() { - return warnings; - } - - - public EventQueue getEventQueue() { - return eventQueue; - } - - - public void setSimulationConditions(SimulationConditions simulationConditions) { - if (this.simulationConditions != null) - this.modIDadd += this.simulationConditions.getModID(); - this.modID++; - this.simulationConditions = simulationConditions; - } - - - public SimulationConditions getSimulationConditions() { - return simulationConditions; - } - - - /** - * Store extra data available for use by simulation listeners. The data can be retrieved - * using {@link #getExtraData(String)}. - * - * @param key the data key - * @param value the value to store - */ - public void putExtraData(String key, Object value) { - extraData.put(key, value); - } - - /** - * Retrieve extra data stored by simulation listeners. This data map is initially empty. - * Data can be stored using {@link #putExtraData(String, Object)}. - * - * @param key the data key to retrieve - * @return the data, or <code>null</code> if nothing has been set for the key - */ - public Object getExtraData(String key) { - return extraData.get(key); - } - - - /** - * Returns a copy of this object. The general purpose is that the conditions, - * rocket configuration, flight data etc. point to the same objects. However, - * subclasses are allowed to deep-clone specific objects, such as those pertaining - * to the current orientation of the rocket. The purpose is to allow creating intermediate - * copies of this object used during step computation. - * - * TODO: HIGH: Deep cloning required for branch saving. - */ - @Override - public SimulationStatus clone() { - try { - SimulationStatus clone = (SimulationStatus) super.clone(); - return clone; - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException?!?", e); - } - } - - - /** - * Copies the data from the provided object to this object. Most included object are - * deep-cloned, except for the flight data object. - * - * @param orig the object from which to copy - */ - public void copyFrom(SimulationStatus orig) { - this.simulationConditions = orig.simulationConditions.clone(); - this.configuration = orig.configuration.clone(); - this.motorConfiguration = orig.motorConfiguration.clone(); - this.flightData = orig.flightData; - this.time = orig.time; - this.previousTimeStep = orig.previousTimeStep; - this.position = orig.position; - this.worldPosition = orig.worldPosition; - this.velocity = orig.velocity; - this.orientation = orig.orientation; - this.rotationVelocity = orig.rotationVelocity; - this.effectiveLaunchRodLength = orig.effectiveLaunchRodLength; - this.simulationStartWallTime = orig.simulationStartWallTime; - this.motorIgnited = orig.motorIgnited; - this.liftoff = orig.liftoff; - this.launchRodCleared = orig.launchRodCleared; - this.apogeeReached = orig.apogeeReached; - - this.deployedRecoveryDevices.clear(); - this.deployedRecoveryDevices.addAll(orig.deployedRecoveryDevices); - - this.eventQueue.clear(); - this.eventQueue.addAll(orig.eventQueue); - - this.warnings = orig.warnings; - - this.extraData.clear(); - this.extraData.putAll(orig.extraData); - - this.modID = orig.modID; - this.modIDadd = orig.modIDadd; - } - - - @Override - public int getModID() { - return (modID + modIDadd + simulationConditions.getModID() + configuration.getModID() + - motorConfiguration.getModID() + flightData.getModID() + deployedRecoveryDevices.getModID() + - eventQueue.getModID() + warnings.getModID()); - } - - -} diff --git a/src/net/sf/openrocket/simulation/SimulationStepper.java b/src/net/sf/openrocket/simulation/SimulationStepper.java deleted file mode 100644 index 90855c3c..00000000 --- a/src/net/sf/openrocket/simulation/SimulationStepper.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.simulation; - -import net.sf.openrocket.simulation.exception.SimulationException; - -public interface SimulationStepper { - - /** - * Initialize a simulation using this simulation stepper based on the provided - * current simulation status and launch conditions. - * - * @param status the current simulation status. - * @return a SimulationStatus suitable for simulating with this simulation stepper. - */ - public SimulationStatus initialize(SimulationStatus status) throws SimulationException; - - /** - * Perform one simulation time step. - * - * @param status the current simulation status, of a type returned by {@link #initialize(SimulationStatus)}. - * @param maxTimeStep the maximum time step to take. This is an upper bound and can be used to limit a stepper - * from stepping over upcoming flight events (motor ignition etc). - */ - public void step(SimulationStatus status, double maxTimeStep) throws SimulationException; - -} diff --git a/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java b/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java deleted file mode 100644 index fd9e60ac..00000000 --- a/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.sf.openrocket.simulation.exception; - -/** - * An exception signifying that the simulation failed because no motors were - * defined or ignited in the rocket. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MotorIgnitionException extends SimulationLaunchException { - - public MotorIgnitionException(String message) { - super(message); - } - - public MotorIgnitionException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/simulation/exception/SimulationCalculationException.java b/src/net/sf/openrocket/simulation/exception/SimulationCalculationException.java deleted file mode 100644 index 40f86321..00000000 --- a/src/net/sf/openrocket/simulation/exception/SimulationCalculationException.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.sf.openrocket.simulation.exception; - -/** - * An exception that indicates that a computation problem has occurred during - * the simulation, for example that some values have exceed reasonable bounds. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationCalculationException extends SimulationException { - - public SimulationCalculationException() { - } - - public SimulationCalculationException(String message) { - super(message); - } - - public SimulationCalculationException(Throwable cause) { - super(cause); - } - - public SimulationCalculationException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java b/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java deleted file mode 100644 index e2293432..00000000 --- a/src/net/sf/openrocket/simulation/exception/SimulationCancelledException.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.simulation.exception; - - -/** - * An exception signifying that a simulation was cancelled. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationCancelledException extends SimulationException { - - public SimulationCancelledException() { - - } - - public SimulationCancelledException(String message) { - super(message); - } - - public SimulationCancelledException(Throwable cause) { - super(cause); - } - - public SimulationCancelledException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/simulation/exception/SimulationException.java b/src/net/sf/openrocket/simulation/exception/SimulationException.java deleted file mode 100644 index f180f892..00000000 --- a/src/net/sf/openrocket/simulation/exception/SimulationException.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.sf.openrocket.simulation.exception; - -public class SimulationException extends Exception { - - public SimulationException() { - - } - - public SimulationException(String message) { - super(message); - } - - public SimulationException(Throwable cause) { - super(cause); - } - - public SimulationException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java b/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java deleted file mode 100644 index df9f3862..00000000 --- a/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.sf.openrocket.simulation.exception; - -/** - * An exception signifying that a problem occurred at launch, for example - * that no motors were defined or no motors ignited. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationLaunchException extends SimulationException { - - public SimulationLaunchException(String message) { - super(message); - } - - public SimulationLaunchException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java b/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java deleted file mode 100644 index d6bd737a..00000000 --- a/src/net/sf/openrocket/simulation/exception/SimulationListenerException.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.sf.openrocket.simulation.exception; - - -public class SimulationListenerException extends SimulationException { - - public SimulationListenerException() { - } - - public SimulationListenerException(String message) { - super(message); - } - - public SimulationListenerException(Throwable cause) { - super(cause); - } - - public SimulationListenerException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java b/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java deleted file mode 100644 index 66618258..00000000 --- a/src/net/sf/openrocket/simulation/exception/SimulationNotSupportedException.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.sf.openrocket.simulation.exception; - - -/** - * A exception that signifies that the attempted simulation is not supported. - * The reason for not being supported may be due to unsupported combination of - * simulator/calculator, unsupported rocket structure or other reasons. - * <p> - * This exception signifies a fatal problem in the simulation; for non-fatal conditions - * add a warning to the simulation results. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationNotSupportedException extends SimulationException { - - public SimulationNotSupportedException() { - } - - public SimulationNotSupportedException(String message) { - super(message); - } - - public SimulationNotSupportedException(Throwable cause) { - super(cause); - } - - public SimulationNotSupportedException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java b/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java deleted file mode 100644 index ebb2ad94..00000000 --- a/src/net/sf/openrocket/simulation/listeners/AbstractSimulationListener.java +++ /dev/null @@ -1,169 +0,0 @@ -package net.sf.openrocket.simulation.listeners; - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.motor.MotorId; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.MassData; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.util.Coordinate; - - -/** - * An abstract base class for implementing simulation listeners. This class implements all - * of the simulation listener interfaces using methods that have no effect on the simulation. - * The recommended way of implementing simulation listeners is to extend this class. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class AbstractSimulationListener implements SimulationListener, SimulationComputationListener, - SimulationEventListener { - - //// SimulationListener //// - - @Override - public void startSimulation(SimulationStatus status) throws SimulationException { - // No-op - } - - @Override - public void endSimulation(SimulationStatus status, SimulationException exception) { - // No-op - } - - @Override - public boolean preStep(SimulationStatus status) throws SimulationException { - return true; - } - - @Override - public void postStep(SimulationStatus status) throws SimulationException { - // No-op - } - - /** - * {@inheritDoc} - * <p> - * <em>This implementation of the method always returns <code>false</code>.</em> - */ - @Override - public boolean isSystemListener() { - return false; - } - - - - - //// SimulationEventListener //// - - @Override - public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { - return true; - } - - @Override - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { - return true; - } - - @Override - public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, MotorInstance instance) throws SimulationException { - return true; - } - - @Override - public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { - return true; - } - - - - //// SimulationComputationListener //// - - @Override - public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException { - return null; - } - - @Override - public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) throws SimulationException { - return null; - } - - @Override - public AtmosphericConditions preAtmosphericModel(SimulationStatus status) throws SimulationException { - return null; - } - - @Override - public FlightConditions preFlightConditions(SimulationStatus status) throws SimulationException { - return null; - } - - @Override - public double preGravityModel(SimulationStatus status) throws SimulationException { - return Double.NaN; - } - - @Override - public MassData preMassCalculation(SimulationStatus status) throws SimulationException { - return null; - } - - @Override - public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException { - return Double.NaN; - } - - @Override - public Coordinate preWindModel(SimulationStatus status) throws SimulationException { - return null; - } - - @Override - public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) throws SimulationException { - return null; - } - - @Override - public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) throws SimulationException { - return null; - } - - @Override - public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) throws SimulationException { - return null; - } - - @Override - public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) throws SimulationException { - return null; - } - - @Override - public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException { - return Double.NaN; - } - - @Override - public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException { - return null; - } - - @Override - public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { - return Double.NaN; - } - - @Override - public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { - return null; - } - -} diff --git a/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java b/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java deleted file mode 100644 index be46c50f..00000000 --- a/src/net/sf/openrocket/simulation/listeners/SimulationComputationListener.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.sf.openrocket.simulation.listeners; - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.MassData; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.util.Coordinate; - -/** - * An interface containing listener callbacks relating to different computational aspects performed - * during flight. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface SimulationComputationListener extends SimulationListener { - - - //////// Computation/modeling related callbacks //////// - - public AccelerationData preAccelerationCalculation(SimulationStatus status) throws SimulationException; - - public AccelerationData postAccelerationCalculation(SimulationStatus status, AccelerationData acceleration) - throws SimulationException; - - public AtmosphericConditions preAtmosphericModel(SimulationStatus status) - throws SimulationException; - - public AtmosphericConditions postAtmosphericModel(SimulationStatus status, AtmosphericConditions atmosphericConditions) - throws SimulationException; - - - public Coordinate preWindModel(SimulationStatus status) throws SimulationException; - - public Coordinate postWindModel(SimulationStatus status, Coordinate wind) throws SimulationException; - - - public double preGravityModel(SimulationStatus status) throws SimulationException; - - public double postGravityModel(SimulationStatus status, double gravity) throws SimulationException; - - - public FlightConditions preFlightConditions(SimulationStatus status) - throws SimulationException; - - public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) - throws SimulationException; - - - public AerodynamicForces preAerodynamicCalculation(SimulationStatus status) - throws SimulationException; - - public AerodynamicForces postAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) - throws SimulationException; - - public MassData preMassCalculation(SimulationStatus status) throws SimulationException; - - public MassData postMassCalculation(SimulationStatus status, MassData massData) throws SimulationException; - - - public double preSimpleThrustCalculation(SimulationStatus status) throws SimulationException; - - public double postSimpleThrustCalculation(SimulationStatus status, double thrust) throws SimulationException; - -} diff --git a/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java b/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java deleted file mode 100644 index 6cbb87f3..00000000 --- a/src/net/sf/openrocket/simulation/listeners/SimulationEventListener.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.sf.openrocket.simulation.listeners; - -import net.sf.openrocket.motor.MotorId; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; - -public interface SimulationEventListener { - - - /** - * Called before adding a flight event to the event queue. - * - * @param status the simulation status - * @param event the event that is being added - * @return <code>true</code> to add the event, - * <code>false</code> to abort adding event to event queue - */ - public boolean addFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; - - - - /** - * Called before handling a flight event. - * - * @param status the simulation status - * @param event the event that is taking place - * @return <code>true</code> to continue handling the event, - * <code>false</code> to abort handling - */ - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException; - - - /** - * Motor ignition event. - * - * @param status the simulation status - * @param motorId the motor id in the MotorInstanceConfiguration - * @param mount the motor mount containing the motor - * @param instance the motor instance being ignited - * @return <code>true</code> to ignite the motor, <code>false</code> to abort ignition - */ - public boolean motorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, - MotorInstance instance) throws SimulationException; - - - /** - * Recovery device deployment. - * - * @param status the simulation status - * @param recoveryDevice the recovery device that is being deployed. - * @return <code>true</code> to deploy the recovery device, <code>false</code> to abort deployment - */ - public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) - throws SimulationException; - - -} diff --git a/src/net/sf/openrocket/simulation/listeners/SimulationListener.java b/src/net/sf/openrocket/simulation/listeners/SimulationListener.java deleted file mode 100644 index 98b28aad..00000000 --- a/src/net/sf/openrocket/simulation/listeners/SimulationListener.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.sf.openrocket.simulation.listeners; - -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; - - - -public interface SimulationListener { - - /** - * Called when starting a simulation. - * - * @param status the simulation status. - */ - public void startSimulation(SimulationStatus status) throws SimulationException; - - - /** - * Called when ending a simulation. This is called either when the simulation ends normally - * (due to an end simulation event) or when a SimulationException is thrown. - * <p> - * This method cannot throw a SimulationException, since the simulation is already being ended. - * - * @param status the simulation status. - * @param exception the exception that caused ending the simulation, or <code>null</code> if ending normally. - */ - public void endSimulation(SimulationStatus status, SimulationException exception); - - - /** - * Called before a simulation step is taken. This method may also prevent the normal - * stepping method from being called. - * - * @param status the simulation status. - * @return <code>true</code> to continue normally, <code>false</code> to skip taking the step - */ - public boolean preStep(SimulationStatus status) throws SimulationException; - - - /** - * Called immediately after a simulation step has been taken. This method is called whether the - * {@link #preStep(SimulationStatus)} aborted the step or not. - * - * @param status the simulation status. - */ - public void postStep(SimulationStatus status) throws SimulationException; - - - /** - * Return whether this is a system listener. System listeners are used internally for various - * purposes by OpenRocket. User-written listeners should always return <code>false</code>. - * <p> - * System listeners do not cause warnings to be added to the simulation results when they affect - * the simulation. - * - * @return whether this is a system listener - */ - public boolean isSystemListener(); - -} diff --git a/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java b/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java deleted file mode 100644 index be658f15..00000000 --- a/src/net/sf/openrocket/simulation/listeners/SimulationListenerHelper.java +++ /dev/null @@ -1,668 +0,0 @@ -package net.sf.openrocket.simulation.listeners; - - -import net.sf.openrocket.aerodynamics.AerodynamicForces; -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.aerodynamics.Warning; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.motor.MotorId; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.rocketcomponent.MotorMount; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.AccelerationData; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.MassData; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.Coordinate; -import net.sf.openrocket.util.MathUtil; - -/** - * Helper methods for firing events to simulation listeners. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class SimulationListenerHelper { - - private static final LogHelper log = Application.getLogger(); - - //////// SimulationListener methods //////// - - - /** - * Fire startSimulation event. - */ - public static void fireStartSimulation(SimulationStatus status) - throws SimulationException { - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - l.startSimulation(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - } - } - - - /** - * Fire endSimulation event. - */ - public static void fireEndSimulation(SimulationStatus status, SimulationException exception) { - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - l.endSimulation(status, exception); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - } - } - - - - - /** - * Fire preStep event. - * - * @return <code>true</code> to handle step normally, <code>false</code> to skip the step. - */ - public static boolean firePreStep(SimulationStatus status) - throws SimulationException { - boolean b; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - b = l.preStep(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (b == false) { - warn(status, l); - return false; - } - } - return true; - } - - - /** - * Fire postStep event. - */ - public static void firePostStep(SimulationStatus status) - throws SimulationException { - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - l.postStep(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - } - } - - - - //////// SimulationEventListener methods //////// - - /** - * Fire an add flight event event. - * - * @return <code>true</code> to add the event normally, <code>false</code> to skip adding the event. - */ - public static boolean fireAddFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { - boolean b; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationEventListener) { - b = ((SimulationEventListener) l).addFlightEvent(status, event); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (b == false) { - warn(status, l); - return false; - } - } - } - return true; - } - - /** - * Fire a handle flight event event. - * - * @return <code>true</code> to handle the event normally, <code>false</code> to skip event. - */ - public static boolean fireHandleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { - boolean b; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationEventListener) { - b = ((SimulationEventListener) l).handleFlightEvent(status, event); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (b == false) { - warn(status, l); - return false; - } - } - } - return true; - } - - /** - * Fire motor ignition event. - * - * @return <code>true</code> to handle the event normally, <code>false</code> to skip event. - */ - public static boolean fireMotorIgnition(SimulationStatus status, MotorId motorId, MotorMount mount, - MotorInstance instance) throws SimulationException { - boolean b; - int modID = status.getModID(); // Contains also motor instance - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationEventListener) { - b = ((SimulationEventListener) l).motorIgnition(status, motorId, mount, instance); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (b == false) { - warn(status, l); - return false; - } - } - } - return true; - } - - - /** - * Fire recovery device deployment event. - * - * @return <code>true</code> to handle the event normally, <code>false</code> to skip event. - */ - public static boolean fireRecoveryDeviceDeployment(SimulationStatus status, RecoveryDevice device) - throws SimulationException { - boolean b; - int modID = status.getModID(); // Contains also motor instance - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationEventListener) { - b = ((SimulationEventListener) l).recoveryDeviceDeployment(status, device); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (b == false) { - warn(status, l); - return false; - } - } - } - return true; - } - - - //////// SimulationComputationalListener methods //////// - - /** - * Fire preAtmosphericModel event. - * - * @return <code>null</code> normally, or overriding atmospheric conditions. - */ - public static AtmosphericConditions firePreAtmosphericModel(SimulationStatus status) - throws SimulationException { - AtmosphericConditions conditions; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - conditions = ((SimulationComputationListener) l).preAtmosphericModel(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (conditions != null) { - warn(status, l); - return conditions; - } - } - } - return null; - } - - /** - * Fire postAtmosphericModel event. - * - * @return the atmospheric conditions to use. - */ - public static AtmosphericConditions firePostAtmosphericModel(SimulationStatus status, AtmosphericConditions conditions) - throws SimulationException { - AtmosphericConditions c; - AtmosphericConditions clone = conditions.clone(); - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - c = ((SimulationComputationListener) l).postAtmosphericModel(status, clone); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (c != null && !c.equals(conditions)) { - warn(status, l); - conditions = c; - clone = conditions.clone(); - } - } - } - return conditions; - } - - - - /** - * Fire preWindModel event. - * - * @return <code>null</code> normally, or overriding wind. - */ - public static Coordinate firePreWindModel(SimulationStatus status) - throws SimulationException { - Coordinate wind; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - wind = ((SimulationComputationListener) l).preWindModel(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (wind != null) { - warn(status, l); - return wind; - } - } - } - return null; - } - - /** - * Fire postWindModel event. - * - * @return the wind to use. - */ - public static Coordinate firePostWindModel(SimulationStatus status, Coordinate wind) throws SimulationException { - Coordinate w; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - w = ((SimulationComputationListener) l).postWindModel(status, wind); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (w != null && !w.equals(wind)) { - warn(status, l); - wind = w; - } - } - } - return wind; - } - - - - /** - * Fire preGravityModel event. - * - * @return <code>NaN</code> normally, or overriding gravity. - */ - public static double firePreGravityModel(SimulationStatus status) - throws SimulationException { - double gravity; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - gravity = ((SimulationComputationListener) l).preGravityModel(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (!Double.isNaN(gravity)) { - warn(status, l); - return gravity; - } - } - } - return Double.NaN; - } - - /** - * Fire postGravityModel event. - * - * @return the gravity to use. - */ - public static double firePostGravityModel(SimulationStatus status, double gravity) throws SimulationException { - double g; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - g = ((SimulationComputationListener) l).postGravityModel(status, gravity); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (!Double.isNaN(g) && !MathUtil.equals(g, gravity)) { - warn(status, l); - gravity = g; - } - } - } - return gravity; - } - - - - - /** - * Fire preFlightConditions event. - * - * @return <code>null</code> normally, or overriding flight conditions. - */ - public static FlightConditions firePreFlightConditions(SimulationStatus status) - throws SimulationException { - FlightConditions conditions; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - conditions = ((SimulationComputationListener) l).preFlightConditions(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (conditions != null) { - warn(status, l); - return conditions; - } - } - } - return null; - } - - /** - * Fire postFlightConditions event. - * - * @return the flight conditions to use: either <code>conditions</code> or a new object - * containing the modified conditions. - */ - public static FlightConditions firePostFlightConditions(SimulationStatus status, FlightConditions conditions) - throws SimulationException { - FlightConditions c; - FlightConditions clone = conditions.clone(); - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - c = ((SimulationComputationListener) l).postFlightConditions(status, clone); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (c != null && !c.equals(conditions)) { - warn(status, l); - conditions = c; - clone = conditions.clone(); - } - } - } - return conditions; - } - - - - - /** - * Fire preAerodynamicCalculation event. - * - * @return <code>null</code> normally, or overriding aerodynamic forces. - */ - public static AerodynamicForces firePreAerodynamicCalculation(SimulationStatus status) - throws SimulationException { - AerodynamicForces forces; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - forces = ((SimulationComputationListener) l).preAerodynamicCalculation(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (forces != null) { - warn(status, l); - return forces; - } - } - } - return null; - } - - /** - * Fire postAerodynamicCalculation event. - * - * @return the aerodynamic forces to use. - */ - public static AerodynamicForces firePostAerodynamicCalculation(SimulationStatus status, AerodynamicForces forces) - throws SimulationException { - AerodynamicForces f; - AerodynamicForces clone = forces.clone(); - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - f = ((SimulationComputationListener) l).postAerodynamicCalculation(status, clone); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (f != null && !f.equals(forces)) { - warn(status, l); - forces = f; - clone = forces.clone(); - } - } - } - return forces; - } - - - - - - /** - * Fire preMassCalculation event. - * - * @return <code>null</code> normally, or overriding mass data. - */ - public static MassData firePreMassCalculation(SimulationStatus status) - throws SimulationException { - MassData mass; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - mass = ((SimulationComputationListener) l).preMassCalculation(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (mass != null) { - warn(status, l); - return mass; - } - } - } - return null; - } - - /** - * Fire postMassCalculation event. - * - * @return the aerodynamic forces to use. - */ - public static MassData firePostMassCalculation(SimulationStatus status, MassData mass) throws SimulationException { - MassData m; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - m = ((SimulationComputationListener) l).postMassCalculation(status, mass); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (m != null && !m.equals(mass)) { - warn(status, l); - mass = m; - } - } - } - return mass; - } - - - - - /** - * Fire preThrustComputation event. - * - * @return <code>NaN</code> normally, or overriding thrust. - */ - public static double firePreThrustCalculation(SimulationStatus status) - throws SimulationException { - double thrust; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - thrust = ((SimulationComputationListener) l).preSimpleThrustCalculation(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (!Double.isNaN(thrust)) { - warn(status, l); - return thrust; - } - } - } - return Double.NaN; - } - - /** - * Fire postThrustComputation event. - * - * @return the thrust value to use. - */ - public static double firePostThrustCalculation(SimulationStatus status, double thrust) throws SimulationException { - double t; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - t = ((SimulationComputationListener) l).postSimpleThrustCalculation(status, thrust); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (!Double.isNaN(t) && !MathUtil.equals(t, thrust)) { - warn(status, l); - thrust = t; - } - } - } - return thrust; - } - - - - - - /** - * Fire preMassCalculation event. - * - * @return <code>null</code> normally, or overriding mass data. - */ - public static AccelerationData firePreAccelerationCalculation(SimulationStatus status) throws SimulationException { - AccelerationData acceleration; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - acceleration = ((SimulationComputationListener) l).preAccelerationCalculation(status); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (acceleration != null) { - warn(status, l); - return acceleration; - } - } - } - return null; - } - - /** - * Fire postMassCalculation event. - * - * @return the aerodynamic forces to use. - */ - public static AccelerationData firePostAccelerationCalculation(SimulationStatus status, - AccelerationData acceleration) throws SimulationException { - AccelerationData a; - int modID = status.getModID(); - - for (SimulationListener l : status.getSimulationConditions().getSimulationListenerList()) { - if (l instanceof SimulationComputationListener) { - a = ((SimulationComputationListener) l).postAccelerationCalculation(status, acceleration); - if (modID != status.getModID()) { - warn(status, l); - modID = status.getModID(); - } - if (a != null && !a.equals(acceleration)) { - warn(status, l); - acceleration = a; - } - } - } - return acceleration; - } - - - - - private static void warn(SimulationStatus status, SimulationListener listener) { - if (!listener.isSystemListener()) { - log.info("Non-system listener " + listener + " affected the simulation"); - status.getWarnings().add(Warning.LISTENERS_AFFECTED); - } - } -} diff --git a/src/net/sf/openrocket/simulation/listeners/example/AirStart.java b/src/net/sf/openrocket/simulation/listeners/example/AirStart.java deleted file mode 100644 index 97ce93a2..00000000 --- a/src/net/sf/openrocket/simulation/listeners/example/AirStart.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.sf.openrocket.simulation.listeners.example; - -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; -import net.sf.openrocket.util.Coordinate; - -/** - * Simulation listener that launches a rocket from a specific altitude. - * <p> - * The altitude is read from the system property "openrocket.airstart.altitude" - * if defined, otherwise a default altitude of 1000 meters is used. - */ -public class AirStart extends AbstractSimulationListener { - - /** Default launch altitude */ - private static final double DEFAULT_ALTITUDE = 1000.0; - - @Override - public void startSimulation(SimulationStatus status) throws SimulationException { - - // Get the launch altitude - double altitude; - String arg = System.getProperty("openrocket.airstart.altitude"); - try { - altitude = Double.parseDouble(arg); - } catch (RuntimeException e) { - altitude = DEFAULT_ALTITUDE; - } - - // Modify launch position - Coordinate position = status.getRocketPosition(); - position = position.add(0, 0, altitude); - status.setRocketPosition(position); - - } - -} diff --git a/src/net/sf/openrocket/simulation/listeners/example/CSVSaveListener.java b/src/net/sf/openrocket/simulation/listeners/example/CSVSaveListener.java deleted file mode 100644 index 05a55fa2..00000000 --- a/src/net/sf/openrocket/simulation/listeners/example/CSVSaveListener.java +++ /dev/null @@ -1,295 +0,0 @@ -package net.sf.openrocket.simulation.listeners.example; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.PrintStream; -import java.util.Iterator; - -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; - - -public class CSVSaveListener extends AbstractSimulationListener { - - private static enum Types { - TIME { - @Override - public double getValue(SimulationStatus status) { - return status.getSimulationTime(); - } - }, - POSITION_X { - @Override - public double getValue(SimulationStatus status) { - return status.getRocketPosition().x; - } - }, - POSITION_Y { - @Override - public double getValue(SimulationStatus status) { - return status.getRocketPosition().y; - } - }, - ALTITUDE { - @Override - public double getValue(SimulationStatus status) { - return status.getRocketPosition().z; - } - }, - VELOCITY_X { - @Override - public double getValue(SimulationStatus status) { - return status.getRocketVelocity().x; - } - }, - VELOCITY_Y { - @Override - public double getValue(SimulationStatus status) { - return status.getRocketVelocity().y; - } - }, - VELOCITY_Z { - @Override - public double getValue(SimulationStatus status) { - return status.getRocketVelocity().z; - } - }, - THETA { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_ORIENTATION_THETA); - } - }, - PHI { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_ORIENTATION_PHI); - } - }, - AOA { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_AOA); - } - }, - ROLLRATE { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_ROLL_RATE); - } - }, - PITCHRATE { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_PITCH_RATE); - } - }, - - PITCHMOMENT { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_PITCH_MOMENT_COEFF); - } - }, - YAWMOMENT { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_YAW_MOMENT_COEFF); - } - }, - ROLLMOMENT { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_ROLL_MOMENT_COEFF); - } - }, - NORMALFORCE { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_NORMAL_FORCE_COEFF); - } - }, - SIDEFORCE { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_SIDE_FORCE_COEFF); - } - }, - AXIALFORCE { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_DRAG_FORCE); - } - }, - WINDSPEED { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_WIND_VELOCITY); - } - }, - PITCHDAMPING { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_PITCH_DAMPING_MOMENT_COEFF); - } - }, - CA { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_AXIAL_DRAG_COEFF); - } - }, - CD { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_DRAG_COEFF); - } - }, - CDpressure { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_PRESSURE_DRAG_COEFF); - } - }, - CDfriction { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_FRICTION_DRAG_COEFF); - } - }, - CDbase { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_BASE_DRAG_COEFF); - } - }, - MACH { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_MACH_NUMBER); - } - }, - RE { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_REYNOLDS_NUMBER); - } - }, - - CONTROL_ANGLE { - @Override - public double getValue(SimulationStatus status) { - Iterator<RocketComponent> iterator = - status.getConfiguration().getRocket().iterator(); - FinSet fin = null; - - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - if (c instanceof FinSet && c.getName().equals("CONTROL")) { - fin = (FinSet) c; - break; - } - } - if (fin == null) - return 0; - return fin.getCantAngle(); - } - }, - - MASS { - @Override - public double getValue(SimulationStatus status) { - return status.getFlightData().getLast(FlightDataType.TYPE_MASS); - } - } - - ; - - public abstract double getValue(SimulationStatus status); - } - - - public static final String FILENAME_FORMAT = "simulation-%03d.csv"; - - private File file; - private PrintStream output = null; - - - @Override - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { - - if (event.getType() == FlightEvent.Type.LAUNCH) { - int n = 1; - - if (output != null) { - System.err.println("WARNING: Ending simulation logging to CSV file " + - "(SIMULATION_END not encountered)."); - output.close(); - output = null; - } - - do { - file = new File(String.format(FILENAME_FORMAT, n)); - n++; - } while (file.exists()); - - System.err.println("Opening file " + file + " for CSV output."); - try { - output = new PrintStream(file); - } catch (FileNotFoundException e) { - System.err.println("ERROR OPENING FILE: " + e); - } - - final Types[] types = Types.values(); - StringBuilder s = new StringBuilder("# " + types[0].toString()); - for (int i = 1; i < types.length; i++) { - s.append("," + types[i].toString()); - } - output.println(s); - - } else if (event.getType() == FlightEvent.Type.SIMULATION_END && output != null) { - - System.err.println("Ending simulation logging to CSV file: " + file); - output.close(); - output = null; - - } else if (event.getType() != FlightEvent.Type.ALTITUDE) { - - if (output != null) { - output.println("# Event " + event); - } else { - System.err.println("WARNING: Event " + event + " encountered without open file"); - } - - } - - return true; - } - - @Override - public void postStep(SimulationStatus status) throws SimulationException { - - final Types[] types = Types.values(); - StringBuilder s; - - if (output != null) { - - s = new StringBuilder("" + types[0].getValue(status)); - for (int i = 1; i < types.length; i++) { - s.append("," + types[i].getValue(status)); - } - output.println(s); - - } else { - - System.err.println("WARNING: stepTaken called with no open file " + - "(t=" + status.getSimulationTime() + ")"); - } - - } -} diff --git a/src/net/sf/openrocket/simulation/listeners/example/PrintSimulationListener.java b/src/net/sf/openrocket/simulation/listeners/example/PrintSimulationListener.java deleted file mode 100644 index ac070d2b..00000000 --- a/src/net/sf/openrocket/simulation/listeners/example/PrintSimulationListener.java +++ /dev/null @@ -1,34 +0,0 @@ -package net.sf.openrocket.simulation.listeners.example; - -import net.sf.openrocket.simulation.FlightDataBranch; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; - - -public class PrintSimulationListener extends AbstractSimulationListener { - - @Override - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) throws SimulationException { - System.out.println("*** handleEvent *** " + event.toString() + - " position=" + status.getRocketPosition() + " velocity=" + status.getRocketVelocity()); - return true; - } - - @Override - public void postStep(SimulationStatus status) throws SimulationException { - FlightDataBranch data = status.getFlightData(); - System.out.printf("*** stepTaken *** time=%.3f position=" + status.getRocketPosition() + - " velocity=" + status.getRocketVelocity() + "=%.3f\n", status.getSimulationTime(), status.getRocketVelocity().length()); - System.out.printf(" thrust=%.3fN drag==%.3fN mass=%.3fkg " + - "accZ=%.3fm/s2 acc=%.3fm/s2\n", - data.getLast(FlightDataType.TYPE_THRUST_FORCE), - data.getLast(FlightDataType.TYPE_DRAG_FORCE), - data.getLast(FlightDataType.TYPE_MASS), - data.getLast(FlightDataType.TYPE_ACCELERATION_Z), - data.getLast(FlightDataType.TYPE_ACCELERATION_TOTAL)); - } - -} diff --git a/src/net/sf/openrocket/simulation/listeners/example/RollControlListener.java b/src/net/sf/openrocket/simulation/listeners/example/RollControlListener.java deleted file mode 100644 index 80993218..00000000 --- a/src/net/sf/openrocket/simulation/listeners/example/RollControlListener.java +++ /dev/null @@ -1,128 +0,0 @@ -package net.sf.openrocket.simulation.listeners.example; - -import net.sf.openrocket.aerodynamics.FlightConditions; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.MathUtil; - -/** - * An example listener that applies a PI-controller to adjust the cant of fins - * named "CONTROL" to stop the rocket from rolling. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RollControlListener extends AbstractSimulationListener { - - // Name of control fin set - private static final String CONTROL_FIN_NAME = "CONTROL"; - - // Define custom flight data type - private static final FlightDataType FIN_CANT_TYPE = FlightDataType.getType("Control fin cant", - UnitGroup.UNITS_ANGLE); - - // Simulation time at which PID controller is activated - private static final double START_TIME = 0.5; - - // Desired roll rate (rad/sec) - private static final double SETPOINT = 0.0; - - // Maximum control fin turn rate (rad/sec) - private static final double TURNRATE = 10 * Math.PI / 180; - - // Maximum control fin angle (rad) - private static final double MAX_ANGLE = 15 * Math.PI / 180; - - - /* - * PID parameters - * - * At M=0.3 KP oscillation threshold between 0.35 and 0.4. Good KI=3 - * At M=0.6 KP oscillation threshold between 0.07 and 0.08 Good KI=2 - * At M=0.9 KP oscillation threshold between 0.013 and 0.014 Good KI=0.5 - */ - private static final double KP = 0.007; - private static final double KI = 0.2; - - - - - private double rollrate; - - private double prevTime = 0; - private double intState = 0; - - private double finPosition = 0; - - - - @Override - public FlightConditions postFlightConditions(SimulationStatus status, FlightConditions flightConditions) { - // Store the current roll rate for later use - rollrate = flightConditions.getRollRate(); - return null; - } - - - @Override - public void postStep(SimulationStatus status) throws SimulationException { - - // Activate PID controller only after a specific time - if (status.getSimulationTime() < START_TIME) { - prevTime = status.getSimulationTime(); - return; - } - - // Find the fin set named CONTROL - FinSet finset = null; - for (RocketComponent c : status.getConfiguration()) { - if ((c instanceof FinSet) && (c.getName().equals(CONTROL_FIN_NAME))) { - finset = (FinSet) c; - break; - } - } - if (finset == null) { - throw new SimulationException("A fin set with name '" + CONTROL_FIN_NAME + "' was not found"); - } - - - // Determine time step - double deltaT = status.getSimulationTime() - prevTime; - prevTime = status.getSimulationTime(); - - - // PID controller - double error = SETPOINT - rollrate; - - double p = KP * error; - intState += error * deltaT; - double i = KI * intState; - - double value = p + i; - - - // Clamp the fin angle between -MAX_ANGLE and MAX_ANGLE - if (Math.abs(value) > MAX_ANGLE) { - System.err.printf("Attempting to set angle %.1f at t=%.3f, clamping.\n", - value * 180 / Math.PI, status.getSimulationTime()); - value = MathUtil.clamp(value, -MAX_ANGLE, MAX_ANGLE); - } - - - // Limit the fin turn rate - if (finPosition < value) { - finPosition = Math.min(finPosition + TURNRATE * deltaT, value); - } else { - finPosition = Math.max(finPosition - TURNRATE * deltaT, value); - } - - // Set the control fin cant and store the data - finset.setCantAngle(finPosition); - status.getFlightData().setValue(FIN_CANT_TYPE, finPosition); - - } -} diff --git a/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java b/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java deleted file mode 100644 index 1dd74436..00000000 --- a/src/net/sf/openrocket/simulation/listeners/example/StopSimulationListener.java +++ /dev/null @@ -1,61 +0,0 @@ -package net.sf.openrocket.simulation.listeners.example; - -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; - -/** - * A simulation listener that stops the simulation after a specified number of steps or - * after a specified abount of simulation time. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class StopSimulationListener extends AbstractSimulationListener { - - private final int REPORT = 500; - - private final double stopTime; - private final int stopStep; - - private int step = 0; - - private long startTime = -1; - private long time = -1; - - public StopSimulationListener(double t, int n) { - stopTime = t; - stopStep = n; - } - - @Override - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { - - if (event.getType() == FlightEvent.Type.LAUNCH) { - System.out.println("Simulation starting."); - time = System.nanoTime(); - startTime = System.nanoTime(); - } - - return true; - } - - - @Override - public void postStep(SimulationStatus status) throws SimulationException { - step++; - if ((step % REPORT) == 0) { - long t = System.nanoTime(); - - System.out.printf("Step %4d, time=%.3f, took %d us/step (avg. %d us/step)\n", - step, status.getSimulationTime(), (t - time) / 1000 / REPORT, (t - startTime) / 1000 / step); - time = t; - } - if (status.getSimulationTime() >= stopTime || step >= stopStep) { - System.out.printf("Stopping simulation, step=%d time=%.3f\n", step, status.getSimulationTime()); - status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, - status.getSimulationTime(), null)); - } - } - -} diff --git a/src/net/sf/openrocket/simulation/listeners/system/ApogeeEndListener.java b/src/net/sf/openrocket/simulation/listeners/system/ApogeeEndListener.java deleted file mode 100644 index 60081224..00000000 --- a/src/net/sf/openrocket/simulation/listeners/system/ApogeeEndListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.simulation.listeners.system; - -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; - - -/** - * A simulation listeners that ends the simulation when apogee is reached. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ApogeeEndListener extends AbstractSimulationListener { - - public static final ApogeeEndListener INSTANCE = new ApogeeEndListener(); - - @Override - public boolean handleFlightEvent(SimulationStatus status, FlightEvent event) { - if (event.getType() == FlightEvent.Type.APOGEE) { - status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); - } - return true; - } - - @Override - public boolean isSystemListener() { - return true; - } -} diff --git a/src/net/sf/openrocket/simulation/listeners/system/InterruptListener.java b/src/net/sf/openrocket/simulation/listeners/system/InterruptListener.java deleted file mode 100644 index e2460098..00000000 --- a/src/net/sf/openrocket/simulation/listeners/system/InterruptListener.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.sf.openrocket.simulation.listeners.system; - -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationCancelledException; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; - - -/** - * A simulation listener that throws a {@link SimulationCancelledException} if - * this thread has been interrupted. The conditions is checked every time a step - * is taken. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class InterruptListener extends AbstractSimulationListener { - - public static final InterruptListener INSTANCE = new InterruptListener(); - - @Override - public void postStep(SimulationStatus status) throws SimulationException { - if (Thread.interrupted()) { - throw new SimulationCancelledException("The simulation was interrupted."); - } - } - - @Override - public boolean isSystemListener() { - return true; - } -} diff --git a/src/net/sf/openrocket/simulation/listeners/system/RecoveryDeviceDeploymentEndListener.java b/src/net/sf/openrocket/simulation/listeners/system/RecoveryDeviceDeploymentEndListener.java deleted file mode 100644 index b6bac333..00000000 --- a/src/net/sf/openrocket/simulation/listeners/system/RecoveryDeviceDeploymentEndListener.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.simulation.listeners.system; - -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.simulation.FlightEvent; -import net.sf.openrocket.simulation.SimulationStatus; -import net.sf.openrocket.simulation.exception.SimulationException; -import net.sf.openrocket.simulation.listeners.AbstractSimulationListener; - - -/** - * A simulation listeners that ends the simulation when apogee is reached. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RecoveryDeviceDeploymentEndListener extends AbstractSimulationListener { - - public static final RecoveryDeviceDeploymentEndListener INSTANCE = new RecoveryDeviceDeploymentEndListener(); - - @Override - public boolean recoveryDeviceDeployment(SimulationStatus status, RecoveryDevice recoveryDevice) throws SimulationException { - status.getEventQueue().add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.getSimulationTime())); - return true; - } - - @Override - public boolean isSystemListener() { - return true; - } -} diff --git a/src/net/sf/openrocket/startup/Application.java b/src/net/sf/openrocket/startup/Application.java deleted file mode 100644 index 7718b67e..00000000 --- a/src/net/sf/openrocket/startup/Application.java +++ /dev/null @@ -1,164 +0,0 @@ -package net.sf.openrocket.startup; - -import net.sf.openrocket.database.MotorDatabase; -import net.sf.openrocket.l10n.ClassBasedTranslator; -import net.sf.openrocket.l10n.DebugTranslator; -import net.sf.openrocket.l10n.ExceptionSuppressingTranslator; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.LogLevel; -import net.sf.openrocket.logging.LogLevelBufferLogger; -import net.sf.openrocket.logging.PrintStreamLogger; - -/** - * A class that provides singleton instances / beans for other classes to utilize. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class Application { - - private static LogHelper logger; - private static LogLevelBufferLogger logBuffer; - - private static Translator baseTranslator = new DebugTranslator(null); - - private static MotorDatabase motorSetDatabase; - - private static Preferences preferences; - - private static ExceptionHandler exceptionHandler; - - // Initialize the logger to something sane for testing without executing Startup - static { - setLogOutputLevel(LogLevel.DEBUG); - } - - /** - * Return whether to use additional safety code checks. - */ - public static boolean useSafetyChecks() { - // Currently default to false unless openrocket.debug.safetycheck is defined - String s = System.getProperty("openrocket.debug.safetycheck"); - if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) { - return true; - } - return false; - } - - /** - * Retrieve the logger to be used in logging. By default this returns - * a logger that outputs to stdout/stderr even if not separately initialized, - * useful for development and debugging. - */ - public static LogHelper getLogger() { - return logger; - } - - /** - * Set the logger to be used in logging. Note that calling this will only have effect - * on not-yet loaded classes, as the instance is stored in a static variable. - */ - public static void setLogger(LogHelper logger) { - Application.logger = logger; - } - - - - /** - * Return the log buffer. - * - * @return the logBuffer or null if not initialized - */ - public static LogLevelBufferLogger getLogBuffer() { - return logBuffer; - } - - /** - * Set the log buffer logger. The logger must be separately configured - * to receive the logging. - */ - public static void setLogBuffer(LogLevelBufferLogger logBuffer) { - Application.logBuffer = logBuffer; - } - - - /** - * Set the logging to output the specified log level and upwards to standard output. - * - * @param level the minimum logging level to output. - */ - public static void setLogOutputLevel(LogLevel level) { - logger = new PrintStreamLogger(); - for (LogLevel l : LogLevel.values()) { - if (l.atLeast(level)) { - ((PrintStreamLogger) logger).setOutput(l, System.out); - } - } - - } - - - /** - * Return the translator to use for obtaining translated strings. - * @return a translator. - */ - public static Translator getTranslator() { - Translator t = baseTranslator; - t = new ClassBasedTranslator(t, 1); - t = new ExceptionSuppressingTranslator(t); - return t; - } - - /** - * Set the translator used in obtaining translated strings. - * @param translator the translator to set. - */ - public static void setBaseTranslator(Translator translator) { - Application.baseTranslator = translator; - } - - - /** - * @return the preferences - */ - public static Preferences getPreferences() { - return preferences; - } - - /** - * @param preferences the preferences to set - */ - public static void setPreferences(Preferences preferences) { - Application.preferences = preferences; - } - - /** - * @return the exceptionHandler - */ - public static ExceptionHandler getExceptionHandler() { - return exceptionHandler; - } - - /** - * @param exceptionHandler the exceptionHandler to set - */ - public static void setExceptionHandler(ExceptionHandler exceptionHandler) { - Application.exceptionHandler = exceptionHandler; - } - - /** - * Return the database of all thrust curves loaded into the system. - */ - public static MotorDatabase getMotorSetDatabase() { - return motorSetDatabase; - } - - /** - * Set the database of thrust curves loaded into the system. - */ - public static void setMotorSetDatabase(MotorDatabase motorSetDatabase) { - Application.motorSetDatabase = motorSetDatabase; - } - - -} diff --git a/src/net/sf/openrocket/startup/ExceptionHandler.java b/src/net/sf/openrocket/startup/ExceptionHandler.java deleted file mode 100644 index fa115322..00000000 --- a/src/net/sf/openrocket/startup/ExceptionHandler.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.sf.openrocket.startup; - -public interface ExceptionHandler { - - public void handleErrorCondition(String message); - public void handleErrorCondition(String message, Throwable exception); - public void handleErrorCondition(final Throwable exception); - - - public void uncaughtException(final Thread thread, final Throwable throwable); - -} diff --git a/src/net/sf/openrocket/startup/Preferences.java b/src/net/sf/openrocket/startup/Preferences.java deleted file mode 100644 index 0a5e62b9..00000000 --- a/src/net/sf/openrocket/startup/Preferences.java +++ /dev/null @@ -1,407 +0,0 @@ -package net.sf.openrocket.startup; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -import net.sf.openrocket.database.Databases; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.material.Material; -import net.sf.openrocket.rocketcomponent.BodyComponent; -import net.sf.openrocket.rocketcomponent.FinSet; -import net.sf.openrocket.rocketcomponent.InternalComponent; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassObject; -import net.sf.openrocket.rocketcomponent.RecoveryDevice; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.BuildProperties; -import net.sf.openrocket.util.Color; -import net.sf.openrocket.util.LineStyle; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.UniqueID; - -public abstract class Preferences { - - /* - * Well known string keys to preferences. - * There are other strings out there in the source as well. - */ - public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition"; - public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves"; - public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation"; - // Preferences related to data export - public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator"; - public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment"; - public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment"; - public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments"; - public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter"; - public static final String USER_LOCAL = "locale"; - - public static final String PLOT_SHOW_POINTS = "ShowPlotPoints"; - - private static final String CHECK_UPDATES = "CheckUpdates"; - public static final String LAST_UPDATE = "LastUpdateVersion"; - - public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch"; - public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar"; - - // Node names - public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors"; - - /* - * ****************************************************************************************** - * - * Abstract methods which must be implemented by any derived class. - */ - public abstract boolean getBoolean( String key, boolean defaultValue ); - public abstract void putBoolean( String key, boolean value ); - - public abstract int getInt( String key, int defaultValue); - public abstract void putInt( String key, int value ); - - public abstract double getDouble( String key, double defaultValue ); - public abstract void putDouble( String key, double value ); - - public abstract String getString( String key, String defaultValue ); - public abstract void putString( String key, String value ); - - /** - * Directory represents a way to collect multiple keys together. Implementors may - * choose to concatenate the directory with the key using some special character. - * @param directory - * @param key - * @param defaultValue - * @return - */ - public abstract String getString( String directory, String key, String defaultValue); - - public abstract void putString( String directory, String key, String value ); - - /* - * ****************************************************************************************** - */ - public final boolean getCheckUpdates() { - return this.getBoolean(CHECK_UPDATES, BuildProperties.getDefaultCheckUpdates()); - } - - public final void setCheckUpdates(boolean check) { - this.putBoolean(CHECK_UPDATES, check); - } - - public final double getDefaultMach() { - // TODO: HIGH: implement custom default mach number - return 0.3; - } - - /** - * Return the OpenRocket unique ID. - * - * @return a random ID string that stays constant between OpenRocket executions - */ - public final String getUniqueID() { - String id = this.getString("id", null); - if (id == null) { - id = UniqueID.uuid(); - this.putString("id", id); - } - return id; - } - - /** - * Returns a limited-range integer value from the preferences. If the value - * in the preferences is negative or greater than max, then the default value - * is returned. - * - * @param key The preference to retrieve. - * @param max Maximum allowed value for the choice. - * @param def Default value. - * @return The preference value. - */ - public final int getChoice(String key, int max, int def) { - int v = this.getInt(key, def); - if ((v < 0) || (v > max)) - return def; - return v; - } - - /** - * Helper method that puts an integer choice value into the preferences. - * - * @param key the preference key. - * @param value the value to store. - */ - public final void putChoice(String key, int value) { - this.putInt(key, value); - } - - /** - * Retrieve an enum value from the user preferences. - * - * @param <T> the enum type - * @param key the key - * @param def the default value, cannot be null - * @return the value in the preferences, or the default value - */ - public final <T extends Enum<T>> T getEnum(String key, T def) { - if (def == null) { - throw new BugException("Default value cannot be null"); - } - - String value = getString(key, null); - if (value == null) { - return def; - } - - try { - return Enum.valueOf(def.getDeclaringClass(), value); - } catch (IllegalArgumentException e) { - return def; - } - } - - /** - * Store an enum value to the user preferences. - * - * @param key the key - * @param value the value to store, or null to remove the value - */ - public final void putEnum(String key, Enum<?> value) { - if (value == null) { - putString(key, null); - } else { - putString(key, value.name()); - } - } - - public Color getDefaultColor(Class<? extends RocketComponent> c) { - String color = get("componentColors", c, DEFAULT_COLORS); - if (color == null) - return Color.BLACK; - - Color clr = parseColor(color); - if (clr != null) { - return clr; - } else { - return Color.BLACK; - } - } - - public final void setDefaultColor(Class<? extends RocketComponent> c, Color color) { - if (color == null) - return; - putString("componentColors", c.getSimpleName(), stringifyColor(color)); - } - - - /** - * Retrieve a Line style for the given component. - * @param c - * @return - */ - public final LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) { - String value = get("componentStyle", c, DEFAULT_LINE_STYLES); - try { - return LineStyle.valueOf(value); - } catch (Exception e) { - return LineStyle.SOLID; - } - } - - /** - * Set a default line style for the given component. - * @param c - * @param style - */ - public final void setDefaultLineStyle(Class<? extends RocketComponent> c, - LineStyle style) { - if (style == null) - return; - putString("componentStyle", c.getSimpleName(), style.name()); - } - - /** - * Get the default material type for the given component. - * @param componentClass - * @param type the Material.Type to return. - * @return - */ - public Material getDefaultComponentMaterial( - Class<? extends RocketComponent> componentClass, - Material.Type type) { - - String material = get("componentMaterials", componentClass, null); - if (material != null) { - try { - Material m = Material.fromStorableString(material, false); - if (m.getType() == type) - return m; - } catch (IllegalArgumentException ignore) { - } - } - - switch (type) { - case LINE: - return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL; - case SURFACE: - return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL; - case BULK: - return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL; - } - throw new IllegalArgumentException("Unknown material type: " + type); - } - - /** - * Set the default material for a component type. - * @param componentClass - * @param material - */ - public void setDefaultComponentMaterial( - Class<? extends RocketComponent> componentClass, Material material) { - - putString("componentMaterials", componentClass.getSimpleName(), - material == null ? null : material.toStorableString()); - } - - /** - * get a net.sf.openrocket.util.Color object for the given key. - * @param key - * @param defaultValue - * @return - */ - public final Color getColor( String key, Color defaultValue ) { - Color c = parseColor( getString(key,null) ); - if ( c == null ) { - return defaultValue; - } - return c; - } - - /** - * set a net.sf.openrocket.util.Color preference value for the given key. - * @param key - * @param value - */ - public final void putColor( String key, Color value ) { - putString( key, stringifyColor(value) ); - } - - /** - * Helper function to convert a string representation into a net.sf.openrocket.util.Color object. - * @param color - * @return - */ - protected static Color parseColor(String color) { - if (color == null) { - return null; - } - - String[] rgb = color.split(","); - if (rgb.length == 3) { - try { - int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255); - int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255); - int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255); - return new Color(red, green, blue); - } catch (NumberFormatException ignore) { - } - } - return null; - } - - /** - * Helper function to convert a net.sf.openrocket.util.Color object into a - * String before storing in a preference. - * @param color - * @return - */ - protected static String stringifyColor(Color color) { - String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue(); - return string; - } - - /** - * Special helper function which allows for a map of default values. - * - * First getString(directory,componentClass.getSimpleName(), null) is invoked, - * if the returned value is null, the defaultMap is consulted for a value. - * - * @param directory - * @param componentClass - * @param defaultMap - * @return - */ - protected String get(String directory, - Class<? extends RocketComponent> componentClass, - Map<Class<?>, String> defaultMap) { - - // Search preferences - Class<?> c = componentClass; - while (c != null && RocketComponent.class.isAssignableFrom(c)) { - String value = this.getString(directory, c.getSimpleName(), null); - if (value != null) - return value; - c = c.getSuperclass(); - } - - if (defaultMap == null) - return null; - - // Search defaults - c = componentClass; - while (RocketComponent.class.isAssignableFrom(c)) { - String value = defaultMap.get(c); - if (value != null) - return value; - c = c.getSuperclass(); - } - - return null; - } - - public abstract void addUserMaterial(Material m); - public abstract Set<Material> getUserMaterials(); - public abstract void removeUserMaterial(Material m); - - /* - * Map of default line styles - */ - private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES = - new HashMap<Class<?>, String>(); - static { - DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name()); - DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name()); - } - - /* - * Within a holder class so they will load only when needed. - */ - private static class DefaultMaterialHolder { - private static final Translator trans = Application.getTranslator(); - - //// Elastic cord (round 2mm, 1/16 in) - private static final Material DEFAULT_LINE_MATERIAL = - Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"), - 0.0018, false); - //// Ripstop nylon - private static final Material DEFAULT_SURFACE_MATERIAL = - Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false); - //// Cardboard - private static final Material DEFAULT_BULK_MATERIAL = - Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false); - } - - private static final HashMap<Class<?>, String> DEFAULT_COLORS = - new HashMap<Class<?>, String>(); - static { - DEFAULT_COLORS.put(BodyComponent.class, "0,0,240"); - DEFAULT_COLORS.put(FinSet.class, "0,0,200"); - DEFAULT_COLORS.put(LaunchLug.class, "0,0,180"); - DEFAULT_COLORS.put(InternalComponent.class, "170,0,100"); - DEFAULT_COLORS.put(MassObject.class, "0,0,0"); - DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0"); - } - - - -} diff --git a/src/net/sf/openrocket/startup/Startup.java b/src/net/sf/openrocket/startup/Startup.java deleted file mode 100644 index d288ea76..00000000 --- a/src/net/sf/openrocket/startup/Startup.java +++ /dev/null @@ -1,203 +0,0 @@ -package net.sf.openrocket.startup; - -import java.io.PrintStream; -import java.util.Locale; -import java.util.prefs.Preferences; - -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.DebugTranslator; -import net.sf.openrocket.l10n.L10N; -import net.sf.openrocket.l10n.ResourceBundleTranslator; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.logging.DelegatorLogger; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.LogLevel; -import net.sf.openrocket.logging.LogLevelBufferLogger; -import net.sf.openrocket.logging.PrintStreamLogger; - - -/** - * The first class in the OpenRocket startup sequence. This class is responsible - * for setting up the Application class with the statically used subsystems - * (logging and translation) and then delegating to Startup2 class. - * <p> - * This class must be very cautious about what classes it calls. This is because - * the loggers/translators for classes are initialized as static final members during - * class initialization. For example, this class MUST NOT use the Prefs class, because - * using it will cause LineStyle to be initialized, which then receives an invalid - * (not-yet-initialized) translator. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Startup { - - static LogHelper log; - - private static final String LOG_STDERR_PROPERTY = "openrocket.log.stderr"; - private static final String LOG_STDOUT_PROPERTY = "openrocket.log.stdout"; - - private static final int LOG_BUFFER_LENGTH = 50; - - - /** - * OpenRocket startup main method. - */ - public static void main(final String[] args) throws Exception { - - // Check for "openrocket.debug" property before anything else - checkDebugStatus(); - - // Initialize logging first so we can use it - initializeLogging(); - - Application.setPreferences( new SwingPreferences() ); - - // Setup the translations - initializeL10n(); - - // Continue startup in Startup2 class (where Application is already set up) - Startup2.runMain(args); - - } - - - - /** - * Set proper system properties if openrocket.debug is defined. - */ - private static void checkDebugStatus() { - if (System.getProperty("openrocket.debug") != null) { - setPropertyIfNotSet("openrocket.log.stdout", "VBOSE"); - setPropertyIfNotSet("openrocket.log.tracelevel", "VBOSE"); - setPropertyIfNotSet("openrocket.debug.menu", "true"); - setPropertyIfNotSet("openrocket.debug.mutexlocation", "true"); - setPropertyIfNotSet("openrocket.debug.motordigest", "true"); - } - } - - private static void setPropertyIfNotSet(String key, String value) { - if (System.getProperty(key) == null) { - System.setProperty(key, value); - } - } - - - - /** - * Initializes the loggins system. - */ - private static void initializeLogging() { - DelegatorLogger delegator = new DelegatorLogger(); - - // Log buffer - LogLevelBufferLogger buffer = new LogLevelBufferLogger(LOG_BUFFER_LENGTH); - delegator.addLogger(buffer); - - // Check whether to log to stdout/stderr - PrintStreamLogger printer = new PrintStreamLogger(); - boolean logout = setLogOutput(printer, System.out, System.getProperty(LOG_STDOUT_PROPERTY), null); - boolean logerr = setLogOutput(printer, System.err, System.getProperty(LOG_STDERR_PROPERTY), LogLevel.ERROR); - if (logout || logerr) { - delegator.addLogger(printer); - } - - // Set the loggers - Application.setLogger(delegator); - Application.setLogBuffer(buffer); - - // Initialize the log for this class - log = Application.getLogger(); - log.info("Logging subsystem initialized"); - String str = "Console logging output:"; - for (LogLevel l : LogLevel.values()) { - PrintStream ps = printer.getOutput(l); - str += " " + l.name() + ":"; - if (ps == System.err) { - str += "stderr"; - } else if (ps == System.out) { - str += "stdout"; - } else { - str += "none"; - } - } - str += " (" + LOG_STDOUT_PROPERTY + "=" + System.getProperty(LOG_STDOUT_PROPERTY) + - " " + LOG_STDERR_PROPERTY + "=" + System.getProperty(LOG_STDERR_PROPERTY) + ")"; - log.info(str); - } - - private static boolean setLogOutput(PrintStreamLogger logger, PrintStream stream, String level, LogLevel defaultLevel) { - LogLevel minLevel = LogLevel.fromString(level, defaultLevel); - if (minLevel == null) { - return false; - } - - for (LogLevel l : LogLevel.values()) { - if (l.atLeast(minLevel)) { - logger.setOutput(l, stream); - } - } - return true; - } - - - - - /** - * Initializes the localization system. - */ - private static void initializeL10n() { - - // Check for locale propery - String langcode = System.getProperty("openrocket.locale"); - - if (langcode != null) { - - Locale l = L10N.toLocale(langcode); - log.info("Setting custom locale " + l); - Locale.setDefault(l); - - } else { - - // Check user-configured locale - Locale l = getUserLocale(); - if (l != null) { - log.info("Setting user-selected locale " + l); - Locale.setDefault(l); - } else { - log.info("Using default locale " + Locale.getDefault()); - } - - } - - // Setup the translator - Translator t; - t = new ResourceBundleTranslator("l10n.messages"); - if (Locale.getDefault().getLanguage().equals("xx")) { - t = new DebugTranslator(t); - } - - log.info("Set up translation for locale " + Locale.getDefault() + - ", debug.currentFile=" + t.get("debug.currentFile")); - - Application.setBaseTranslator(t); - } - - - - - private static Locale getUserLocale() { - /* - * This method MUST NOT use the Prefs class, since is causes a multitude - * of classes to be initialized. Therefore this duplicates the functionality - * of the Prefs class locally. - */ - - if (System.getProperty("openrocket.debug.prefs") != null) { - return null; - } - - return L10N.toLocale(Preferences.userRoot().node("OpenRocket").get("locale", null)); - } - - -} diff --git a/src/net/sf/openrocket/startup/Startup2.java b/src/net/sf/openrocket/startup/Startup2.java deleted file mode 100644 index 11ceb190..00000000 --- a/src/net/sf/openrocket/startup/Startup2.java +++ /dev/null @@ -1,294 +0,0 @@ -package net.sf.openrocket.startup; - -import java.awt.GraphicsEnvironment; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.File; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.swing.SwingUtilities; -import javax.swing.Timer; -import javax.swing.ToolTipManager; - -import net.sf.openrocket.communication.UpdateInfo; -import net.sf.openrocket.communication.UpdateInfoRetriever; -import net.sf.openrocket.database.Databases; -import net.sf.openrocket.database.ThrustCurveMotorSet; -import net.sf.openrocket.database.ThrustCurveMotorSetDatabase; -import net.sf.openrocket.file.iterator.DirectoryIterator; -import net.sf.openrocket.file.iterator.FileIterator; -import net.sf.openrocket.file.motor.MotorLoaderHelper; -import net.sf.openrocket.gui.dialogs.UpdateInfoDialog; -import net.sf.openrocket.gui.main.BasicFrame; -import net.sf.openrocket.gui.main.Splash; -import net.sf.openrocket.gui.main.SwingExceptionHandler; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.gui.util.SimpleFileFilter; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.util.BuildProperties; - -/** - * The second class in the OpenRocket startup sequence. This class can assume the - * Application class to be properly set up, and can use any classes safely. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Startup2 { - private static final LogHelper log = Application.getLogger(); - - - private static final String THRUSTCURVE_DIRECTORY = "datafiles/thrustcurves/"; - - /** Block motor loading for this many milliseconds */ - private static AtomicInteger blockLoading = new AtomicInteger(Integer.MAX_VALUE); - - - - /** - * Run when starting up OpenRocket after Application has been set up. - * - * @param args command line arguments - */ - static void runMain(final String[] args) throws Exception { - - log.info("Starting up OpenRocket version " + BuildProperties.getVersion()); - - // Check that we're not running headless - log.info("Checking for graphics head"); - checkHead(); - - // Check that we're running a good version of a JRE - log.info("Checking JRE compatibility"); - VersionHelper.checkVersion(); - VersionHelper.checkOpenJDK(); - - // Run the actual startup method in the EDT since it can use progress dialogs etc. - log.info("Moving startup to EDT"); - SwingUtilities.invokeAndWait(new Runnable() { - @Override - public void run() { - runInEDT(args); - } - }); - - log.info("Startup complete"); - } - - - /** - * Run in the EDT when starting up OpenRocket. - * - * @param args command line arguments - */ - private static void runInEDT(String[] args) { - - // Initialize the splash screen with version info - log.info("Initializing the splash screen"); - Splash.init(); - - // Setup the uncaught exception handler - log.info("Registering exception handler"); - SwingExceptionHandler exceptionHandler = new SwingExceptionHandler(); - Application.setExceptionHandler(exceptionHandler); - exceptionHandler.registerExceptionHandler(); - - // Start update info fetching - final UpdateInfoRetriever updateInfo; - if ( Application.getPreferences().getCheckUpdates()) { - log.info("Starting update check"); - updateInfo = new UpdateInfoRetriever(); - updateInfo.start(); - } else { - log.info("Update check disabled"); - updateInfo = null; - } - - // Set the best available look-and-feel - log.info("Setting best LAF"); - GUIUtil.setBestLAF(); - - // Set tooltip delay time. Tooltips are used in MotorChooserDialog extensively. - ToolTipManager.sharedInstance().setDismissDelay(30000); - - // Load defaults - ((SwingPreferences) Application.getPreferences()).loadDefaultUnits(); - - // Load motors etc. - log.info("Loading databases"); - loadMotor(); - Databases.fakeMethod(); - - // Starting action (load files or open new document) - log.info("Opening main application window"); - if (!handleCommandLine(args)) { - BasicFrame.newAction(); - } - - // Check whether update info has been fetched or whether it needs more time - log.info("Checking update status"); - checkUpdateStatus(updateInfo); - - // Block motor loading for 1.5 seconds to allow window painting to be faster - blockLoading.set(1500); - } - - - /** - * Check that the JRE is not running headless. - */ - private static void checkHead() { - - if (GraphicsEnvironment.isHeadless()) { - log.error("Application is headless."); - System.err.println(); - System.err.println("OpenRocket cannot currently be run without the graphical " + - "user interface."); - System.err.println(); - System.exit(1); - } - - } - - - private static void loadMotor() { - - log.info("Starting motor loading from " + THRUSTCURVE_DIRECTORY + " in background thread."); - ThrustCurveMotorSetDatabase db = new ThrustCurveMotorSetDatabase(true) { - - @Override - protected void loadMotors() { - - // Block loading until timeout occurs or database is taken into use - log.info("Blocking motor loading while starting up"); - while (!inUse && blockLoading.addAndGet(-100) > 0) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - } - } - log.info("Blocking ended, inUse=" + inUse + " blockLoading=" + blockLoading.get()); - - // Start loading - log.info("Loading motors from " + THRUSTCURVE_DIRECTORY); - long t0 = System.currentTimeMillis(); - int fileCount; - int thrustCurveCount; - - // Load the packaged thrust curves - List<Motor> list; - FileIterator iterator = DirectoryIterator.findDirectory(THRUSTCURVE_DIRECTORY, - new SimpleFileFilter("", false, "eng", "rse")); - if (iterator == null) { - throw new IllegalStateException("Thrust curve directory " + THRUSTCURVE_DIRECTORY + - "not found, distribution built wrong"); - } - list = MotorLoaderHelper.load(iterator); - for (Motor m : list) { - this.addMotor((ThrustCurveMotor) m); - } - fileCount = iterator.getFileCount(); - - thrustCurveCount = list.size(); - - // Load the user-defined thrust curves - for (File file : ((SwingPreferences) Application.getPreferences()).getUserThrustCurveFiles()) { - log.info("Loading motors from " + file); - list = MotorLoaderHelper.load(file); - for (Motor m : list) { - this.addMotor((ThrustCurveMotor) m); - } - fileCount++; - thrustCurveCount += list.size(); - } - - long t1 = System.currentTimeMillis(); - - // Count statistics - int distinctMotorCount = 0; - int distinctThrustCurveCount = 0; - distinctMotorCount = motorSets.size(); - for (ThrustCurveMotorSet set : motorSets) { - distinctThrustCurveCount += set.getMotorCount(); - } - log.info("Motor loading done, took " + (t1 - t0) + " ms to load " - + fileCount + " files/directories containing " - + thrustCurveCount + " thrust curves which contained " - + distinctMotorCount + " distinct motors with " - + distinctThrustCurveCount + " distinct thrust curves."); - } - - }; - db.startLoading(); - Application.setMotorSetDatabase(db); - } - - private static void checkUpdateStatus(final UpdateInfoRetriever updateInfo) { - if (updateInfo == null) - return; - - int delay = 1000; - if (!updateInfo.isRunning()) - delay = 100; - - final Timer timer = new Timer(delay, null); - - ActionListener listener = new ActionListener() { - private int count = 5; - - @Override - public void actionPerformed(ActionEvent e) { - if (!updateInfo.isRunning()) { - timer.stop(); - - String current = BuildProperties.getVersion(); - String last = Application.getPreferences().getString(Preferences.LAST_UPDATE, ""); - - UpdateInfo info = updateInfo.getUpdateInfo(); - if (info != null && info.getLatestVersion() != null && - !current.equals(info.getLatestVersion()) && - !last.equals(info.getLatestVersion())) { - - UpdateInfoDialog infoDialog = new UpdateInfoDialog(info); - infoDialog.setVisible(true); - if (infoDialog.isReminderSelected()) { - Application.getPreferences().putString(Preferences.LAST_UPDATE, ""); - } else { - Application.getPreferences().putString(Preferences.LAST_UPDATE, info.getLatestVersion()); - } - } - } - count--; - if (count <= 0) - timer.stop(); - } - }; - timer.addActionListener(listener); - timer.start(); - } - - /** - * Handles arguments passed from the command line. This may be used either - * when starting the first instance of OpenRocket or later when OpenRocket is - * executed again while running. - * - * @param args the command-line arguments. - * @return whether a new frame was opened or similar user desired action was - * performed as a result. - */ - private static boolean handleCommandLine(String[] args) { - - // Check command-line for files - boolean opened = false; - for (String file : args) { - if (BasicFrame.open(new File(file), null)) { - opened = true; - } - } - return opened; - } - -} diff --git a/src/net/sf/openrocket/startup/VersionHelper.java b/src/net/sf/openrocket/startup/VersionHelper.java deleted file mode 100644 index 097f1dc4..00000000 --- a/src/net/sf/openrocket/startup/VersionHelper.java +++ /dev/null @@ -1,128 +0,0 @@ -package net.sf.openrocket.startup; - -import java.awt.GraphicsEnvironment; - -import javax.swing.JOptionPane; - -import net.sf.openrocket.logging.LogHelper; - -public class VersionHelper { - - private static final LogHelper log = Application.getLogger(); - - private static final int REQUIRED_MAJOR_VERSION = 1; - private static final int REQUIRED_MINOR_VERSION = 6; - - // OpenJDK 1.6.0_0-b16 is known to work, 1.6.0_0-b12 does not - private static final String BAD_OPENJDK_VERSION = "^1.6.0_0-b([0-9]|1[1-5])$"; - - - /** - * Check that the JRE version is high enough. - */ - static void checkVersion() { - - String[] version = System.getProperty("java.specification.version", "").split("\\."); - - String jreName = System.getProperty("java.vm.name", "(unknown)"); - String jreVersion = System.getProperty("java.runtime.version", "(unknown)"); - String jreVendor = System.getProperty("java.vendor", "(unknown)"); - - log.info("Running JRE " + jreName + " version " + jreVersion + " by " + jreVendor); - - int major, minor; - - try { - major = Integer.parseInt(version[0]); - minor = Integer.parseInt(version[1]); - - if (major < REQUIRED_MAJOR_VERSION || - (major == REQUIRED_MAJOR_VERSION && minor < REQUIRED_MINOR_VERSION)) { - error(new String[] { "Java SE version 6 is required to run OpenRocket.", - "You are currently running " + jreName + " version " + - jreVersion + " by " + jreVendor }); - } - - } catch (RuntimeException e) { - - confirm(new String[] { "The Java version in use could not be detected.", - "OpenRocket requires at least Java SE 6.", - "Continue anyway?" }); - - } - - } - - /** - * Check whether OpenJDK is being used, and if it is warn the user about - * problems and confirm whether to continue. - */ - static void checkOpenJDK() { - - if (System.getProperty("java.runtime.name", "").toLowerCase().indexOf("icedtea") >= 0 || - System.getProperty("java.vm.name", "").toLowerCase().indexOf("openjdk") >= 0) { - - String jreName = System.getProperty("java.vm.name", "(unknown)"); - String jreVersion = System.getProperty("java.runtime.version", "(unknown)"); - String jreVendor = System.getProperty("java.vendor", "(unknown)"); - - if (jreVersion.matches(BAD_OPENJDK_VERSION)) { - - confirm(new String[] { "Old versions of OpenJDK are known to have problems " + - "running OpenRocket.", - " ", - "You are currently running " + jreName + " version " + - jreVersion + " by " + jreVendor, - "Do you want to continue?" }); - } - } - } - - - - /////////// Helper methods ////////// - - /** - * Presents an error message to the user and exits the application. - * - * @param message an array of messages to present. - */ - private static void error(String[] message) { - - System.err.println(); - System.err.println("Error starting OpenRocket:"); - System.err.println(); - for (int i = 0; i < message.length; i++) { - System.err.println(message[i]); - } - System.err.println(); - - - if (!GraphicsEnvironment.isHeadless()) { - - JOptionPane.showMessageDialog(null, message, "Error starting OpenRocket", - JOptionPane.ERROR_MESSAGE); - - } - - System.exit(1); - } - - - /** - * Presents the user with a message dialog and asks whether to continue. - * If the user does not select "Yes" the the application exits. - * - * @param message the message Strings to show. - */ - private static void confirm(String[] message) { - - if (!GraphicsEnvironment.isHeadless()) { - - if (JOptionPane.showConfirmDialog(null, message, "Error starting OpenRocket", - JOptionPane.YES_NO_OPTION) != JOptionPane.YES_OPTION) { - System.exit(1); - } - } - } -} diff --git a/src/net/sf/openrocket/unit/CaliberUnit.java b/src/net/sf/openrocket/unit/CaliberUnit.java deleted file mode 100644 index cc93cb51..00000000 --- a/src/net/sf/openrocket/unit/CaliberUnit.java +++ /dev/null @@ -1,133 +0,0 @@ -package net.sf.openrocket.unit; - -import java.util.Iterator; - -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.SymmetricComponent; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; - - -public class CaliberUnit extends GeneralUnit { - - public static final double DEFAULT_CALIBER = 0.01; - - private final Configuration configuration; - private final Rocket rocket; - - private int rocketModId = -1; - private int configurationModId = -1; - - private double caliber = -1; - - - - - public CaliberUnit(Configuration configuration) { - super(1.0, "cal"); - this.configuration = configuration; - - if (configuration == null) { - this.rocket = null; - } else { - this.rocket = configuration.getRocket(); - } - } - - public CaliberUnit(Rocket rocket) { - super(1.0, "cal"); - this.configuration = null; - this.rocket = rocket; - } - - public CaliberUnit(double reference) { - super(1.0, "cal"); - this.configuration = null; - this.rocket = null; - this.caliber = reference; - - if (reference <= 0) { - throw new IllegalArgumentException("Illegal reference = " + reference); - } - } - - - @Override - public double fromUnit(double value) { - checkCaliber(); - - return value * caliber; - } - - @Override - public double toUnit(double value) { - checkCaliber(); - - return value / caliber; - } - - - - private void checkCaliber() { - if (configuration != null && configuration.getModID() != configurationModId) { - caliber = -1; - configurationModId = configuration.getModID(); - } - if (rocket != null && rocket.getModID() != rocketModId) { - caliber = -1; - rocketModId = rocket.getModID(); - } - if (caliber < 0) { - if (configuration != null) { - caliber = calculateCaliber(configuration); - } else if (rocket != null) { - caliber = calculateCaliber(rocket); - } else { - throw new BugException("Both rocket and configuration are null"); - } - } - } - - - /** - * Calculate the caliber of a rocket configuration. - * - * @param config the rocket configuration - * @return the caliber of the rocket, or the default caliber. - */ - public static double calculateCaliber(Configuration config) { - return calculateCaliber(config.iterator()); - } - - /** - * Calculate the caliber of a rocket. - * - * @param rocket the rocket - * @return the caliber of the rocket, or the default caliber. - */ - public static double calculateCaliber(Rocket rocket) { - return calculateCaliber(rocket.iterator()); - } - - - - private static double calculateCaliber(Iterator<RocketComponent> iterator) { - double cal = 0; - - while (iterator.hasNext()) { - RocketComponent c = iterator.next(); - if (c instanceof SymmetricComponent) { - double r1 = ((SymmetricComponent) c).getForeRadius() * 2; - double r2 = ((SymmetricComponent) c).getAftRadius() * 2; - cal = MathUtil.max(cal, r1, r2); - } - } - - if (cal < 0.0001) - cal = DEFAULT_CALIBER; - - return cal; - } -} diff --git a/src/net/sf/openrocket/unit/DegreeUnit.java b/src/net/sf/openrocket/unit/DegreeUnit.java deleted file mode 100644 index 240e0b84..00000000 --- a/src/net/sf/openrocket/unit/DegreeUnit.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.sf.openrocket.unit; - -import java.text.DecimalFormat; - -import net.sf.openrocket.util.Chars; - -public class DegreeUnit extends GeneralUnit { - - public DegreeUnit() { - super(Math.PI/180.0, ""+Chars.DEGREE); - } - - @Override - public boolean hasSpace() { - return false; - } - - @Override - public double round(double v) { - return Math.rint(v); - } - - private final DecimalFormat decFormat = new DecimalFormat("0.#"); - @Override - public String toString(double value) { - double val = toUnit(value); - return decFormat.format(val); - } -} diff --git a/src/net/sf/openrocket/unit/FixedPrecisionUnit.java b/src/net/sf/openrocket/unit/FixedPrecisionUnit.java deleted file mode 100644 index c175d4ef..00000000 --- a/src/net/sf/openrocket/unit/FixedPrecisionUnit.java +++ /dev/null @@ -1,137 +0,0 @@ -package net.sf.openrocket.unit; - -import java.util.ArrayList; - -public class FixedPrecisionUnit extends Unit { - - private final double precision; - private final String formatString; - - public FixedPrecisionUnit(String unit, double precision) { - this(unit, precision, 1.0); - } - - public FixedPrecisionUnit(String unit, double precision, double multiplier) { - super(multiplier, unit); - - this.precision = precision; - - int decimals = 0; - double p = precision; - while ((p - Math.floor(p)) > 0.0000001) { - p *= 10; - decimals++; - } - formatString = "%." + decimals + "f"; - } - - - @Override - public double getNextValue(double value) { - return round(value + precision); - } - - @Override - public double getPreviousValue(double value) { - return round(value - precision); - } - - - @Override - public double round(double value) { - return Math.rint(value/precision)*precision; - } - - - - - @Override - public String toString(double value) { - return String.format(formatString, this.toUnit(value)); - } - - - - // TODO: LOW: This is copied from GeneralUnit, perhaps combine - @Override - public Tick[] getTicks(double start, double end, double minor, double major) { - // Convert values - start = toUnit(start); - end = toUnit(end); - minor = toUnit(minor); - major = toUnit(major); - - if (minor <= 0 || major <= 0 || major < minor) { - throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major); - } - - ArrayList<Tick> ticks = new ArrayList<Tick>(); - - int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable - double minstep; - - // Find the smallest possible step size - double one=1; - while (one > minor) - one /= 10; - while (one < minor) - one *= 10; - // one is the smallest round-ten that is larger than minor - if (one/2 >= minor) { - // smallest step is round-five - minstep = one/2; - mod2 = 2; // Changed later if clashes with major ticks - } else { - minstep = one; - mod2 = 10; // Changed later if clashes with major ticks - } - - // Find step size for major ticks - one = 1; - while (one > major) - one /= 10; - while (one < major) - one *= 10; - if (one/2 >= major) { - // major step is round-five, major-notable is next round-ten - double majorstep = one/2; - mod3 = (int)Math.round(majorstep/minstep); - mod4 = mod3*2; - } else { - // major step is round-ten, major-notable is next round-ten - mod3 = (int)Math.round(one/minstep); - mod4 = mod3*10; - } - // Check for clashes between minor-notable and major-nonnotable - if (mod3 == mod2) { - if (mod2==2) - mod2 = 1; // Every minor tick is notable - else - mod2 = 5; // Every fifth minor tick is notable - } - - - // Calculate starting position - int pos = (int)Math.ceil(start/minstep); -// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4); - while (pos*minstep <= end) { - double unitValue = pos*minstep; - double value = fromUnit(unitValue); - - if (pos%mod4 == 0) - ticks.add(new Tick(value,unitValue,true,true)); - else if (pos%mod3 == 0) - ticks.add(new Tick(value,unitValue,true,false)); - else if (pos%mod2 == 0) - ticks.add(new Tick(value,unitValue,false,true)); - else - ticks.add(new Tick(value,unitValue,false,false)); - - pos++; - } - - return ticks.toArray(new Tick[0]); - } - - -} diff --git a/src/net/sf/openrocket/unit/FrequencyUnit.java b/src/net/sf/openrocket/unit/FrequencyUnit.java deleted file mode 100644 index 7b5d15cf..00000000 --- a/src/net/sf/openrocket/unit/FrequencyUnit.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.sf.openrocket.unit; - -public class FrequencyUnit extends GeneralUnit { - - - public FrequencyUnit(double multiplier, String unit) { - super(multiplier, unit); - } - - - - @Override - public double toUnit(double value) { - double hz = 1/value; - return hz / multiplier; - } - - - @Override - public double fromUnit(double value) { - double hz = value * multiplier; - return 1/hz; - } - -} diff --git a/src/net/sf/openrocket/unit/GeneralUnit.java b/src/net/sf/openrocket/unit/GeneralUnit.java deleted file mode 100644 index d604fe6e..00000000 --- a/src/net/sf/openrocket/unit/GeneralUnit.java +++ /dev/null @@ -1,228 +0,0 @@ -package net.sf.openrocket.unit; - -import java.util.ArrayList; - -public class GeneralUnit extends Unit { - - private final int significantNumbers; - private final int decimalRounding; - - // Values smaller that this are rounded using decimal rounding - // [pre-calculated as 10^(significantNumbers-1)] - private final double decimalLimit; - - // Pre-calculated as 10^significantNumbers - private final double significantNumbersLimit; - - - public GeneralUnit(double multiplier, String unit) { - this(multiplier, unit, 2, 10); - } - - public GeneralUnit(double multiplier, String unit, int significantNumbers) { - this(multiplier, unit, significantNumbers, 10); - } - - public GeneralUnit(double multiplier, String unit, int significantNumbers, int decimalRounding) { - super(multiplier, unit); - assert(significantNumbers>0); - assert(decimalRounding>0); - - this.significantNumbers = significantNumbers; - this.decimalRounding = decimalRounding; - - double d=1; - double e=10; - for (int i=1; i<significantNumbers; i++) { - d *= 10.0; - e *= 10.0; - } - decimalLimit = d; - significantNumbersLimit = e; - } - - @Override - public double round(double value) { - if (value < decimalLimit) { - // Round to closest 1/decimalRounding - return Math.rint(value*decimalRounding)/decimalRounding; - } else { - // Round to given amount of significant numbers - double m = 1; - while (value >= significantNumbersLimit) { - m *= 10.0; - value /= 10.0; - } - return Math.rint(value)*m; - } - } - - - - - // TODO: LOW: untested - // start, end and scale in this units -// @Override - public ArrayList<Tick> getTicks(double start, double end, double scale) { - ArrayList<Tick> ticks = new ArrayList<Tick>(); - double delta; - int normal, major; - - // TODO: LOW: more fine-grained (e.g. 0||||5||||10||||15||||20) - if (scale <= 1.0/decimalRounding) { - delta = 1.0/decimalRounding; - normal = 1; - major = decimalRounding; - } else if (scale <= 1.0) { - delta = 1.0/decimalRounding; - normal = decimalRounding; - major = decimalRounding*10; - } else { - double r = scale; - delta = 1; - while (r > 10) { - r /= 10; - delta *= 10; - } - normal = 10; - major = 100; // TODO: LOW: More fine-grained with 5 - } - - double v = Math.ceil(start/delta)*delta; - int n = (int)Math.round(v/delta); - -// while (v <= end) { -// if (n%major == 0) -// ticks.add(new Tick(v,Tick.MAJOR)); -// else if (n%normal == 0) -// ticks.add(new Tick(v,Tick.NORMAL)); -// else -// ticks.add(new Tick(v,Tick.MINOR)); -// v += delta; -// n++; -// } - - return ticks; - } - - @Override - public Tick[] getTicks(double start, double end, double minor, double major) { - // Convert values - start = toUnit(start); - end = toUnit(end); - minor = toUnit(minor); - major = toUnit(major); - - if (minor <= 0 || major <= 0 || major < minor) { - throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major); - } - - ArrayList<Tick> ticks = new ArrayList<Tick>(); - - int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable - double minstep; - - // Find the smallest possible step size - double one=1; - while (one > minor) - one /= 10; - while (one < minor) - one *= 10; - // one is the smallest round-ten that is larger than minor - if (one/2 >= minor) { - // smallest step is round-five - minstep = one/2; - mod2 = 2; // Changed later if clashes with major ticks - } else { - minstep = one; - mod2 = 10; // Changed later if clashes with major ticks - } - - // Find step size for major ticks - one = 1; - while (one > major) - one /= 10; - while (one < major) - one *= 10; - if (one/2 >= major) { - // major step is round-five, major-notable is next round-ten - double majorstep = one/2; - mod3 = (int)Math.round(majorstep/minstep); - mod4 = mod3*2; - } else { - // major step is round-ten, major-notable is next round-ten - mod3 = (int)Math.round(one/minstep); - mod4 = mod3*10; - } - // Check for clashes between minor-notable and major-nonnotable - if (mod3 == mod2) { - if (mod2==2) - mod2 = 1; // Every minor tick is notable - else - mod2 = 5; // Every fifth minor tick is notable - } - - - // Calculate starting position - int pos = (int)Math.ceil(start/minstep); -// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4); - while (pos*minstep <= end) { - double unitValue = pos*minstep; - double value = fromUnit(unitValue); - - if (pos%mod4 == 0) - ticks.add(new Tick(value,unitValue,true,true)); - else if (pos%mod3 == 0) - ticks.add(new Tick(value,unitValue,true,false)); - else if (pos%mod2 == 0) - ticks.add(new Tick(value,unitValue,false,true)); - else - ticks.add(new Tick(value,unitValue,false,false)); - - pos++; - } - - return ticks.toArray(new Tick[0]); - } - - - @Override - public double getNextValue(double value) { - // TODO: HIGH: Auto-generated method stub - return value+1; - } - - @Override - public double getPreviousValue(double value) { - // TODO: HIGH: Auto-generated method stub - return value-1; - } - - - ///// TESTING: - - private static void printTicks(double start, double end, double minor, double major) { - Tick[] ticks = Unit.NOUNIT2.getTicks(start, end, minor, major); - String str = "Ticks for ("+start+","+end+","+minor+","+major+"):"; - for (int i=0; i<ticks.length; i++) { - str += " "+ticks[i].value; - if (ticks[i].major) { - if (ticks[i].notable) - str += "*"; - else - str += "o"; - } else { - if (ticks[i].notable) - str += "_"; - else - str += " "; - } - } - System.out.println(str); - } - public static void main(String[] arg) { - printTicks(0,100,1,10); - printTicks(4.7,11.0,0.15,0.7); - } - -} diff --git a/src/net/sf/openrocket/unit/RadianUnit.java b/src/net/sf/openrocket/unit/RadianUnit.java deleted file mode 100644 index 18f1aff6..00000000 --- a/src/net/sf/openrocket/unit/RadianUnit.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.openrocket.unit; - -import java.text.DecimalFormat; - -public class RadianUnit extends GeneralUnit { - - public RadianUnit() { - super(1,"rad"); - } - - @Override - public double round(double v) { - return Math.rint(v*10.0)/10.0; - } - - private final DecimalFormat decFormat = new DecimalFormat("0.0"); - @Override - public String toString(double value) { - double val = toUnit(value); - return decFormat.format(val); - } -} diff --git a/src/net/sf/openrocket/unit/TemperatureUnit.java b/src/net/sf/openrocket/unit/TemperatureUnit.java deleted file mode 100644 index 23645ceb..00000000 --- a/src/net/sf/openrocket/unit/TemperatureUnit.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.sf.openrocket.unit; - -public class TemperatureUnit extends FixedPrecisionUnit { - - protected final double addition; - - public TemperatureUnit(double multiplier, double addition, String unit) { - super(unit, 1, multiplier); - - this.addition = addition; - } - - @Override - public boolean hasSpace() { - return false; - } - - @Override - public double toUnit(double value) { - return value/multiplier - addition; - } - - @Override - public double fromUnit(double value) { - return (value + addition)*multiplier; - } -} diff --git a/src/net/sf/openrocket/unit/Tick.java b/src/net/sf/openrocket/unit/Tick.java deleted file mode 100644 index 4448aac1..00000000 --- a/src/net/sf/openrocket/unit/Tick.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.sf.openrocket.unit; - -public final class Tick { - public final double value; - public final double unitValue; - public final boolean major; - public final boolean notable; - - public Tick(double value, double unitValue, boolean major, boolean notable) { - this.value = value; - this.unitValue = unitValue; - this.major = major; - this.notable = notable; - } - - @Override - public String toString() { - String s = "Tick[value="+value; - if (major) - s += ",major"; - else - s += ",minor"; - if (notable) - s += ",notable"; - s+= "]"; - return s; - } -} diff --git a/src/net/sf/openrocket/unit/Unit.java b/src/net/sf/openrocket/unit/Unit.java deleted file mode 100644 index 4703b31b..00000000 --- a/src/net/sf/openrocket/unit/Unit.java +++ /dev/null @@ -1,237 +0,0 @@ -package net.sf.openrocket.unit; - -import java.text.DecimalFormat; - -import net.sf.openrocket.util.Chars; - -public abstract class Unit { - - /** No unit with 2 digit precision */ - public static final Unit NOUNIT2 = new GeneralUnit(1, "" + Chars.ZWSP, 2); - - protected final double multiplier; // meters = units * multiplier - protected final String unit; - - /** - * Creates a new Unit with a given multiplier and unit name. - * - * Multiplier e.g. 1 in = 0.0254 meter - * - * @param multiplier The multiplier to use on the value, 1 this unit == multiplier SI units - * @param unit The unit's short form. - */ - public Unit(double multiplier, String unit) { - if (multiplier == 0) - throw new IllegalArgumentException("Unit has multiplier=0"); - this.multiplier = multiplier; - this.unit = unit; - } - - /** - * Converts from SI units to this unit. The default implementation simply divides by the - * multiplier. - * - * @param value Value in SI unit - * @return Value in these units - */ - public double toUnit(double value) { - return value / multiplier; - } - - /** - * Convert from this type of units to SI units. The default implementation simply - * multiplies by the multiplier. - * - * @param value Value in these units - * @return Value in SI units - */ - public double fromUnit(double value) { - return value * multiplier; - } - - - /** - * Return the unit name. - * - * @return the unit. - */ - public String getUnit() { - return unit; - } - - /** - * Whether the value and unit should be separated by a whitespace. This method - * returns true as most units have a space between the value and unit, but may be - * overridden. - * - * @return true if the value and unit should be separated - */ - public boolean hasSpace() { - return true; - } - - - // Testcases for toString(double) - public static void main(String arg[]) { - System.out.println(NOUNIT2.toString(0.0049)); - System.out.println(NOUNIT2.toString(0.0050)); - System.out.println(NOUNIT2.toString(0.0051)); - System.out.println(NOUNIT2.toString(0.00123)); - System.out.println(NOUNIT2.toString(0.0123)); - System.out.println(NOUNIT2.toString(0.1234)); - System.out.println(NOUNIT2.toString(1.2345)); - System.out.println(NOUNIT2.toString(12.345)); - System.out.println(NOUNIT2.toString(123.456)); - System.out.println(NOUNIT2.toString(1234.5678)); - System.out.println(NOUNIT2.toString(12345.6789)); - System.out.println(NOUNIT2.toString(123456.789)); - System.out.println(NOUNIT2.toString(1234567.89)); - System.out.println(NOUNIT2.toString(12345678.9)); - - System.out.println(NOUNIT2.toString(-0.0049)); - System.out.println(NOUNIT2.toString(-0.0050)); - System.out.println(NOUNIT2.toString(-0.0051)); - System.out.println(NOUNIT2.toString(-0.00123)); - System.out.println(NOUNIT2.toString(-0.0123)); - System.out.println(NOUNIT2.toString(-0.1234)); - System.out.println(NOUNIT2.toString(-1.2345)); - System.out.println(NOUNIT2.toString(-12.345)); - System.out.println(NOUNIT2.toString(-123.456)); - System.out.println(NOUNIT2.toString(-1234.5678)); - System.out.println(NOUNIT2.toString(-12345.6789)); - System.out.println(NOUNIT2.toString(-123456.789)); - System.out.println(NOUNIT2.toString(-1234567.89)); - System.out.println(NOUNIT2.toString(-12345678.9)); - - } - - - @Override - public String toString() { - return unit; - } - - // TODO: Should this use grouping separator ("#,##0.##")? - - private static final DecimalFormat intFormat = new DecimalFormat("#"); - private static final DecimalFormat decFormat = new DecimalFormat("0.##"); - private static final DecimalFormat expFormat = new DecimalFormat("0.00E0"); - - /** - * Format the given value (in SI units) to a string representation of the value in this - * units. An suitable amount of decimals for the unit are used in the representation. - * The unit is not appended to the numerical value. - * - * @param value Value in SI units. - * @return A string representation of the number in these units. - */ - public String toString(double value) { - double val = toUnit(value); - - if (Math.abs(val) > 1E6) { - return expFormat.format(val); - } - if (Math.abs(val) >= 100) { - return intFormat.format(val); - } - if (Math.abs(val) <= 0.005) { - return "0"; - } - - double sign = Math.signum(val); - val = Math.abs(val); - double mul = 1.0; - while (val < 100) { - mul *= 10; - val *= 10; - } - val = Math.rint(val) / mul * sign; - - return decFormat.format(val); - } - - - /** - * Return a string with the specified value and unit. The value is converted into - * this unit. If <code>value</code> is NaN, returns "N/A" (not applicable). - * - * @param value the value to print in SI units. - * @return the value and unit, or "N/A". - */ - public String toStringUnit(double value) { - if (Double.isNaN(value)) - return "N/A"; - - String s = toString(value); - if (hasSpace()) - s += " "; - s += unit; - return s; - } - - - - /** - * Creates a new Value object with the specified value and this unit. - * - * @param value the value to set. - * @return a new Value object. - */ - public Value toValue(double value) { - return new Value(value, this); - } - - - - /** - * Round the value (in the current units) to a precision suitable for rough valuing - * (approximately 2 significant numbers). - * - * @param value Value in current units - * @return Rounded value. - */ - public abstract double round(double value); - - /** - * Return the next rounded value after the given value. - * @param value Value in these units. - * @return The next suitable rounded value. - */ - public abstract double getNextValue(double value); - - /** - * Return the previous rounded value before the given value. - * @param value Value in these units. - * @return The previous suitable rounded value. - */ - public abstract double getPreviousValue(double value); - - //public abstract ArrayList<Tick> getTicks(double start, double end, double scale); - - /** - * Return ticks in the range start - end (in current units). minor is the minimum - * distance between minor, non-notable ticks and major the minimum distance between - * major non-notable ticks. The values are in current units, i.e. no conversion is - * performed. - */ - public abstract Tick[] getTicks(double start, double end, double minor, double major); - - /** - * Compares whether the two units are equal. Equality requires the unit classes, - * multiplier values and units to be equal. - */ - @Override - public boolean equals(Object other) { - if (other == null) - return false; - if (this.getClass() != other.getClass()) - return false; - return ((this.multiplier == ((Unit) other).multiplier) && this.unit.equals(((Unit) other).unit)); - } - - @Override - public int hashCode() { - return this.getClass().hashCode() + this.unit.hashCode(); - } - -} diff --git a/src/net/sf/openrocket/unit/UnitGroup.java b/src/net/sf/openrocket/unit/UnitGroup.java deleted file mode 100644 index f72f22a9..00000000 --- a/src/net/sf/openrocket/unit/UnitGroup.java +++ /dev/null @@ -1,569 +0,0 @@ -package net.sf.openrocket.unit; - -import static net.sf.openrocket.util.Chars.*; -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import net.sf.openrocket.rocketcomponent.Configuration; -import net.sf.openrocket.rocketcomponent.Rocket; - - -/** - * A group of units (eg. length, mass etc.). Contains a list of different units of a same - * quantity. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class UnitGroup { - - public static final UnitGroup UNITS_NONE; - - public static final UnitGroup UNITS_MOTOR_DIMENSIONS; - public static final UnitGroup UNITS_LENGTH; - public static final UnitGroup UNITS_DISTANCE; - - public static final UnitGroup UNITS_AREA; - public static final UnitGroup UNITS_STABILITY; - /** - * This unit group contains only the caliber unit that never scales the originating "SI" value. - * It can be used in cases where the originating value is already in calibers to obtains the correct unit. - */ - public static final UnitGroup UNITS_STABILITY_CALIBERS; - public static final UnitGroup UNITS_VELOCITY; - public static final UnitGroup UNITS_ACCELERATION; - public static final UnitGroup UNITS_MASS; - public static final UnitGroup UNITS_INERTIA; - public static final UnitGroup UNITS_ANGLE; - public static final UnitGroup UNITS_DENSITY_BULK; - public static final UnitGroup UNITS_DENSITY_SURFACE; - public static final UnitGroup UNITS_DENSITY_LINE; - public static final UnitGroup UNITS_FORCE; - public static final UnitGroup UNITS_IMPULSE; - - /** Time in the order of less than a second (time step etc). */ - public static final UnitGroup UNITS_TIME_STEP; - - /** Time in the order of seconds (motor delay etc). */ - public static final UnitGroup UNITS_SHORT_TIME; - - /** Time in the order of the flight time of a rocket. */ - public static final UnitGroup UNITS_FLIGHT_TIME; - public static final UnitGroup UNITS_ROLL; - public static final UnitGroup UNITS_TEMPERATURE; - public static final UnitGroup UNITS_PRESSURE; - public static final UnitGroup UNITS_RELATIVE; - public static final UnitGroup UNITS_ROUGHNESS; - - public static final UnitGroup UNITS_COEFFICIENT; - - // public static final UnitGroup UNITS_FREQUENCY; - - - public static final Map<String, UnitGroup> UNITS; - - - /* - * Note: Units may not use HTML tags. - * - * The scaling value "X" is obtained by "one of this unit is X of SI units" - * Type into Google for example: "1 in^2 in m^2" - */ - static { - UNITS_NONE = new UnitGroup(); - UNITS_NONE.addUnit(Unit.NOUNIT2); - - UNITS_LENGTH = new UnitGroup(); - UNITS_LENGTH.addUnit(new GeneralUnit(0.001, "mm")); - UNITS_LENGTH.addUnit(new GeneralUnit(0.01, "cm")); - UNITS_LENGTH.addUnit(new GeneralUnit(1, "m")); - UNITS_LENGTH.addUnit(new GeneralUnit(0.0254, "in")); - UNITS_LENGTH.addUnit(new GeneralUnit(0.3048, "ft")); - UNITS_LENGTH.setDefaultUnit(1); - - UNITS_MOTOR_DIMENSIONS = new UnitGroup(); - UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.001, "mm")); - UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.01, "cm")); - UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.0254, "in")); - UNITS_MOTOR_DIMENSIONS.setDefaultUnit(0); - - UNITS_DISTANCE = new UnitGroup(); - UNITS_DISTANCE.addUnit(new GeneralUnit(1, "m")); - UNITS_DISTANCE.addUnit(new GeneralUnit(1000, "km")); - UNITS_DISTANCE.addUnit(new GeneralUnit(0.3048, "ft")); - UNITS_DISTANCE.addUnit(new GeneralUnit(0.9144, "yd")); - UNITS_DISTANCE.addUnit(new GeneralUnit(1609.344, "mi")); - UNITS_DISTANCE.addUnit(new GeneralUnit(1852, "nmi")); - - UNITS_AREA = new UnitGroup(); - UNITS_AREA.addUnit(new GeneralUnit(pow2(0.001), "mm" + SQUARED)); - UNITS_AREA.addUnit(new GeneralUnit(pow2(0.01), "cm" + SQUARED)); - UNITS_AREA.addUnit(new GeneralUnit(1, "m" + SQUARED)); - UNITS_AREA.addUnit(new GeneralUnit(pow2(0.0254), "in" + SQUARED)); - UNITS_AREA.addUnit(new GeneralUnit(pow2(0.3048), "ft" + SQUARED)); - UNITS_AREA.setDefaultUnit(1); - - - UNITS_STABILITY = new UnitGroup(); - UNITS_STABILITY.addUnit(new GeneralUnit(0.001, "mm")); - UNITS_STABILITY.addUnit(new GeneralUnit(0.01, "cm")); - UNITS_STABILITY.addUnit(new GeneralUnit(0.0254, "in")); - UNITS_STABILITY.addUnit(new CaliberUnit((Rocket) null)); - UNITS_STABILITY.setDefaultUnit(3); - - UNITS_STABILITY_CALIBERS = new UnitGroup(); - UNITS_STABILITY_CALIBERS.addUnit(new GeneralUnit(1, "cal")); - - - UNITS_VELOCITY = new UnitGroup(); - UNITS_VELOCITY.addUnit(new GeneralUnit(1, "m/s")); - UNITS_VELOCITY.addUnit(new GeneralUnit(1 / 3.6, "km/h")); - UNITS_VELOCITY.addUnit(new GeneralUnit(0.3048, "ft/s")); - UNITS_VELOCITY.addUnit(new GeneralUnit(0.44704, "mph")); - - UNITS_ACCELERATION = new UnitGroup(); - UNITS_ACCELERATION.addUnit(new GeneralUnit(1, "m/s" + SQUARED)); - UNITS_ACCELERATION.addUnit(new GeneralUnit(0.3048, "ft/s" + SQUARED)); - UNITS_ACCELERATION.addUnit(new GeneralUnit(9.80665, "G")); - - UNITS_MASS = new UnitGroup(); - UNITS_MASS.addUnit(new GeneralUnit(0.001, "g")); - UNITS_MASS.addUnit(new GeneralUnit(1, "kg")); - UNITS_MASS.addUnit(new GeneralUnit(0.0283495231, "oz")); - UNITS_MASS.addUnit(new GeneralUnit(0.45359237, "lb")); - - UNITS_INERTIA = new UnitGroup(); - UNITS_INERTIA.addUnit(new GeneralUnit(0.0001, "kg" + DOT + "cm" + SQUARED)); - UNITS_INERTIA.addUnit(new GeneralUnit(1, "kg" + DOT + "m" + SQUARED)); - UNITS_INERTIA.addUnit(new GeneralUnit(1.82899783e-5, "oz" + DOT + "in" + SQUARED)); - UNITS_INERTIA.addUnit(new GeneralUnit(0.000292639653, "lb" + DOT + "in" + SQUARED)); - UNITS_INERTIA.addUnit(new GeneralUnit(0.0421401101, "lb" + DOT + "ft" + SQUARED)); - UNITS_INERTIA.addUnit(new GeneralUnit(1.35581795, "lbf" + DOT + "ft" + DOT + "s" + SQUARED)); - UNITS_INERTIA.setDefaultUnit(1); - - UNITS_ANGLE = new UnitGroup(); - UNITS_ANGLE.addUnit(new DegreeUnit()); - UNITS_ANGLE.addUnit(new FixedPrecisionUnit("rad", 0.01)); - UNITS_ANGLE.addUnit(new GeneralUnit(1.0 / 3437.74677078, "arcmin")); - - UNITS_DENSITY_BULK = new UnitGroup(); - UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1000, "g/cm" + CUBED)); - UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1, "kg/m" + CUBED)); - UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1729.99404, "oz/in" + CUBED)); - UNITS_DENSITY_BULK.addUnit(new GeneralUnit(16.0184634, "lb/ft" + CUBED)); - - UNITS_DENSITY_SURFACE = new UnitGroup(); - UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(10, "g/cm" + SQUARED)); - UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.001, "g/m" + SQUARED)); - UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(1, "kg/m" + SQUARED)); - UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(43.9418487, "oz/in" + SQUARED)); - UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.305151727, "oz/ft" + SQUARED)); - UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(4.88242764, "lb/ft" + SQUARED)); - UNITS_DENSITY_SURFACE.setDefaultUnit(1); - - UNITS_DENSITY_LINE = new UnitGroup(); - UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.001, "g/m")); - UNITS_DENSITY_LINE.addUnit(new GeneralUnit(1, "kg/m")); - UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.0930102465, "oz/ft")); - - UNITS_FORCE = new UnitGroup(); - UNITS_FORCE.addUnit(new GeneralUnit(1, "N")); - UNITS_FORCE.addUnit(new GeneralUnit(4.44822162, "lbf")); - UNITS_FORCE.addUnit(new GeneralUnit(9.80665, "kgf")); - - UNITS_IMPULSE = new UnitGroup(); - UNITS_IMPULSE.addUnit(new GeneralUnit(1, "Ns")); - UNITS_IMPULSE.addUnit(new GeneralUnit(4.44822162, "lbf" + DOT + "s")); - - UNITS_TIME_STEP = new UnitGroup(); - UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("ms", 1, 0.001)); - UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("s", 0.01)); - UNITS_TIME_STEP.setDefaultUnit(1); - - UNITS_SHORT_TIME = new UnitGroup(); - UNITS_SHORT_TIME.addUnit(new GeneralUnit(1, "s")); - - UNITS_FLIGHT_TIME = new UnitGroup(); - UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(1, "s")); - UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(60, "min")); - - UNITS_ROLL = new UnitGroup(); - UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s")); - UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI, "r/s")); - UNITS_ROLL.addUnit(new GeneralUnit(2 * Math.PI / 60, "rpm")); - UNITS_ROLL.setDefaultUnit(1); - - UNITS_TEMPERATURE = new UnitGroup(); - UNITS_TEMPERATURE.addUnit(new FixedPrecisionUnit("K", 1)); - UNITS_TEMPERATURE.addUnit(new TemperatureUnit(1, 273.15, DEGREE + "C")); - UNITS_TEMPERATURE.addUnit(new TemperatureUnit(5.0 / 9.0, 459.67, DEGREE + "F")); - UNITS_TEMPERATURE.setDefaultUnit(1); - - UNITS_PRESSURE = new UnitGroup(); - UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("mbar", 1, 1.0e2)); - UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("bar", 0.001, 1.0e5)); - UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("atm", 0.001, 1.01325e5)); - UNITS_PRESSURE.addUnit(new GeneralUnit(101325.0 / 760.0, "mmHg")); - UNITS_PRESSURE.addUnit(new GeneralUnit(3386.389, "inHg")); - UNITS_PRESSURE.addUnit(new GeneralUnit(6894.75729, "psi")); - UNITS_PRESSURE.addUnit(new GeneralUnit(1, "Pa")); - - UNITS_RELATIVE = new UnitGroup(); - UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01, 1.0)); - UNITS_RELATIVE.addUnit(new GeneralUnit(0.01, "%")); - UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + PERMILLE, 1, 0.001)); - // UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01, 1.0)); - // UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("%", 1, 0.01)); - // UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + PERMILLE, 1, 0.001)); - UNITS_RELATIVE.setDefaultUnit(1); - - - UNITS_ROUGHNESS = new UnitGroup(); - UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.000001, MICRO + "m")); - UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.0000254, "mil")); - - - UNITS_COEFFICIENT = new UnitGroup(); - UNITS_COEFFICIENT.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01)); // zero-width space - - - // This is not used by OpenRocket, and not extensively tested: - // UNITS_FREQUENCY = new UnitGroup(); - // UNITS_FREQUENCY.addUnit(new GeneralUnit(1, "s")); - // UNITS_FREQUENCY.addUnit(new GeneralUnit(0.001, "ms")); - // UNITS_FREQUENCY.addUnit(new GeneralUnit(0.000001, MICRO + "s")); - // UNITS_FREQUENCY.addUnit(new FrequencyUnit(1, "Hz")); - // UNITS_FREQUENCY.addUnit(new FrequencyUnit(1000, "kHz")); - // UNITS_FREQUENCY.setDefaultUnit(3); - - - HashMap<String, UnitGroup> map = new HashMap<String, UnitGroup>(); - map.put("NONE", UNITS_NONE); - map.put("LENGTH", UNITS_LENGTH); - map.put("MOTOR_DIMENSIONS", UNITS_MOTOR_DIMENSIONS); - map.put("DISTANCE", UNITS_DISTANCE); - map.put("VELOCITY", UNITS_VELOCITY); - map.put("ACCELERATION", UNITS_ACCELERATION); - map.put("AREA", UNITS_AREA); - map.put("STABILITY", UNITS_STABILITY); - map.put("MASS", UNITS_MASS); - map.put("INERTIA", UNITS_INERTIA); - map.put("ANGLE", UNITS_ANGLE); - map.put("DENSITY_BULK", UNITS_DENSITY_BULK); - map.put("DENSITY_SURFACE", UNITS_DENSITY_SURFACE); - map.put("DENSITY_LINE", UNITS_DENSITY_LINE); - map.put("FORCE", UNITS_FORCE); - map.put("IMPULSE", UNITS_IMPULSE); - map.put("TIME_STEP", UNITS_TIME_STEP); - map.put("SHORT_TIME", UNITS_SHORT_TIME); - map.put("FLIGHT_TIME", UNITS_FLIGHT_TIME); - map.put("ROLL", UNITS_ROLL); - map.put("TEMPERATURE", UNITS_TEMPERATURE); - map.put("PRESSURE", UNITS_PRESSURE); - map.put("RELATIVE", UNITS_RELATIVE); - map.put("ROUGHNESS", UNITS_ROUGHNESS); - map.put("COEFFICIENT", UNITS_COEFFICIENT); - - UNITS = Collections.unmodifiableMap(map); - } - - public static void setDefaultMetricUnits() { - UNITS_LENGTH.setDefaultUnit("cm"); - UNITS_MOTOR_DIMENSIONS.setDefaultUnit("mm"); - UNITS_DISTANCE.setDefaultUnit("m"); - UNITS_AREA.setDefaultUnit("cm" + SQUARED); - UNITS_STABILITY.setDefaultUnit("cal"); - UNITS_VELOCITY.setDefaultUnit("m/s"); - UNITS_ACCELERATION.setDefaultUnit("m/s" + SQUARED); - UNITS_MASS.setDefaultUnit("g"); - UNITS_INERTIA.setDefaultUnit("kg" + DOT + "m" + SQUARED); - UNITS_ANGLE.setDefaultUnit("" + DEGREE); - UNITS_DENSITY_BULK.setDefaultUnit("g/cm" + CUBED); - UNITS_DENSITY_SURFACE.setDefaultUnit("g/m" + SQUARED); - UNITS_DENSITY_LINE.setDefaultUnit("g/m"); - UNITS_FORCE.setDefaultUnit("N"); - UNITS_IMPULSE.setDefaultUnit("Ns"); - UNITS_TIME_STEP.setDefaultUnit("s"); - UNITS_FLIGHT_TIME.setDefaultUnit("s"); - UNITS_ROLL.setDefaultUnit("r/s"); - UNITS_TEMPERATURE.setDefaultUnit(DEGREE + "C"); - UNITS_PRESSURE.setDefaultUnit("mbar"); - UNITS_RELATIVE.setDefaultUnit("%"); - UNITS_ROUGHNESS.setDefaultUnit(MICRO + "m"); - } - - public static void setDefaultImperialUnits() { - UNITS_LENGTH.setDefaultUnit("in"); - UNITS_MOTOR_DIMENSIONS.setDefaultUnit("in"); - UNITS_DISTANCE.setDefaultUnit("ft"); - UNITS_AREA.setDefaultUnit("in" + SQUARED); - UNITS_STABILITY.setDefaultUnit("cal"); - UNITS_VELOCITY.setDefaultUnit("ft/s"); - UNITS_ACCELERATION.setDefaultUnit("ft/s" + SQUARED); - UNITS_MASS.setDefaultUnit("oz"); - UNITS_INERTIA.setDefaultUnit("lb" + DOT + "ft" + SQUARED); - UNITS_ANGLE.setDefaultUnit("" + DEGREE); - UNITS_DENSITY_BULK.setDefaultUnit("oz/in" + CUBED); - UNITS_DENSITY_SURFACE.setDefaultUnit("oz/ft" + SQUARED); - UNITS_DENSITY_LINE.setDefaultUnit("oz/ft"); - UNITS_FORCE.setDefaultUnit("N"); - UNITS_IMPULSE.setDefaultUnit("Ns"); - UNITS_TIME_STEP.setDefaultUnit("s"); - UNITS_FLIGHT_TIME.setDefaultUnit("s"); - UNITS_ROLL.setDefaultUnit("r/s"); - UNITS_TEMPERATURE.setDefaultUnit(DEGREE + "F"); - UNITS_PRESSURE.setDefaultUnit("mbar"); - UNITS_RELATIVE.setDefaultUnit("%"); - UNITS_ROUGHNESS.setDefaultUnit("mil"); - } - - - /** - * Return a UnitGroup for stability units based on the rocket. - * - * @param rocket the rocket from which to calculate the caliber - * @return the unit group - */ - public static UnitGroup stabilityUnits(Rocket rocket) { - return new StabilityUnitGroup(rocket); - } - - - /** - * Return a UnitGroup for stability units based on the rocket configuration. - * - * @param config the rocket configuration from which to calculate the caliber - * @return the unit group - */ - public static UnitGroup stabilityUnits(Configuration config) { - return new StabilityUnitGroup(config); - } - - - /** - * Return a UnitGroup for stability units based on a constant caliber. - * - * @param reference the constant reference length - * @return the unit group - */ - public static UnitGroup stabilityUnits(double reference) { - return new StabilityUnitGroup(reference); - } - - - ////////////////////////////////////////////////////// - - - protected ArrayList<Unit> units = new ArrayList<Unit>(); - protected int defaultUnit = 0; - - public int getUnitCount() { - return units.size(); - } - - public Unit getDefaultUnit() { - return units.get(defaultUnit); - } - - public int getDefaultUnitIndex() { - return defaultUnit; - } - - public void setDefaultUnit(int n) { - if (n < 0 || n >= units.size()) { - throw new IllegalArgumentException("index out of range: " + n); - } - defaultUnit = n; - } - - - - /** - * Find a unit by approximate unit name. Only letters and (ordinary) numbers are - * considered in the matching. This method is mainly means for testing, allowing - * a simple means to obtain a particular unit. - * - * @param str the unit name. - * @return the corresponding unit, or <code>null</code> if not found. - */ - public Unit findApproximate(String str) { - str = str.replaceAll("\\W", "").trim(); - for (Unit u : units) { - String name = u.getUnit().replaceAll("\\W", "").trim(); - if (str.equalsIgnoreCase(name)) - return u; - } - return null; - } - - /** - * Set the default unit based on the unit name. Throws an exception if a - * unit with the provided name is not available. - * - * @param name the unit name. - * @throws IllegalArgumentException if the corresponding unit is not found in the group. - */ - public void setDefaultUnit(String name) throws IllegalArgumentException { - for (int i = 0; i < units.size(); i++) { - if (units.get(i).getUnit().equals(name)) { - setDefaultUnit(i); - return; - } - } - throw new IllegalArgumentException("name=" + name); - } - - - public Unit getUnit(int n) { - return units.get(n); - } - - public int getUnitIndex(Unit u) { - return units.indexOf(u); - } - - public void addUnit(Unit u) { - units.add(u); - } - - public boolean contains(Unit u) { - return units.contains(u); - } - - public Unit[] getUnits() { - return units.toArray(new Unit[0]); - } - - - /** - * Return the value formatted by the default unit of this group. - * It is the same as calling <code>getDefaultUnit().toString(value)</code>. - * - * @param value the SI value to format. - * @return the formatted string. - * @see Unit#toString(double) - */ - public String toString(double value) { - return this.getDefaultUnit().toString(value); - } - - - /** - * Return the value formatted by the default unit of this group including the unit. - * It is the same as calling <code>getDefaultUnit().toStringUnit(value)</code>. - * - * @param value the SI value to format. - * @return the formatted string. - * @see Unit#toStringUnit(double) - */ - public String toStringUnit(double value) { - return this.getDefaultUnit().toStringUnit(value); - } - - - - - - /** - * Creates a new Value object with the specified value and the default unit of this group. - * - * @param value the value to set. - * @return a new Value object. - */ - public Value toValue(double value) { - return this.getDefaultUnit().toValue(value); - } - - - - - private static final Pattern STRING_PATTERN = Pattern.compile("^\\s*([0-9.,-]+)(.*?)$"); - - /** - * Converts a string into an SI value. If the string has one of the units in this - * group appended to it, that unit will be used in conversion. Otherwise the default - * unit will be used. If an unknown unit is specified or the value does not parse - * with <code>Double.parseDouble</code> then a <code>NumberFormatException</code> - * is thrown. - * <p> - * This method is applicable only for simple units without e.g. powers. - * - * @param str the string to parse. - * @return the SI value. - * @throws NumberFormatException if the string cannot be parsed. - */ - public double fromString(String str) { - Matcher matcher = STRING_PATTERN.matcher(str); - - if (!matcher.matches()) { - throw new NumberFormatException("string did not match required pattern"); - } - - double value = Double.parseDouble(matcher.group(1)); - String unit = matcher.group(2).trim(); - - if (unit.equals("")) { - value = this.getDefaultUnit().fromUnit(value); - } else { - int i; - for (i = 0; i < units.size(); i++) { - Unit u = units.get(i); - if (unit.equalsIgnoreCase(u.getUnit())) { - value = u.fromUnit(value); - break; - } - } - if (i >= units.size()) { - throw new NumberFormatException("unknown unit " + unit); - } - } - - return value; - } - - - /////////////////////////// - - - /** - * A private class that switches the CaliberUnit to a rocket-specific CaliberUnit. - * All other methods are passed through to UNITS_STABILITY. - */ - private static class StabilityUnitGroup extends UnitGroup { - - public StabilityUnitGroup(double ref) { - this(new CaliberUnit(ref)); - } - - public StabilityUnitGroup(Rocket rocket) { - this(new CaliberUnit(rocket)); - } - - public StabilityUnitGroup(Configuration config) { - this(new CaliberUnit(config)); - } - - private StabilityUnitGroup(CaliberUnit caliberUnit) { - this.units.addAll(UnitGroup.UNITS_STABILITY.units); - this.defaultUnit = UnitGroup.UNITS_STABILITY.defaultUnit; - for (int i = 0; i < units.size(); i++) { - if (units.get(i) instanceof CaliberUnit) { - units.set(i, caliberUnit); - } - } - } - - - @Override - public void setDefaultUnit(int n) { - super.setDefaultUnit(n); - UNITS_STABILITY.setDefaultUnit(n); - } - } -} diff --git a/src/net/sf/openrocket/unit/Value.java b/src/net/sf/openrocket/unit/Value.java deleted file mode 100644 index 65382523..00000000 --- a/src/net/sf/openrocket/unit/Value.java +++ /dev/null @@ -1,153 +0,0 @@ -package net.sf.openrocket.unit; - -import net.sf.openrocket.util.MathUtil; - -/** - * An immutable class representing an SI value and a unit. The toString() method yields the - * current value in the current units. This class may be used to encapsulate - * a sortable value for example for tables. The sorting is performed by the - * value in the current units, ignoring the unit. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Value implements Comparable<Value> { - - private final double value; - private final Unit unit; - - - /** - * Create a new Value object. - * - * @param value the value to set. - * @param unit the unit to set (<code>null</code> not allowed) - */ - public Value(double value, Unit unit) { - if (unit == null) { - throw new IllegalArgumentException("unit is null"); - } - this.value = value; - this.unit = unit; - } - - - /** - * Creates a new Value object using unit group. Currently it simply uses the default - * unit of the group, but may later change. - * - * @param value the value to set. - * @param group the group the value belongs to. - */ - public Value(double value, UnitGroup group) { - this(value, group.getDefaultUnit()); - } - - - /** - * Get the value of this object (in SI units). - * - * @return the value - */ - public double getValue() { - return value; - } - - - - /** - * Get the value of this object in the current units. - * - * @return the value in the current units. - */ - public double getUnitValue() { - return unit.toUnit(value); - } - - - /** - * Get the unit of this object. - * - * @return the unit - */ - public Unit getUnit() { - return unit; - } - - - /** - * Return a string formatted using the {@link Unit#toStringUnit(double)} method - * of the current unit. If the unit is <code>null</code> then the UNITS_NONE - * group is used. - */ - @Override - public String toString() { - return unit.toStringUnit(value); - } - - - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - - Value other = (Value) obj; - if (this.unit != other.unit) { - return false; - } - - if (!MathUtil.equals(this.value, other.value)) { - return false; - } - - return true; - } - - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((unit == null) ? 0 : unit.hashCode()); - long temp; - temp = Double.doubleToLongBits(value); - result = prime * result + (int) (temp ^ (temp >>> 32)); - return result; - } - - - /** - * Compare this value to another value. The comparison is performed primarily by - * the unit text, secondarily the value in the unit values. - */ - @Override - public int compareTo(Value o) { - int n = this.getUnit().getUnit().compareTo(o.getUnit().getUnit()); - if (n != 0) - return n; - - double us = this.getUnitValue(); - double them = o.getUnitValue(); - - if (Double.isNaN(us)) { - if (Double.isNaN(them)) - return 0; - else - return 1; - } - if (Double.isNaN(them)) - return -1; - - if (us < them) - return -1; - else if (us > them) - return 1; - else - return 0; - } - -} diff --git a/src/net/sf/openrocket/unit/ValueComparator.java b/src/net/sf/openrocket/unit/ValueComparator.java deleted file mode 100644 index a948e9d2..00000000 --- a/src/net/sf/openrocket/unit/ValueComparator.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.sf.openrocket.unit; - -import java.util.Comparator; - -public class ValueComparator implements Comparator<Value> { - - public static final ValueComparator INSTANCE = new ValueComparator(); - - @Override - public int compare(Value o1, Value o2) { - return o1.compareTo(o2); - } - -} diff --git a/src/net/sf/openrocket/util/AbstractChangeSource.java b/src/net/sf/openrocket/util/AbstractChangeSource.java deleted file mode 100644 index 57664aed..00000000 --- a/src/net/sf/openrocket/util/AbstractChangeSource.java +++ /dev/null @@ -1,48 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.EventListener; -import java.util.EventObject; -import java.util.List; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * Abstract implementation of a ChangeSource. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class AbstractChangeSource implements ChangeSource { - private static final LogHelper log = Application.getLogger(); - - private final List<EventListener> listeners = new ArrayList<EventListener>(); - - private final EventObject event = new EventObject(this); - - - @Override - public final void addChangeListener(EventListener listener) { - listeners.add(listener); - log.verbose(1, "Adding change listeners, listener count is now " + listeners.size()); - } - - @Override - public final void removeChangeListener(EventListener listener) { - listeners.remove(listener); - log.verbose(1, "Removing change listeners, listener count is now " + listeners.size()); - } - - - /** - * Fire a change event to all listeners. - */ - protected void fireChangeEvent() { - // Copy the list before iterating to prevent concurrent modification exceptions. - EventListener[] list = listeners.toArray(new EventListener[0]); - for (EventListener l : list) { - if ( l instanceof StateChangeListener ) { - ((StateChangeListener)l).stateChanged(event); - } - } - } -} diff --git a/src/net/sf/openrocket/util/ArrayList.java b/src/net/sf/openrocket/util/ArrayList.java deleted file mode 100644 index a91deaa4..00000000 --- a/src/net/sf/openrocket/util/ArrayList.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Collection; - -/** - * An implementation of an ArrayList with a type-safe {@link #clone()} method. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ArrayList<E> extends java.util.ArrayList<E> { - - public ArrayList() { - super(); - } - - public ArrayList(Collection<? extends E> c) { - super(c); - } - - public ArrayList(int initialCapacity) { - super(initialCapacity); - } - - @SuppressWarnings("unchecked") - @Override - public ArrayList<E> clone() { - return (ArrayList<E>) super.clone(); - } - -} diff --git a/src/net/sf/openrocket/util/Base64.java b/src/net/sf/openrocket/util/Base64.java deleted file mode 100644 index 042d3671..00000000 --- a/src/net/sf/openrocket/util/Base64.java +++ /dev/null @@ -1,232 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Random; - -public class Base64 { - - public static final int DEFAULT_CHARS_PER_LINE = 72; - - private static final char[] ALPHABET = new char[] { - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', - 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', - 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', - 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' - }; - private static final char PAD = '='; - -// private static final byte[] REVERSE; -// static { -// REVERSE = new byte[128]; -// Arrays.fill(REVERSE, (byte)-1); -// for (int i=0; i<64; i++) { -// REVERSE[ALPHABET[i]] = (byte)i; -// } -// REVERSE['-'] = 62; -// REVERSE['_'] = 63; -// REVERSE[PAD] = 0; -// } - - private static final Map<Character,Integer> REVERSE = new HashMap<Character,Integer>(); - static { - for (int i=0; i<64; i++) { - REVERSE.put(ALPHABET[i], i); - } - REVERSE.put('-', 62); - REVERSE.put('_', 63); - REVERSE.put(PAD, 0); - } - - - public static String encode(byte[] data) { - return encode(data, DEFAULT_CHARS_PER_LINE); - } - - public static String encode(byte[] data, int maxColumn) { - StringBuilder builder = new StringBuilder(); - int column = 0; - - for (int position=0; position < data.length; position+=3) { - if (column+4 > maxColumn) { - builder.append('\n'); - column = 0; - } - builder.append(encodeGroup(data, position)); - column += 4; - } - builder.append('\n'); - return builder.toString(); - } - - - - - public static byte[] decode(String data) { - byte[] array = new byte[data.length()*3/4]; - char[] block = new char[4]; - int length = 0; - - for (int position=0; position < data.length(); ) { - int p; - for (p=0; p<4 && position < data.length(); position++) { - char c = data.charAt(position); - if (!Character.isWhitespace(c)) { - block[p] = c; - p++; - } - } - - if (p==0) - break; - if (p!=4) { - throw new IllegalArgumentException("Data ended when decoding Base64, p="+p); - } - - int l = decodeGroup(block, array, length); - length += l; - if (l < 3) - break; - } - return Arrays.copyOf(array, length); - } - - - //// Helper methods - - - /** - * Encode three bytes of data into four characters. - */ - private static char[] encodeGroup(byte[] data, int position) { - char[] c = new char[] { '=','=','=','=' }; - int b1=0, b2=0, b3=0; - int length = data.length - position; - - if (length == 0) - return c; - - if (length >= 1) { - b1 = ((int)data[position])&0xFF; - } - if (length >= 2) { - b2 = ((int)data[position+1])&0xFF; - } - if (length >= 3) { - b3 = ((int)data[position+2])&0xFF; - } - - c[0] = ALPHABET[b1>>2]; - c[1] = ALPHABET[(b1 & 3)<<4 | (b2>>4)]; - if (length == 1) - return c; - c[2] = ALPHABET[(b2 & 15)<<2 | (b3>>6)]; - if (length == 2) - return c; - c[3] = ALPHABET[b3 & 0x3f]; - return c; - } - - - /** - * Decode four chars from data into 0-3 bytes of data starting at position in array. - * @return the number of bytes decoded. - */ - private static int decodeGroup(char[] data, byte[] array, int position) { - int b1, b2, b3, b4; - - try { - b1 = REVERSE.get(data[0]); - b2 = REVERSE.get(data[1]); - b3 = REVERSE.get(data[2]); - b4 = REVERSE.get(data[3]); - } catch (NullPointerException e) { - // If auto-boxing fails - throw new IllegalArgumentException("Illegal characters in the sequence to be "+ - "decoded: "+Arrays.toString(data)); - } - - array[position] = (byte)((b1 << 2) | (b2 >> 4)); - array[position+1] = (byte)((b2 << 4) | (b3 >> 2)); - array[position+2] = (byte)((b3 << 6) | (b4)); - - // Check the amount of data decoded - if (data[0] == PAD) - return 0; - if (data[1] == PAD) { - throw new IllegalArgumentException("Illegal character padding in sequence to be "+ - "decoded: "+Arrays.toString(data)); - } - if (data[2] == PAD) - return 1; - if (data[3] == PAD) - return 2; - - return 3; - } - - - - public static void main(String[] arg) { - Random rnd = new Random(); - - for (int round=0; round < 1000; round++) { - int n = rnd.nextInt(1000); - n = 100000; - - byte[] array = new byte[n]; - rnd.nextBytes(array); - - String encoded = encode(array); - - System.out.println(encoded); - System.exit(0); -// for (int i=0; i<1000; i++) { -// int pos = rnd.nextInt(encoded.length()); -// String s1 = encoded.substring(0, pos); -// String s2 = encoded.substring(pos); -// switch (rnd.nextInt(15)) { -// case 0: -// encoded = s1 + " " + s2; -// break; -// case 1: -// encoded = s1 + "\u0009" + s2; -// break; -// case 2: -// encoded = s1 + "\n" + s2; -// break; -// case 3: -// encoded = s1 + "\u000B" + s2; -// break; -// case 4: -// encoded = s1 + "\r" + s2; -// break; -// case 5: -// encoded = s1 + "\u000C" + s2; -// break; -// case 6: -// encoded = s1 + "\u001C" + s2; -// break; -// } -// } - - byte[] decoded = null; - try { - decoded = decode(encoded); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - System.err.println("Bad data:\n"+encoded); - System.exit(1); - } - - if (!Arrays.equals(array, decoded)) { - System.err.println("Data differs! n="+n); - System.exit(1); - } - System.out.println("n="+n+" ok!"); - } - } - - -} diff --git a/src/net/sf/openrocket/util/BugException.java b/src/net/sf/openrocket/util/BugException.java deleted file mode 100644 index 8d113add..00000000 --- a/src/net/sf/openrocket/util/BugException.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.openrocket.util; - -/** - * Thrown when a bug is noticed. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class BugException extends FatalException { - - public BugException(String message) { - super("BUG: " + message); - } - - public BugException(Throwable cause) { - super("BUG: " + cause.getMessage(), cause); - } - - public BugException(String message, Throwable cause) { - super("BUG: " + message, cause); - } - -} diff --git a/src/net/sf/openrocket/util/BuildProperties.java b/src/net/sf/openrocket/util/BuildProperties.java deleted file mode 100644 index 46bc3c20..00000000 --- a/src/net/sf/openrocket/util/BuildProperties.java +++ /dev/null @@ -1,75 +0,0 @@ -package net.sf.openrocket.util; - -import java.io.IOException; -import java.io.InputStream; -import java.util.MissingResourceException; -import java.util.Properties; - -public class BuildProperties { - - private static final Properties PROPERTIES; - private static final String BUILD_VERSION; - private static final String BUILD_SOURCE; - private static final boolean DEFAULT_CHECK_UPDATES; - - /** - * Return the OpenRocket version number. - */ - public static String getVersion() { - return BUILD_VERSION; - } - - /** - * Return the OpenRocket build source (e.g. "default" or "Debian") - */ - public static String getBuildSource() { - return BUILD_SOURCE; - } - - public static boolean getDefaultCheckUpdates() { - return DEFAULT_CHECK_UPDATES; - } - - static { - try { - InputStream is = ClassLoader.getSystemResourceAsStream("build.properties"); - if (is == null) { - throw new MissingResourceException( - "build.properties not found, distribution built wrong" + - " classpath:" + System.getProperty("java.class.path"), - "build.properties", "build.version"); - } - - PROPERTIES = new Properties(); - PROPERTIES.load(is); - is.close(); - - String version = PROPERTIES.getProperty("build.version"); - if (version == null) { - throw new MissingResourceException( - "build.version not found in property file", - "build.properties", "build.version"); - } - BUILD_VERSION = version.trim(); - - BUILD_SOURCE = PROPERTIES.getProperty("build.source"); - if (BUILD_SOURCE == null) { - throw new MissingResourceException( - "build.source not found in property file", - "build.properties", "build.source"); - } - - String value = PROPERTIES.getProperty("build.checkupdates"); - if (value != null) - DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value); - else - DEFAULT_CHECK_UPDATES = true; - - } catch (IOException e) { - throw new MissingResourceException( - "Error reading build.properties", - "build.properties", "build.version"); - } - } - -} diff --git a/src/net/sf/openrocket/util/ChangeSource.java b/src/net/sf/openrocket/util/ChangeSource.java deleted file mode 100644 index f6669ef3..00000000 --- a/src/net/sf/openrocket/util/ChangeSource.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.EventListener; - -/** - * An interface defining an object firing ChangeEvents. Why isn't this included in the Java API?? - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface ChangeSource { - - public void addChangeListener(EventListener listener); - public void removeChangeListener(EventListener listener); - -} diff --git a/src/net/sf/openrocket/util/Chars.java b/src/net/sf/openrocket/util/Chars.java deleted file mode 100644 index eeba22bc..00000000 --- a/src/net/sf/openrocket/util/Chars.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.sf.openrocket.util; - -/** - * A class defining various non-ASCII characters for easier use. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Chars { - - /** The fraction 1/2 */ - public static final char FRAC12 = '\u00BD'; - /** The fraction 1/4 */ - public static final char FRAC14 = '\u00BC'; - /** The fraction 3/4 */ - public static final char FRAC34 = '\u00BE'; - - /** Degree sign */ - public static final char DEGREE = '\u00B0'; - - /** Squared, superscript 2 */ - public static final char SQUARED = '\u00B2'; - /** Cubed, superscript 3 */ - public static final char CUBED = '\u00B3'; - - /** Per mille sign */ - public static final char PERMILLE = '\u2030'; - - /** Middle dot, multiplication */ - public static final char DOT = '\u00B7'; - /** Multiplication sign, cross */ - public static final char TIMES = '\u00D7'; - - /** No-break space */ - public static final char NBSP = '\u00A0'; - /** Zero-width space */ - public static final char ZWSP = '\u200B'; - - /** Em dash */ - public static final char EMDASH = '\u2014'; - - /** Micro sign (Greek letter mu) */ - public static final char MICRO = '\u00B5'; - - /** Alpha */ - public static final char ALPHA = '\u03b1'; - /** Theta */ - public static final char THETA = '\u0398'; - - /** Copyright symbol */ - public static final char COPY = '\u00A9'; - /** A centered bullet */ - public static final char BULLET = '\u2022'; - - /** Left arrow (light) */ - public static final char LEFT_ARROW = '\u2190'; - /** Right arrow (light) */ - public static final char RIGHT_ARROW = '\u2192'; - -} diff --git a/src/net/sf/openrocket/util/Color.java b/src/net/sf/openrocket/util/Color.java deleted file mode 100644 index 5f56a173..00000000 --- a/src/net/sf/openrocket/util/Color.java +++ /dev/null @@ -1,58 +0,0 @@ -package net.sf.openrocket.util; - -public class Color { - - public static Color BLACK = new Color(255,255,255); - - private int red; - private int green; - private int blue; - private int alpha; - - public Color( int red, int green, int blue ) { - this.red = red; - this.green = green; - this.blue = blue; - this.alpha = 255; - } - - public Color( int red, int green, int blue, int alpha ) { - this.red = red; - this.green = green; - this.blue = blue; - this.alpha = alpha; - } - - public int getRed() { - return red; - } - - public void setRed(int red) { - this.red = red; - } - - public int getGreen() { - return green; - } - - public void setGreen(int green) { - this.green = green; - } - - public int getBlue() { - return blue; - } - - public void setBlue(int blue) { - this.blue = blue; - } - - public int getAlpha() { - return alpha; - } - - public void setAlpha(int alpha) { - this.alpha = alpha; - } - -} diff --git a/src/net/sf/openrocket/util/ComparablePair.java b/src/net/sf/openrocket/util/ComparablePair.java deleted file mode 100644 index 7b879683..00000000 --- a/src/net/sf/openrocket/util/ComparablePair.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.sf.openrocket.util; - -/** - * Sortable storage of a pair of objects. A list of these objects can be sorted according - * to the first object. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - * @param <U> the first object type, according to which comparisons are performed. - * @param <V> the second object type. - */ -public class ComparablePair<U extends Comparable<U>, V> extends Pair<U, V> - implements Comparable<ComparablePair<U, V>>{ - - public ComparablePair(U u, V v) { - super(u, v); - } - - - /** - * Compares the first objects. If either of the objects is <code>null</code> this - * method throws <code>NullPointerException</code>. - */ - @Override - public int compareTo(ComparablePair<U, V> other) { - return this.getU().compareTo(other.getU()); - } - -} diff --git a/src/net/sf/openrocket/util/ConcurrencyException.java b/src/net/sf/openrocket/util/ConcurrencyException.java deleted file mode 100644 index 252267c8..00000000 --- a/src/net/sf/openrocket/util/ConcurrencyException.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.sf.openrocket.util; - -/** - * An exception that indicates a concurrency bug in the software. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ConcurrencyException extends FatalException { - - public ConcurrencyException() { - super(); - } - - public ConcurrencyException(String message, Throwable cause) { - super(message, cause); - } - - public ConcurrencyException(String message) { - super(message); - } - - public ConcurrencyException(Throwable cause) { - super(cause); - } - -} diff --git a/src/net/sf/openrocket/util/ConfigurationException.java b/src/net/sf/openrocket/util/ConfigurationException.java deleted file mode 100644 index 692b3ac1..00000000 --- a/src/net/sf/openrocket/util/ConfigurationException.java +++ /dev/null @@ -1,26 +0,0 @@ -package net.sf.openrocket.util; - -/** - * An exception to be thrown when a fatal problem with the environment - * is encountered (for example some file cannot be found). - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class ConfigurationException extends FatalException { - - public ConfigurationException() { - } - - public ConfigurationException(String message) { - super(message); - } - - public ConfigurationException(Throwable cause) { - super(cause); - } - - public ConfigurationException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/util/Coordinate.java b/src/net/sf/openrocket/util/Coordinate.java deleted file mode 100644 index 3bc981aa..00000000 --- a/src/net/sf/openrocket/util/Coordinate.java +++ /dev/null @@ -1,320 +0,0 @@ -package net.sf.openrocket.util; - -import java.io.Serializable; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * An immutable class of weighted coordinates. The weights are non-negative. - * - * Can also be used as non-weighted coordinates with weight=0. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class Coordinate implements Serializable { - private static final LogHelper log = Application.getLogger(); - - //////// Debug section - /* - * Debugging info. If openrocket.debug.coordinatecount is defined, a line is - * printed every 1000000 instantiations (or as many as defined). - */ - private static final boolean COUNT_DEBUG; - private static final int COUNT_DIFF; - static { - String str = System.getProperty("openrocket.debug.coordinatecount"); - int diff = 0; - if (str == null) { - COUNT_DEBUG = false; - COUNT_DIFF = 0; - } else { - COUNT_DEBUG = true; - try { - diff = Integer.parseInt(str); - } catch (NumberFormatException ignore) { - } - if (diff < 1000) - diff = 1000000; - COUNT_DIFF = diff; - } - } - - private static int count = 0; - { - // Debug count - if (COUNT_DEBUG) { - synchronized (Coordinate.class) { - count++; - if ((count % COUNT_DIFF) == 0) { - log.debug("Coordinate instantiated " + count + " times."); - } - } - } - } - - //////// End debug section - - - - public static final Coordinate NUL = new Coordinate(0, 0, 0, 0); - public static final Coordinate NaN = new Coordinate(Double.NaN, Double.NaN, - Double.NaN, Double.NaN); - - public final double x, y, z; - public final double weight; - - - private double length = -1; /* Cached when calculated */ - - - - - public Coordinate() { - this(0, 0, 0, 0); - } - - public Coordinate(double x) { - this(x, 0, 0, 0); - } - - public Coordinate(double x, double y) { - this(x, y, 0, 0); - } - - public Coordinate(double x, double y, double z) { - this(x, y, z, 0); - } - - public Coordinate(double x, double y, double z, double w) { - this.x = x; - this.y = y; - this.z = z; - this.weight = w; - - } - - - public boolean isWeighted() { - return (weight != 0); - } - - /** - * Check whether any of the coordinate values is NaN. - * - * @return true if the x, y, z or weight is NaN - */ - public boolean isNaN() { - return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight); - } - - public Coordinate setX(double x) { - return new Coordinate(x, this.y, this.z, this.weight); - } - - public Coordinate setY(double y) { - return new Coordinate(this.x, y, this.z, this.weight); - } - - public Coordinate setZ(double z) { - return new Coordinate(this.x, this.y, z, this.weight); - } - - public Coordinate setWeight(double weight) { - return new Coordinate(this.x, this.y, this.z, weight); - } - - public Coordinate setXYZ(Coordinate c) { - return new Coordinate(c.x, c.y, c.z, this.weight); - } - - - /** - * Add the coordinate and weight of two coordinates. - * - * @param other the other <code>Coordinate</code> - * @return the sum of the coordinates - */ - public Coordinate add(Coordinate other) { - return new Coordinate(this.x + other.x, this.y + other.y, this.z + other.z, - this.weight + other.weight); - } - - public Coordinate add(double x, double y, double z) { - return new Coordinate(this.x + x, this.y + y, this.z + z, this.weight); - } - - public Coordinate add(double x, double y, double z, double weight) { - return new Coordinate(this.x + x, this.y + y, this.z + z, this.weight + weight); - } - - /** - * Subtract a Coordinate from this Coordinate. The weight of the resulting Coordinate - * is the same as of this Coordinate, the weight of the argument is ignored. - * - * @param other Coordinate to subtract from this. - * @return The result - */ - public Coordinate sub(Coordinate other) { - return new Coordinate(this.x - other.x, this.y - other.y, this.z - other.z, this.weight); - } - - /** - * Subtract the specified values from this Coordinate. The weight of the result - * is the same as the weight of this Coordinate. - * - * @param x x value to subtract - * @param y y value to subtract - * @param z z value to subtract - * @return the result. - */ - public Coordinate sub(double x, double y, double z) { - return new Coordinate(this.x - x, this.y - y, this.z - z, this.weight); - } - - - /** - * Multiply the <code>Coordinate</code> with a scalar. All coordinates and the - * weight are multiplied by the given scalar. - - * @param m Factor to multiply by. - * @return The product. - */ - public Coordinate multiply(double m) { - return new Coordinate(this.x * m, this.y * m, this.z * m, this.weight * m); - } - - /** - * Dot product of two Coordinates, taken as vectors. Equal to - * x1*x2+y1*y2+z1*z2 - * @param other Coordinate to take product with. - * @return The dot product. - */ - public double dot(Coordinate other) { - return this.x * other.x + this.y * other.y + this.z * other.z; - } - - /** - * Dot product of two Coordinates. - */ - public static double dot(Coordinate v1, Coordinate v2) { - return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; - } - - /** - * Distance from the origin to the Coordinate. - */ - public double length() { - if (length < 0) { - length = MathUtil.safeSqrt(x * x + y * y + z * z); - } - return length; - } - - /** - * Square of the distance from the origin to the Coordinate. - */ - public double length2() { - return x * x + y * y + z * z; - } - - - /** - * Return the largest of the absolute values of the coordinates. This can be - * used as a norm of the vector that is faster to calculate than the - * 2-norm. - * - * @return the largest absolute value of (x,y,z) - */ - public double max() { - return MathUtil.max(Math.abs(x), Math.abs(y), Math.abs(z)); - } - - - /** - * Returns a new coordinate which has the same direction from the origin as this - * coordinate but is at a distance of one. If this coordinate is the origin, - * this method throws an <code>IllegalStateException</code>. The weight of the - * coordinate is unchanged. - * - * @return the coordinate normalized to distance one of the origin. - * @throws IllegalStateException if this coordinate is the origin. - */ - public Coordinate normalize() { - double l = length(); - if (l < 0.0000001) { - throw new IllegalStateException("Cannot normalize zero coordinate"); - } - return new Coordinate(x / l, y / l, z / l, weight); - } - - - - - /** - * Weighted average of two coordinates. If either of the weights are positive, - * the result is the weighted average of the coordinates and the weight is the sum - * of the original weights. If the sum of the weights is zero (and especially if - * both of the weights are zero), the result is the unweighted average of the - * coordinates with weight zero. - * <p> - * If <code>other</code> is <code>null</code> then this <code>Coordinate</code> is - * returned. - */ - public Coordinate average(Coordinate other) { - double x, y, z, w; - - if (other == null) - return this; - - w = this.weight + other.weight; - if (Math.abs(w) < MathUtil.pow2(MathUtil.EPSILON)) { - x = (this.x + other.x) / 2; - y = (this.y + other.y) / 2; - z = (this.z + other.z) / 2; - w = 0; - } else { - x = (this.x * this.weight + other.x * other.weight) / w; - y = (this.y * this.weight + other.y * other.weight) / w; - z = (this.z * this.weight + other.z * other.weight) / w; - } - return new Coordinate(x, y, z, w); - } - - - /** - * Tests whether the coordinates are the equal. - * - * @param other Coordinate to compare to. - * @return true if the coordinates are equal (x, y, z and weight) - */ - @Override - public boolean equals(Object other) { - if (!(other instanceof Coordinate)) - return false; - - final Coordinate c = (Coordinate) other; - return (MathUtil.equals(this.x, c.x) && - MathUtil.equals(this.y, c.y) && - MathUtil.equals(this.z, c.z) && MathUtil.equals(this.weight, c.weight)); - } - - /** - * Hash code method compatible with {@link #equals(Object)}. - */ - @Override - public int hashCode() { - return (int) ((x + y + z) * 100000); - } - - - @Override - public String toString() { - if (isWeighted()) - return String.format("(%.3f,%.3f,%.3f,w=%.3f)", x, y, z, weight); - else - return String.format("(%.3f,%.3f,%.3f)", x, y, z); - } - - -} diff --git a/src/net/sf/openrocket/util/FatalException.java b/src/net/sf/openrocket/util/FatalException.java deleted file mode 100644 index b50feef1..00000000 --- a/src/net/sf/openrocket/util/FatalException.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.sf.openrocket.util; - -/** - * A superclass for all types of fatal error conditions. This class is - * abstract so only subclasses can be used. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - * @see BugException - * @see ConfigurationException - */ -public abstract class FatalException extends RuntimeException { - - public FatalException() { - } - - public FatalException(String message) { - super(message); - } - - public FatalException(Throwable cause) { - super(cause); - } - - public FatalException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/net/sf/openrocket/util/GeodeticComputationStrategy.java b/src/net/sf/openrocket/util/GeodeticComputationStrategy.java deleted file mode 100644 index f8a37a09..00000000 --- a/src/net/sf/openrocket/util/GeodeticComputationStrategy.java +++ /dev/null @@ -1,289 +0,0 @@ -package net.sf.openrocket.util; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -/** - * A strategy that performs computations on WorldCoordinates. - * <p> - * The directions of the coordinate is: - * positive X = EAST - * positive Y = NORTH - * positive Z = UPWARDS - */ -public enum GeodeticComputationStrategy { - - - /** - * Perform computations using a flat Earth approximation. addCoordinate computes the - * location using a direct meters-per-degree scaling and getCoriolisAcceleration always - * returns NUL. - */ - FLAT { - private static final double METERS_PER_DEGREE_LATITUDE = 111325; // "standard figure" - private static final double METERS_PER_DEGREE_LONGITUDE_EQUATOR = 111050; - - - @Override - public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { - - double metersPerDegreeLongitude = METERS_PER_DEGREE_LONGITUDE_EQUATOR * Math.cos(location.getLatitudeRad()); - // Limit to 1 meter per degree near poles - metersPerDegreeLongitude = MathUtil.max(metersPerDegreeLongitude, 1); - - double newLat = location.getLatitudeDeg() + delta.y / METERS_PER_DEGREE_LATITUDE; - double newLon = location.getLongitudeDeg() + delta.x / metersPerDegreeLongitude; - double newAlt = location.getAltitude() + delta.z; - - return new WorldCoordinate(newLat, newLon, newAlt); - } - - @Override - public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { - return Coordinate.NUL; - } - }, - - /** - * Perform geodetic computations with a spherical Earch approximation. - */ - SPHERICAL { - - @Override - public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { - double newAlt = location.getAltitude() + delta.z; - - // bearing (in radians, clockwise from north); - // d/R is the angular distance (in radians), where d is the distance traveled and R is the earth's radius - double d = MathUtil.hypot(delta.x, delta.y); - - // Check for zero movement before computing bearing - if (MathUtil.equals(d, 0)) { - return new WorldCoordinate(location.getLatitudeDeg(), location.getLongitudeDeg(), newAlt); - } - - double bearing = Math.atan2(delta.x, delta.y); - - // Calculate the new lat and lon - double newLat, newLon; - double sinLat = Math.sin(location.getLatitudeRad()); - double cosLat = Math.cos(location.getLatitudeRad()); - double sinDR = Math.sin(d / WorldCoordinate.REARTH); - double cosDR = Math.cos(d / WorldCoordinate.REARTH); - - newLat = Math.asin(sinLat * cosDR + cosLat * sinDR * Math.cos(bearing)); - newLon = location.getLongitudeRad() + Math.atan2(Math.sin(bearing) * sinDR * cosLat, cosDR - sinLat * Math.sin(newLat)); - - if (Double.isNaN(newLat) || Double.isNaN(newLon)) { - throw new BugException("addCoordinate resulted in NaN location: location=" + location + " delta=" + delta - + " newLat=" + newLat + " newLon=" + newLon); - } - - return new WorldCoordinate(Math.toDegrees(newLat), Math.toDegrees(newLon), newAlt); - } - - @Override - public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { - return computeCoriolisAcceleration(location, velocity); - } - - }, - - /** - * Perform geodetic computations on a WGS84 reference ellipsoid using Vincenty Direct Solution. - */ - WGS84 { - - @Override - public WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta) { - double newAlt = location.getAltitude() + delta.z; - - // bearing (in radians, clockwise from north); - // d/R is the angular distance (in radians), where d is the distance traveled and R is the earth's radius - double d = MathUtil.hypot(delta.x, delta.y); - - // Check for zero movement before computing bearing - if (MathUtil.equals(d, 0)) { - return new WorldCoordinate(location.getLatitudeDeg(), location.getLongitudeDeg(), newAlt); - } - - double bearing = Math.atan(delta.x / delta.y); - if (delta.y < 0) - bearing = bearing + Math.PI; - - // Calculate the new lat and lon - double newLat, newLon; - double ret[] = dirct1(location.getLatitudeRad(), location.getLongitudeRad(), bearing, d, 6378137, 1.0 / 298.25722210088); - newLat = ret[0]; - newLon = ret[1]; - - if (Double.isNaN(newLat) || Double.isNaN(newLon)) { - throw new BugException("addCoordinate resulted in NaN location: location=" + location + " delta=" + delta - + " newLat=" + newLat + " newLon=" + newLon); - } - - return new WorldCoordinate(Math.toDegrees(newLat), Math.toDegrees(newLon), newAlt); - } - - @Override - public Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity) { - return computeCoriolisAcceleration(location, velocity); - } - }; - - - private static final Translator trans = Application.getTranslator(); - - private static final double PRECISION_LIMIT = 0.5e-13; - - - /** - * Return the name of this geodetic computation method. - */ - public String getName() { - return trans.get(name().toLowerCase() + ".name"); - } - - /** - * Return a description of the geodetic computation methods. - */ - public String getDescription() { - return trans.get(name().toLowerCase() + ".desc"); - } - - @Override - public String toString() { - return getName(); - } - - - /** - * Add a cartesian movement coordinate to a WorldCoordinate. - */ - public abstract WorldCoordinate addCoordinate(WorldCoordinate location, Coordinate delta); - - - /** - * Compute the coriolis acceleration at a specified WorldCoordinate and velocity. - */ - public abstract Coordinate getCoriolisAcceleration(WorldCoordinate location, Coordinate velocity); - - - - - - private static Coordinate computeCoriolisAcceleration(WorldCoordinate latlon, Coordinate velocity) { - - double sinlat = Math.sin(latlon.getLatitudeRad()); - double coslat = Math.cos(latlon.getLatitudeRad()); - - double v_n = velocity.y; - double v_e = -1 * velocity.x; - double v_u = velocity.z; - - // Not exactly sure why I have to reverse the x direction, but this gives the precession in the - // correct direction (e.g, flying north in northern hemisphere should cause defection to the east (+ve x)) - // All the directions are very confusing because they are tied to the wind direction (to/from?), in which - // +ve x or east according to WorldCoordinate is what everything is relative to. - // The directions of everything need so thought, ideally the wind direction and launch rod should be - // able to be set independently and in terms of bearing with north == +ve y. - - Coordinate coriolis = new Coordinate(2.0 * WorldCoordinate.EROT * (v_n * sinlat - v_u * coslat), - 2.0 * WorldCoordinate.EROT * (-1.0 * v_e * sinlat), - 2.0 * WorldCoordinate.EROT * (v_e * coslat) - ); - return coriolis; - } - - - - // ******************************************************************** // - // The Vincenty Direct Solution. - // Code from GeoConstants.java, Ian Cameron Smith, GPL - // ******************************************************************** // - - /** - * Solution of the geodetic direct problem after T. Vincenty. - * Modified Rainsford's method with Helmert's elliptical terms. - * Effective in any azimuth and at any distance short of antipodal. - * - * Programmed for the CDC-6600 by lcdr L. Pfeifer, NGS Rockville MD, - * 20 Feb 1975. - * - * @param glat1 The latitude of the starting point, in radians, - * positive north. - * @param glon1 The latitude of the starting point, in radians, - * positive east. - * @param azimuth The azimuth to the desired location, in radians - * clockwise from north. - * @param dist The distance to the desired location, in meters. - * @param axis The semi-major axis of the reference ellipsoid, - * in meters. - * @param flat The flattening of the reference ellipsoid. - * @return An array containing the latitude and longitude - * of the desired point, in radians, and the - * azimuth back from that point to the starting - * point, in radians clockwise from north. - */ - private static double[] dirct1(double glat1, double glon1, - double azimuth, double dist, - double axis, double flat) { - double r = 1.0 - flat; - - double tu = r * Math.sin(glat1) / Math.cos(glat1); - - double sf = Math.sin(azimuth); - double cf = Math.cos(azimuth); - - double baz = 0.0; - - if (cf != 0.0) - baz = Math.atan2(tu, cf) * 2.0; - - double cu = 1.0 / Math.sqrt(tu * tu + 1.0); - double su = tu * cu; - double sa = cu * sf; - double c2a = -sa * sa + 1.0; - - double x = Math.sqrt((1.0 / r / r - 1.0) * c2a + 1.0) + 1.0; - x = (x - 2.0) / x; - double c = 1.0 - x; - c = (x * x / 4.0 + 1) / c; - double d = (0.375 * x * x - 1.0) * x; - tu = dist / r / axis / c; - double y = tu; - - double sy, cy, cz, e; - do { - sy = Math.sin(y); - cy = Math.cos(y); - cz = Math.cos(baz + y); - e = cz * cz * 2.0 - 1.0; - - c = y; - x = e * cy; - y = e + e - 1.0; - y = (((sy * sy * 4.0 - 3.0) * y * cz * d / 6.0 + x) * - d / 4.0 - cz) * sy * d + tu; - } while (Math.abs(y - c) > PRECISION_LIMIT); - - baz = cu * cy * cf - su * sy; - c = r * Math.sqrt(sa * sa + baz * baz); - d = su * cy + cu * sy * cf; - double glat2 = Math.atan2(d, c); - c = cu * cy - su * sy * cf; - x = Math.atan2(sy * sf, c); - c = ((-3.0 * c2a + 4.0) * flat + 4.0) * c2a * flat / 16.0; - d = ((e * cy * c + cz) * sy * c + y) * sa; - double glon2 = glon1 + x - (1.0 - c) * d * flat; - baz = Math.atan2(sa, baz) + Math.PI; - - double[] ret = new double[3]; - ret[0] = glat2; - ret[1] = glon2; - ret[2] = baz; - return ret; - } - - -} diff --git a/src/net/sf/openrocket/util/Inertia.java b/src/net/sf/openrocket/util/Inertia.java deleted file mode 100644 index f7d65c41..00000000 --- a/src/net/sf/openrocket/util/Inertia.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.sf.openrocket.util; - -import static net.sf.openrocket.util.MathUtil.pow2; -public final class Inertia { - - private Inertia() { - } - - - /** - * Return the rotational unit moment of inertia of a solid cylinder. - * - * @param radius the radius of the cylinder. - */ - public static double filledCylinderRotational(double radius) { - return pow2(radius) / 2; - } - - /** - * Return the longitudinal unit moment of inertia of a solid cylinder, - * relative to the midpoint lengthwise. - * - * @param radius the radius of the cylinder. - * @param length the total length of the cylinder (reference at midpoint) - */ - public static double filledCylinderLongitudinal(double radius, double length) { - return (3*pow2(radius) + pow2(length))/12; - } - - - /** - * Return the unit moment of inertia that is shifted from the CG of an object - * by a specified distance. The rotation axis are parallel. - * - * @param cgInertia the unit moment of inertia through the CG of the object - * @param distance the distance to shift the rotation axis - */ - public static double shift(double cgInertia, double distance) { - return cgInertia + pow2(distance); - } - -} diff --git a/src/net/sf/openrocket/util/Invalidatable.java b/src/net/sf/openrocket/util/Invalidatable.java deleted file mode 100644 index 94499894..00000000 --- a/src/net/sf/openrocket/util/Invalidatable.java +++ /dev/null @@ -1,17 +0,0 @@ -package net.sf.openrocket.util; - -/** - * An object that can be invalidated (in some sense of the word). After calling the - * invalidate method the object should not be used any more and it may enforce - * disusage for certain methods. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface Invalidatable { - - /** - * Invalidate this object. - */ - public void invalidate(); - -} diff --git a/src/net/sf/openrocket/util/Invalidator.java b/src/net/sf/openrocket/util/Invalidator.java deleted file mode 100644 index 9e40c102..00000000 --- a/src/net/sf/openrocket/util/Invalidator.java +++ /dev/null @@ -1,73 +0,0 @@ -package net.sf.openrocket.util; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.startup.Application; - -/** - * A class that performs object invalidation functions. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Invalidator implements Invalidatable { - private static final boolean USE_CHECKS = Application.useSafetyChecks(); - - private static final LogHelper log = Application.getLogger(); - - private final Object monitorable; - private TraceException invalidated = null; - - - /** - * Sole constructor. The parameter is used when writing error messages, and - * is not referenced otherwise. - * - * @param monitorable the object this invalidator is monitoring (may be null or a descriptive string) - */ - public Invalidator(Object monitorable) { - this.monitorable = monitorable; - } - - - /** - * Check whether the object has been invalidated. Depending on the parameter either - * a BugException is thrown or a warning about the object access is logged. - * - * @param throwException whether to throw an exception or log a warning. - * @return <code>true</code> when the object has not been invalidated, <code>false</code> if it has - * @throws BugException if the object has been invalidated and <code>throwException</code> is true. - */ - public boolean check(boolean throwException) { - if (invalidated != null) { - if (throwException) { - throw new BugException(monitorable + ": This object has been invalidated", invalidated); - } else { - log.warn(1, monitorable + ": This object has been invalidated", - new TraceException("Usage was attempted here", invalidated)); - } - return false; - } - return true; - } - - - /** - * Check whether the object has been invalidated. - * @return <code>true</code> if the object has been invalidated, <code>false</code> otherwise. - */ - public boolean isInvalidated() { - return invalidated != null; - } - - - @Override - public void invalidate() { - if (USE_CHECKS) { - if (invalidated != null) { - log.warn(1, monitorable + ": This object has already been invalidated, ignoring", invalidated); - } - invalidated = new TraceException("Invalidation occurred here"); - } - } - -} diff --git a/src/net/sf/openrocket/util/JarUtil.java b/src/net/sf/openrocket/util/JarUtil.java deleted file mode 100644 index b73438ed..00000000 --- a/src/net/sf/openrocket/util/JarUtil.java +++ /dev/null @@ -1,54 +0,0 @@ -package net.sf.openrocket.util; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.security.CodeSource; - -import net.sf.openrocket.database.Database; - -public class JarUtil { - - /** - * Return the a File object pointing to the JAR file that this class belongs to, - * or <code>null</code> if it cannot be found. - * - * @return a File object of the current Java archive, or <code>null</code> - */ - public static File getCurrentJarFile() { - // Find the jar file this class is contained in - URL jarUrl = null; - CodeSource codeSource = Database.class.getProtectionDomain().getCodeSource(); - if (codeSource != null) - jarUrl = codeSource.getLocation(); - - if (jarUrl == null) { - return null; - } - - File file = urlToFile(jarUrl); - if (file.isFile()) - return file; - return null; - } - - - - public static File urlToFile(URL url) { - URI uri; - try { - uri = url.toURI(); - } catch (URISyntaxException e) { - try { - uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), - url.getPort(), url.getPath(), url.getQuery(), url.getRef()); - } catch (URISyntaxException e1) { - throw new IllegalArgumentException("Broken URL: " + url); - } - } - return new File(uri); - } - - -} diff --git a/src/net/sf/openrocket/util/LimitedInputStream.java b/src/net/sf/openrocket/util/LimitedInputStream.java deleted file mode 100644 index 4e264fa1..00000000 --- a/src/net/sf/openrocket/util/LimitedInputStream.java +++ /dev/null @@ -1,83 +0,0 @@ -package net.sf.openrocket.util; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * A filtering InputStream that limits the number of bytes that can be - * read from a stream. This can be used to enforce security, so that overlong - * input is ignored. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class LimitedInputStream extends FilterInputStream { - - private int remaining; - - public LimitedInputStream(InputStream is, int limit) { - super(is); - this.remaining = limit; - } - - - @Override - public int available() throws IOException { - int available = super.available(); - return Math.min(available, remaining); - } - - - @Override - public int read(byte[] b, int off, int len) throws IOException { - if (remaining <= 0) - return -1; - - int result = super.read(b, off, Math.min(len, remaining)); - if (result >= 0) - remaining -= result; - return result; - } - - - @Override - public long skip(long n) throws IOException { - if (n > remaining) - n = remaining; - long result = super.skip(n); - remaining -= result; - return result; - } - - - @Override - public int read() throws IOException { - if (remaining <= 0) - return -1; - - int result = super.read(); - if (result >= 0) - remaining--; - return result; - } - - - - // Disable mark support - - @Override - public void mark(int readlimit) { - - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public synchronized void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } - -} diff --git a/src/net/sf/openrocket/util/LineStyle.java b/src/net/sf/openrocket/util/LineStyle.java deleted file mode 100644 index fc2b4fdc..00000000 --- a/src/net/sf/openrocket/util/LineStyle.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Arrays; - -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.startup.Application; - -/** - * An enumeration of line styles. The line styles are defined by an array of - * floats suitable for <code>BasicStroke</code>. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public enum LineStyle { - - - //// Solid - SOLID("LineStyle.Solid", new float[] { 10f, 0f }), - //// Dashed - DASHED("LineStyle.Dashed", new float[] { 6f, 4f }), - //// Dotted - DOTTED("LineStyle.Dotted", new float[] { 2f, 3f }), - //// Dash-dotted - DASHDOT("LineStyle.Dash-dotted", new float[] { 8f, 3f, 2f, 3f }); - - private static final Translator trans = Application.getTranslator(); - private final String name; - private final float[] dashes; - - LineStyle(String name, float[] dashes) { - this.name = name; - this.dashes = dashes; - } - - public float[] getDashes() { - return Arrays.copyOf(dashes, dashes.length); - } - - @Override - public String toString() { - return trans.get(name); - } -} \ No newline at end of file diff --git a/src/net/sf/openrocket/util/LinearInterpolator.java b/src/net/sf/openrocket/util/LinearInterpolator.java deleted file mode 100644 index e94a3976..00000000 --- a/src/net/sf/openrocket/util/LinearInterpolator.java +++ /dev/null @@ -1,128 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Iterator; -import java.util.Map; -import java.util.TreeMap; - -public class LinearInterpolator implements Cloneable { - - private TreeMap<Double, Double> sortMap = new TreeMap<Double,Double>(); - - /** - * Construct a <code>LinearInterpolator</code> with no points. Some points must be - * added using {@link #addPoints(double[], double[])} before using the interpolator. - */ - public LinearInterpolator() { - } - - /** - * Construct a <code>LinearInterpolator</code> with the given points. - * - * @param x the x-coordinates of the points. - * @param y the y-coordinates of the points. - * @throws IllegalArgumentException if the lengths of <code>x</code> and <code>y</code> - * are not equal. - * @see #addPoints(double[], double[]) - */ - public LinearInterpolator(double[] x, double[] y) { - addPoints(x,y); - } - - - /** - * Add the point to the linear interpolation. - * - * @param x the x-coordinate of the point. - * @param y the y-coordinate of the point. - */ - public void addPoint(double x, double y) { - sortMap.put(x, y); - } - - /** - * Add the points to the linear interpolation. - * - * @param x the x-coordinates of the points. - * @param y the y-coordinates of the points. - * @throws IllegalArgumentException if the lengths of <code>x</code> and <code>y</code> - * are not equal. - */ - public void addPoints(double[] x, double[] y) { - if (x.length != y.length) { - throw new IllegalArgumentException("Array lengths do not match, x="+x.length + - " y="+y.length); - } - for (int i=0; i < x.length; i++) { - sortMap.put(x[i],y[i]); - } - } - - - - public double getValue(double x) { - Map.Entry<Double,Double> e1, e2; - double x1, x2; - double y1, y2; - - e1 = sortMap.floorEntry(x); - - if (e1 == null) { - // x smaller than any value in the set - e1 = sortMap.firstEntry(); - if (e1 == null) { - throw new IllegalStateException("No points added yet to the interpolator."); - } - return e1.getValue(); - } - - x1 = e1.getKey(); - e2 = sortMap.higherEntry(x1); - - if (e2 == null) { - // x larger than any value in the set - return e1.getValue(); - } - - x2 = e2.getKey(); - y1 = e1.getValue(); - y2 = e2.getValue(); - - return (x - x1)/(x2-x1) * (y2-y1) + y1; - } - - - public double[] getXPoints() { - double[] x = new double[sortMap.size()]; - Iterator<Double> iter = sortMap.keySet().iterator(); - for (int i=0; iter.hasNext(); i++) { - x[i] = iter.next(); - } - return x; - } - - - @SuppressWarnings("unchecked") - @Override - public LinearInterpolator clone() { - try { - LinearInterpolator other = (LinearInterpolator)super.clone(); - other.sortMap = (TreeMap<Double,Double>)this.sortMap.clone(); - return other; - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException?!",e); - } - } - - - public static void main(String[] args) { - LinearInterpolator interpolator = new LinearInterpolator( - new double[] {1, 1.5, 2, 4, 5}, - new double[] {0, 1, 0, 2, 2} - ); - - for (double x=0; x < 6; x+=0.1) { - System.out.printf("%.1f: %.2f\n", x, interpolator.getValue(x)); - } - } - -} diff --git a/src/net/sf/openrocket/util/ListenerList.java b/src/net/sf/openrocket/util/ListenerList.java deleted file mode 100644 index b4f4c02b..00000000 --- a/src/net/sf/openrocket/util/ListenerList.java +++ /dev/null @@ -1,264 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Iterator; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.startup.Application; - -/** - * A list of listeners of a specific type. This class contains various utility, - * safety and debugging methods for handling listeners. - * <p> - * Note that unlike normal listener implementations, this list does NOT allow the - * exact same listener (equality using ==) twice. While adding a listener twice to - * a event source would in principle be valid, in practice it's most likely a bug. - * For example the Swing implementation Sun JRE contains such bugs. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - * @param <T> the type of the listeners. - */ -public class ListenerList<T> implements Invalidatable, Iterable<T> { - private static final LogHelper log = Application.getLogger(); - - private final ArrayList<ListenerData<T>> listeners = new ArrayList<ListenerData<T>>(); - private final TraceException instantiationLocation; - - private TraceException invalidated = null; - - - /** - * Sole contructor. - */ - public ListenerList() { - this.instantiationLocation = new TraceException(1, 1); - } - - - /** - * Adds the specified listener to this list. The listener is not added if it - * already is in the list (checked by the equality operator ==). This method throws - * a BugException if {@link #invalidate()} has been called. - * - * @param listener the listener to add. - * @return whether the listeners was actually added to the list. - * @throws BugException if this listener list has been invalidated. - */ - public boolean addListener(T listener) { - checkState(true); - - ListenerData<T> data = new ListenerData<T>(listener); - if (listeners.contains(data)) { - log.warn(1, "Attempting to add duplicate listener " + listener); - return false; - } - listeners.add(data); - return true; - } - - - /** - * Remove the specified listener from the list. The listener is removed based on the - * quality operator ==, not by the equals() method. - * - * @param listener the listener to remove. - * @return whether the listener was actually removed. - */ - public boolean removeListener(T listener) { - checkState(false); - - Iterator<ListenerData<T>> iterator = listeners.iterator(); - while (iterator.hasNext()) { - if (iterator.next().listener == listener) { - iterator.remove(); - log.verbose(1, "Removing listener " + listener); - return true; - } - } - log.info(1, "Attempting to remove non-existant listener " + listener); - return false; - } - - - /** - * Return the number of listeners in this list. - */ - public int getListenerCount() { - return listeners.size(); - } - - - /** - * Return an iterator that iterates of the listeners. This iterator is backed by - * a copy of the iterator list, so {@link #addListener(Object)} and {@link #removeListener(Object)} - * may be called while iterating the list without effect on the iteration. The returned - * iterator does not support the {@link Iterator#remove()} method. - */ - @Override - public Iterator<T> iterator() { - checkState(false); - return new ListenerDataIterator(); - } - - /** - * Return the instantiation location of this listener list. - * @return the location where this listener list was instantiated. - */ - public TraceException getInstantiationLocation() { - return instantiationLocation; - } - - - /** - * Invalidate this listener list. Invalidation removes all listeners from the list. - * After invalidation {@link #addListener(Object)} will throw an exception, the other - * methods produce a warning log message. - */ - @Override - public void invalidate() { - this.invalidated = new TraceException("Invalidation occurred at this point"); - if (!listeners.isEmpty()) { - log.info("Invalidating " + this + " while still having listeners " + listeners); - } - listeners.clear(); - } - - - public boolean isInvalidated() { - return this.invalidated != null; - } - - - private void checkState(boolean error) { - if (this.invalidated != null) { - if (error) { - throw new BugException(this + ": this ListenerList has been invalidated", invalidated); - } else { - log.warn(1, this + ": this ListenerList has been invalidated", - new TraceException("ListenerList was attempted to be used here", invalidated)); - } - } - } - - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("ListenerList["); - - if (this.invalidated != null) { - sb.append("INVALIDATED]"); - return sb.toString(); - } - - if (listeners.isEmpty()) { - sb.append("empty"); - } else { - boolean first = true; - for (ListenerData<T> l : listeners) { - if (!first) { - sb.append("; "); - } - first = false; - sb.append(l); - } - } - sb.append("]"); - return sb.toString(); - } - - - /** - * A class containing data about a listener. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - * @param <T> the listener type - */ - public static class ListenerData<T> { - private final T listener; - private final long addTimestamp; - private final TraceException addLocation; - private long accessTimestamp; - - /** - * Sole constructor. - */ - private ListenerData(T listener) { - if (listener == null) { - throw new NullPointerException("listener is null"); - } - this.listener = listener; - this.addTimestamp = System.currentTimeMillis(); - this.accessTimestamp = this.addTimestamp; - this.addLocation = new TraceException("Listener " + listener + " add position"); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (!(obj instanceof ListenerData)) - return false; - ListenerData<?> other = (ListenerData<?>) obj; - return this.listener == other.listener; - } - - @Override - public int hashCode() { - return listener.hashCode(); - } - - /** - * Return the listener. - */ - public T getListener() { - return listener; - } - - /** - * Return the millisecond timestamp when this listener was added to the - * listener list. - */ - public long getAddTimestamp() { - return addTimestamp; - } - - /** - * Return the location where this listener was added to the listener list. - */ - public TraceException getAddLocation() { - return addLocation; - } - - /** - * Return the millisecond timestamp when this listener was last accessed through - * the listener list iterator. - */ - public long getAccessTimestamp() { - return accessTimestamp; - } - } - - - private class ListenerDataIterator implements Iterator<T> { - private final Iterator<ListenerData<T>> iterator = listeners.clone().iterator(); - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public T next() { - ListenerData<T> data = iterator.next(); - data.accessTimestamp = System.currentTimeMillis(); - return data.listener; - } - - @Override - public void remove() { - throw new UnsupportedOperationException("Remove not supported"); - } - } - -} diff --git a/src/net/sf/openrocket/util/MathUtil.java b/src/net/sf/openrocket/util/MathUtil.java deleted file mode 100644 index dc2e105d..00000000 --- a/src/net/sf/openrocket/util/MathUtil.java +++ /dev/null @@ -1,314 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -public class MathUtil { - private static final LogHelper log = Application.getLogger(); - - public static final double EPSILON = 0.00000001; // 10mm^3 in m^3 - - /** - * The square of x (x^2). On Sun's JRE using this method is as fast as typing x*x. - * @param x x - * @return x^2 - */ - public static double pow2(double x) { - return x * x; - } - - /** - * The cube of x (x^3). - * @param x x - * @return x^3 - */ - public static double pow3(double x) { - return x * x * x; - } - - public static double pow4(double x) { - return (x * x) * (x * x); - } - - /** - * Clamps the value x to the range min - max. - * @param x Original value. - * @param min Minimum value to return. - * @param max Maximum value to return. - * @return The clamped value. - */ - public static double clamp(double x, double min, double max) { - if (x < min) - return min; - if (x > max) - return max; - return x; - } - - public static float clamp(float x, float min, float max) { - if (x < min) - return min; - if (x > max) - return max; - return x; - } - - public static int clamp(int x, int min, int max) { - if (x < min) - return min; - if (x > max) - return max; - return x; - } - - - /** - * Maps a value from one value range to another. - * - * @param value the value to map. - * @param fromMin the minimum of the starting range. - * @param fromMax the maximum of the starting range. - * @param toMin the minimum of the destination range. - * @param toMax the maximum of the destination range. - * @return the mapped value. - * @throws IllegalArgumentException if fromMin == fromMax, but toMin != toMax. - */ - public static double map(double value, double fromMin, double fromMax, - double toMin, double toMax) { - if (equals(toMin, toMax)) - return toMin; - if (equals(fromMin, fromMax)) { - throw new IllegalArgumentException("from range is singular and to range is not: " + - "value=" + value + " fromMin=" + fromMin + " fromMax=" + fromMax + - "toMin=" + toMin + " toMax=" + toMax); - } - return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin; - } - - - /** - * Maps a coordinate from one value range to another. - * - * @param value the value to map. - * @param fromMin the minimum of the starting range. - * @param fromMax the maximum of the starting range. - * @param toMin the minimum coordinate of the destination; - * @param toMax the maximum coordinate of the destination; - * @return the mapped value. - * @throws IllegalArgumentException if fromMin == fromMax, but toMin != toMax. - */ - public static Coordinate map(double value, double fromMin, double fromMax, - Coordinate toMin, Coordinate toMax) { - if (toMin.equals(toMax)) - return toMin; - if (equals(fromMin, fromMax)) { - throw new IllegalArgumentException("from range is singular and to range is not: " + - "value=" + value + " fromMin=" + fromMin + " fromMax=" + fromMax + - "toMin=" + toMin + " toMax=" + toMax); - } - double a = (value - fromMin) / (fromMax - fromMin); - return toMax.multiply(a).add(toMin.multiply(1 - a)); - } - - - /** - * Compute the minimum of two values. This is performed by direct comparison. - * However, if one of the values is NaN and the other is not, the non-NaN value is - * returned. - */ - public static double min(double x, double y) { - if (Double.isNaN(y)) - return x; - return (x < y) ? x : y; - } - - /** - * Compute the maximum of two values. This is performed by direct comparison. - * However, if one of the values is NaN and the other is not, the non-NaN value is - * returned. - */ - public static double max(double x, double y) { - if (Double.isNaN(x)) - return y; - return (x < y) ? y : x; - } - - /** - * Compute the minimum of three values. This is performed by direct comparison. - * However, if one of the values is NaN and the other is not, the non-NaN value is - * returned. - */ - public static double min(double x, double y, double z) { - if (x < y || Double.isNaN(y)) { - return min(x, z); - } else { - return min(y, z); - } - } - - - - /** - * Compute the minimum of three values. This is performed by direct comparison. - * However, if one of the values is NaN and the other is not, the non-NaN value is - * returned. - */ - public static double min(double w, double x, double y, double z) { - return min(min(w, x), min(y, z)); - } - - - /** - * Compute the maximum of three values. This is performed by direct comparison. - * However, if one of the values is NaN and the other is not, the non-NaN value is - * returned. - */ - public static double max(double x, double y, double z) { - if (x > y || Double.isNaN(y)) { - return max(x, z); - } else { - return max(y, z); - } - } - - /** - * Calculates the hypotenuse <code>sqrt(x^2+y^2)</code>. This method is SIGNIFICANTLY - * faster than <code>Math.hypot(x,y)</code>. - */ - public static double hypot(double x, double y) { - return Math.sqrt(x * x + y * y); - } - - /** - * Reduce the angle x to the range 0 - 2*PI. - * @param x Original angle. - * @return The equivalent angle in the range 0 ... 2*PI. - */ - public static double reduce360(double x) { - double d = Math.floor(x / (2 * Math.PI)); - return x - d * 2 * Math.PI; - } - - /** - * Reduce the angle x to the range -PI - PI. - * - * Either -PI and PI might be returned, depending on the rounding function. - * - * @param x Original angle. - * @return The equivalent angle in the range -PI ... PI. - */ - public static double reduce180(double x) { - double d = Math.rint(x / (2 * Math.PI)); - return x - d * 2 * Math.PI; - } - - - /** - * Return the square root of a value. If the value is negative, zero is returned. - * This is safer in cases where rounding errors might make a value slightly negative. - * - * @param d the value of which the square root is to be taken. - * @return the square root of the value. - */ - public static double safeSqrt(double d) { - if (d < 0) { - if (d < 0.01) { - log.warn(1, "Attempting to compute sqrt(" + d + ")"); - } - return 0; - } - return Math.sqrt(d); - } - - - - public static boolean equals(double a, double b) { - double absb = Math.abs(b); - - if (absb < EPSILON / 2) { - // Near zero - return Math.abs(a) < EPSILON / 2; - } - return Math.abs(a - b) < EPSILON * absb; - } - - - /** - * Return the sign of the number. This corresponds to Math.signum, but ignores - * the special cases of zero and NaN. The value returned for those is arbitrary. - * <p> - * This method is about 4 times faster than Math.signum(). - * - * @param x the checked value. - * @return -1.0 if x<0; 1.0 if x>0; otherwise either -1.0 or 1.0. - */ - public static double sign(double x) { - return (x < 0) ? -1.0 : 1.0; - } - - /* Math.abs() is about 3x as fast as this: - - public static double abs(double x) { - return (x<0) ? -x : x; - } - */ - - - public static double average(Collection<? extends Number> values) { - if (values.isEmpty()) { - return Double.NaN; - } - - double avg = 0.0; - int count = 0; - for (Number n : values) { - avg += n.doubleValue(); - count++; - } - return avg / count; - } - - public static double stddev(Collection<? extends Number> values) { - if (values.size() < 2) { - return Double.NaN; - } - - double avg = average(values); - double stddev = 0.0; - int count = 0; - for (Number n : values) { - stddev += pow2(n.doubleValue() - avg); - count++; - } - stddev = Math.sqrt(stddev / (count - 1)); - return stddev; - } - - public static double median(Collection<? extends Number> values) { - if (values.isEmpty()) { - return Double.NaN; - } - - List<Number> sorted = new ArrayList<Number>(values); - Collections.sort(sorted, new Comparator<Number>() { - @Override - public int compare(Number o1, Number o2) { - return Double.compare(o1.doubleValue(), o2.doubleValue()); - } - }); - - int n = sorted.size(); - if (n % 2 == 0) { - return (sorted.get(n / 2).doubleValue() + sorted.get(n / 2 - 1).doubleValue()) / 2; - } else { - return sorted.get(n / 2).doubleValue(); - } - } - -} diff --git a/src/net/sf/openrocket/util/MemoryManagement.java b/src/net/sf/openrocket/util/MemoryManagement.java deleted file mode 100644 index 7b55200b..00000000 --- a/src/net/sf/openrocket/util/MemoryManagement.java +++ /dev/null @@ -1,194 +0,0 @@ -package net.sf.openrocket.util; - -import java.lang.ref.WeakReference; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * A class that performs certain memory-management operations for debugging purposes. - * For example, complex objects that are being discarded and that should be garbage-collectable - * (such as dialog windows) should be registered for monitoring by calling - * {@link #collectable(Object)}. This will allow monitoring whether the object really is - * garbage-collected or whether it is retained in memory due to a memory leak. - * Only complex objects should be registered due to the overhead of the monitoring. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public final class MemoryManagement { - private static final LogHelper log = Application.getLogger(); - - /** Purge cleared references every this many calls to {@link #collectable(Object)} */ - private static final int PURGE_CALL_COUNT = 1000; - - - /** - * Storage of the objects. This is basically a mapping from the objects (using weak references) - * to - */ - private static List<MemoryData> objects = new LinkedList<MemoryData>(); - private static int collectableCallCount = 0; - - - private static List<WeakReference<ListenerList<?>>> listenerLists = new LinkedList<WeakReference<ListenerList<?>>>(); - private static int listenerCallCount = 0; - - - private MemoryManagement() { - } - - - /** - * Mark an object that should be garbage-collectable by the GC. This class will monitor - * whether the object actually gets garbage-collected or not by holding a weak reference - * to the object. - * - * @param o the object to monitor. - */ - public static synchronized void collectable(Object o) { - if (o == null) { - throw new IllegalArgumentException("object is null"); - } - log.debug("Adding object into collectable list: " + o); - objects.add(new MemoryData(o)); - collectableCallCount++; - if (collectableCallCount % PURGE_CALL_COUNT == 0) { - purgeCollectables(); - } - } - - - /** - * Return a list of MemoryData objects corresponding to the objects that have been - * registered by {@link #collectable(Object)} and have not been garbage-collected properly. - * This method first calls <code>System.gc()</code> multiple times to attempt to - * force any remaining garbage collection. - * - * @return a list of MemoryData objects for objects that have not yet been garbage-collected. - */ - public static synchronized List<MemoryData> getRemainingCollectableObjects() { - for (int i = 0; i < 5; i++) { - System.runFinalization(); - System.gc(); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - } - } - purgeCollectables(); - return new ArrayList<MemoryData>(objects); - } - - - - - /** - * Register a new ListenerList object. This can be used to monitor freeing of listeners - * and find memory leaks. The objects are held by a weak reference, allowing them to be - * garbage-collected. - * - * @param list the listener list to register - */ - public static synchronized void registerListenerList(ListenerList<?> list) { - listenerLists.add(new WeakReference<ListenerList<?>>(list)); - listenerCallCount++; - if (listenerCallCount % PURGE_CALL_COUNT == 0) { - purgeListeners(); - } - - } - - /** - * Return a list of listener list objects corresponding to the objects that have been - * registered by {@link #registerListenerList(ListenerList)} and have not been garbage-collected yet. - * This method first calls <code>System.gc()</code> multiple times to attempt to - * force any remaining garbage collection. - * - * @return a list of listener list objects that have not yet been garbage-collected. - */ - public static synchronized List<ListenerList<?>> getRemainingListenerLists() { - for (int i = 0; i < 5; i++) { - System.runFinalization(); - System.gc(); - try { - Thread.sleep(1); - } catch (InterruptedException e) { - } - } - purgeListeners(); - List<ListenerList<?>> list = new ArrayList<ListenerList<?>>(); - for (WeakReference<ListenerList<?>> ref : listenerLists) { - ListenerList<?> l = ref.get(); - if (l != null) { - list.add(l); - } - } - return list; - } - - - - /** - * Purge all cleared references from the object list. - */ - private static void purgeCollectables() { - int origCount = objects.size(); - Iterator<MemoryData> iterator = objects.iterator(); - while (iterator.hasNext()) { - MemoryData data = iterator.next(); - if (data.getReference().get() == null) { - iterator.remove(); - } - } - log.debug(objects.size() + " of " + origCount + " objects remaining in discarded objects list after purge."); - } - - - /** - * Purge all cleared references from the object list. - */ - private static void purgeListeners() { - int origCount = listenerLists.size(); - Iterator<WeakReference<ListenerList<?>>> iterator = listenerLists.iterator(); - while (iterator.hasNext()) { - WeakReference<ListenerList<?>> ref = iterator.next(); - if (ref.get() == null) { - iterator.remove(); - } - } - log.debug(listenerLists.size() + " of " + origCount + " listener lists remaining after purge."); - } - - /** - * A value object class containing data of a discarded object reference. - */ - public static final class MemoryData { - private final WeakReference<Object> reference; - private final long registrationTime; - - private MemoryData(Object object) { - this.reference = new WeakReference<Object>(object); - this.registrationTime = System.currentTimeMillis(); - } - - /** - * Return the weak reference to the discarded object. - */ - public WeakReference<Object> getReference() { - return reference; - } - - /** - * Return the time when the object was discarded. - * @return a millisecond timestamp of when the object was discarded. - */ - public long getRegistrationTime() { - return registrationTime; - } - } - -} diff --git a/src/net/sf/openrocket/util/Monitorable.java b/src/net/sf/openrocket/util/Monitorable.java deleted file mode 100644 index bff7480e..00000000 --- a/src/net/sf/openrocket/util/Monitorable.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.sf.openrocket.util; - -/** - * Interface describing objects whose state changes can be monitored based on - * a modification ID number. If a specific object has the same modification ID - * at two different points in time, the object state is guaranteed to be the same. - * This does not necessarily hold between two different instances of an object type. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public interface Monitorable { - - /** - * Return a modification ID unique to the current state of this object and contained objects. - * The general contract is that if a specific object has the same modification ID at two moments - * in time, then the state of the object has not changed in between. Additionally the - * modification ID value must be monotonically increasing. This value can be used as a monitor - * to whether an object has been changed between two points of time. - * <p> - * Implementations may optionally fulfill the stronger requirement that any two objects of the same - * type that have the same modification ID will be equal, though for complex objects guaranteeing - * this may be impractical. - * <p> - * Objects that contain only primitive types or immutable objects can implement this method by - * increasing a modification counter or retrieving a new unique ID every time a value is set. - * <p> - * Objects that contain other objects with a mutable state may for example return the sum of the - * object's own modification ID, a modification ID counter (initially zero) and the modification IDs - * of the contained objects. When a mutable object is set, the modification counter is increased by - * the modification ID of the current object in order to preserve monotonicity. - * <p> - * If an object does not have any fields, this method can simply return zero. - * <p> - * Cloned objects may or may not have the same modification ID as the original object. - * - * @return a modification ID value for this object. - * @see UniqueID#next() - */ - public int getModID(); - -} diff --git a/src/net/sf/openrocket/util/MonitorableSet.java b/src/net/sf/openrocket/util/MonitorableSet.java deleted file mode 100644 index a7601bdf..00000000 --- a/src/net/sf/openrocket/util/MonitorableSet.java +++ /dev/null @@ -1,92 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; - -/** - * A Set that additionally implements the Monitorable interface. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class MonitorableSet<E> extends HashSet<E> implements Monitorable { - - private int modID; - - @Override - public boolean add(E e) { - modID++; - return super.add(e); - }; - - @Override - public boolean addAll(Collection<? extends E> c) { - modID++; - return super.addAll(c); - } - - @Override - public void clear() { - modID++; - super.clear(); - } - - @Override - public Iterator<E> iterator() { - return new MonitorableIterator<E>(super.iterator()); - } - - @Override - public boolean remove(Object o) { - modID++; - return super.remove(o); - } - - @Override - public boolean removeAll(Collection<?> c) { - modID++; - return super.removeAll(c); - } - - @Override - public boolean retainAll(Collection<?> c) { - modID++; - return super.retainAll(c); - } - - - @Override - public int getModID() { - return modID; - } - - @SuppressWarnings("unchecked") - @Override - public MonitorableSet<E> clone() { - return (MonitorableSet<E>) super.clone(); - } - - private class MonitorableIterator<F> implements Iterator<F> { - private final Iterator<F> iterator; - - public MonitorableIterator(Iterator<F> iterator) { - this.iterator = iterator; - } - - @Override - public boolean hasNext() { - return iterator.hasNext(); - } - - @Override - public F next() { - return iterator.next(); - } - - @Override - public void remove() { - iterator.remove(); - modID++; - } - } -} diff --git a/src/net/sf/openrocket/util/Mutable.java b/src/net/sf/openrocket/util/Mutable.java deleted file mode 100644 index c7c91359..00000000 --- a/src/net/sf/openrocket/util/Mutable.java +++ /dev/null @@ -1,66 +0,0 @@ -package net.sf.openrocket.util; - -import net.sf.openrocket.logging.TraceException; - -/** - * A utility class helping an object to be made immutable after a certain point of time. - * An object should contain an instance of Mutable and an immute() method which calls - * {@link #immute()}. Additionally, every method that changes the state of the object - * should call {@link #check()} before modification. - * <p> - * This class also provides a stack trace of the position where the object was made - * immutable to help in debugging modifications of immuted objects. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Mutable implements Cloneable { - - private TraceException immuteTrace = null; - - /** - * Mark the object immutable. Once the object has been called the object - * cannot be made mutable again. Repeated calls to this method have no effect. - */ - public void immute() { - if (immuteTrace == null) { - immuteTrace = new TraceException(1, 2); - } - } - - /** - * Check that the object is still mutable, and throw an exception if it is not. - * <p> - * The thrown exception will contain a trace of the position where the object was made - * immutable to help in debugging. - * - * @throws IllegalStateException if {@link #immute()} has been called for this object. - */ - public void check() { - if (immuteTrace != null) { - throw new IllegalStateException("Object has been made immutable at " - + immuteTrace.getMessage(), immuteTrace); - } - } - - /** - * Check whether this object is still mutable. - * - * @return whether this object is still mutable. - */ - public boolean isMutable() { - return immuteTrace == null; - } - - - /** - * Return a new Mutable instance with the same state as the current object. - */ - @Override - public Mutable clone() { - try { - return (Mutable) super.clone(); - } catch (CloneNotSupportedException e) { - throw new BugException("CloneNotSupportedException", e); - } - } -} diff --git a/src/net/sf/openrocket/util/Named.java b/src/net/sf/openrocket/util/Named.java deleted file mode 100644 index b91ae46a..00000000 --- a/src/net/sf/openrocket/util/Named.java +++ /dev/null @@ -1,56 +0,0 @@ -package net.sf.openrocket.util; - -import java.text.Collator; - -/** - * An object holder that provides a custom toString return value. - * <p> - * The class supports sorting by the name. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - * @param <T> the holder type - */ -public class Named<T> implements Comparable<Named<T>> { - - private final T object; - private final String name; - - private Collator collator = null; - - /** - * Sole constructor. - * - * @param object the held object - * @param name the value to return by toString(). - */ - public Named(T object, String name) { - this.object = object; - this.name = name; - } - - - /** - * Get the held object. - * - * @return the object. - */ - public T get() { - return object; - } - - @Override - public String toString() { - return name; - } - - - @Override - public int compareTo(Named<T> other) { - if (collator == null) { - collator = Collator.getInstance(); - } - - return collator.compare(this.toString(), other.toString()); - } - -} diff --git a/src/net/sf/openrocket/util/NumericComparator.java b/src/net/sf/openrocket/util/NumericComparator.java deleted file mode 100644 index a337dee6..00000000 --- a/src/net/sf/openrocket/util/NumericComparator.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Comparator; - -public class NumericComparator implements Comparator<Object> { - - public static final NumericComparator INSTANCE = new NumericComparator(); - - @Override - public int compare(Object o1, Object o2) { - double v1 = getValue(o1); - double v2 = getValue(o2); - - if (Double.isNaN(v1) || Double.isNaN(v2)) { - String s1 = o1.toString(); - String s2 = o2.toString(); - return s1.compareTo(s2); - } - - return Double.compare(v1, v2); - } - - private double getValue(Object o) { - if (o instanceof Number) { - return ((Number) o).doubleValue(); - } - String s = o.toString(); - try { - return Double.parseDouble(s); - } catch (NumberFormatException e) { - return Double.NaN; - } - } - -} diff --git a/src/net/sf/openrocket/util/Pair.java b/src/net/sf/openrocket/util/Pair.java deleted file mode 100644 index 9a562cb8..00000000 --- a/src/net/sf/openrocket/util/Pair.java +++ /dev/null @@ -1,71 +0,0 @@ -package net.sf.openrocket.util; - -/** - * Storage for a pair of objects. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - * @param <U> the first object type. - * @param <V> the second object type. - */ -public class Pair<U,V> { - - private final U u; - private final V v; - - - public Pair(U u, V v) { - this.u = u; - this.v = v; - } - - public U getU() { - return u; - } - - public V getV() { - return v; - } - - - /** - * Compare both components of the Pair to another object. - * The pair is equal iff both items are equal (or null). - */ - @SuppressWarnings("unchecked") - @Override - public boolean equals(Object other) { - if (!(other instanceof Pair)) - return false; - Object otherU = ((Pair)other).getU(); - Object otherV = ((Pair)other).getV(); - - if (otherU == null) { - if (this.u != null) - return false; - } else { - if (!otherU.equals(this.u)) - return false; - } - - if (otherV == null) { - if (this.v != null) - return false; - } else { - if (!otherV.equals(this.v)) - return false; - } - return true; - } - - @Override - public int hashCode() { - return ((u != null) ? u.hashCode() : 0) + ((v != null) ? v.hashCode() : 0); - } - - - @Override - public String toString() { - return "[" + u + ";" + v + "]"; - } - -} diff --git a/src/net/sf/openrocket/util/PinkNoise.java b/src/net/sf/openrocket/util/PinkNoise.java deleted file mode 100644 index 572ad032..00000000 --- a/src/net/sf/openrocket/util/PinkNoise.java +++ /dev/null @@ -1,140 +0,0 @@ -package net.sf.openrocket.util; -import java.util.Random; - - -/** - * A class that provides a source of pink noise with a power spectrum density - * proportional to 1/f^alpha. The values are computed by applying an IIR filter to - * generated Gaussian random numbers. The number of poles used in the filter may be - * specified. Values as low as 3 produce good results, but using a larger number of - * poles allows lower frequencies to be amplified. Below the cutoff frequency the - * power spectrum density if constant. - * <p> - * The IIR filter use by this class is presented by N. Jeremy Kasdin, Proceedings of - * the IEEE, Vol. 83, No. 5, May 1995, p. 822. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class PinkNoise { - private final int poles; - private final double[] multipliers; - - private final double[] values; - private final Random rnd; - - - /** - * Generate pink noise with alpha=1.0 using a five-pole IIR. - */ - public PinkNoise() { - this(1.0, 5, new Random()); - } - - - /** - * Generate a specific pink noise using a five-pole IIR. - * - * @param alpha the exponent of the pink noise, 1/f^alpha. - */ - public PinkNoise(double alpha) { - this(alpha, 5, new Random()); - } - - - /** - * Generate pink noise specifying alpha and the number of poles. The larger the - * number of poles, the lower are the lowest frequency components that are amplified. - * - * @param alpha the exponent of the pink noise, 1/f^alpha. - * @param poles the number of poles to use. - */ - public PinkNoise(double alpha, int poles) { - this(alpha, poles, new Random()); - } - - - /** - * Generate pink noise specifying alpha, the number of poles and the randomness source. - * - * @param alpha the exponent of the pink noise, 1/f^alpha. - * @param poles the number of poles to use. - * @param random the randomness source. - */ - public PinkNoise(double alpha, int poles, Random random) { - this.rnd = random; - this.poles = poles; - this.multipliers = new double[poles]; - this.values = new double[poles]; - - double a = 1; - for (int i=0; i < poles; i++) { - a = (i - alpha/2) * a / (i+1); - multipliers[i] = a; - } - - // Fill the history with random values - for (int i=0; i < 5*poles; i++) - this.nextValue(); - } - - - - public double nextValue() { - double x = rnd.nextGaussian(); -// double x = rnd.nextDouble()-0.5; - - for (int i=0; i < poles; i++) { - x -= multipliers[i] * values[i]; - } - System.arraycopy(values, 0, values, 1, values.length-1); - values[0] = x; - - return x; - } - - - public static void main(String[] arg) { - - PinkNoise source; - - source = new PinkNoise(1.0, 100); - double std = 0; - for (int i=0; i < 1000000; i++) { - - } - - -// int n = 5000000; -// double avgavg=0; -// double avgstd = 0; -// double[] val = new double[n]; -// -// for (int j=0; j < 10; j++) { -// double avg=0, std=0; -// source = new PinkNoise(5.0/3.0, 2); -// -// for (int i=0; i < n; i++) { -// val[i] = source.nextValue(); -// avg += val[i]; -// } -// avg /= n; -// for (int i=0; i < n; i++) { -// std += (val[i]-avg)*(val[i]-avg); -// } -// std /= n; -// std = Math.sqrt(std); -// -// System.out.println("avg:"+avg+" stddev:"+std); -// avgavg += avg; -// avgstd += std; -// } -// avgavg /= 10; -// avgstd /= 10; -// System.out.println("Average avg:"+avgavg+" std:"+avgstd); -// - // Two poles: - - } - - -} diff --git a/src/net/sf/openrocket/util/PolyInterpolator.java b/src/net/sf/openrocket/util/PolyInterpolator.java deleted file mode 100644 index f97d45c4..00000000 --- a/src/net/sf/openrocket/util/PolyInterpolator.java +++ /dev/null @@ -1,262 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Arrays; - -/** - * A class for polynomial interpolation. The interpolation constraints can be specified - * either as function values or values of the n'th derivative of the function. - * Using an interpolation consists of three steps: - * <p> - * 1. constructing a <code>PolyInterpolator</code> using the interpolation x coordinates <br> - * 2. generating the interpolation polynomial using the function and derivative values <br> - * 3. evaluating the polynomial at the desired point - * <p> - * The constructor takes an array of double arrays. The first array defines x coordinate - * values for the function values, the second array x coordinate values for the function's - * derivatives, the third array for second derivatives and so on. Constructing the - * <code>PolyInterpolator</code> is relatively slow, O(n^3) where n is the order of the - * polynomial. (It contains calculation of the inverse of an n x n matrix.) - * <p> - * Generating the interpolation polynomial is performed by the method - * {@link #interpolator(double...)}, which takes as an argument the function and - * derivative values. This operation takes O(n^2) time. - * <p> - * Finally, evaluating the polynomial at different positions takes O(n) time. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class PolyInterpolator { - - // Order of the polynomial - private final int count; - - private final double[][] interpolationMatrix; - - - /** - * Construct a polynomial interpolation generator. All arguments to the constructor - * are the x coordinates of the interpolated function. The first array correspond to - * the function values, the second to function derivatives, the third to second - * derivatives and so forth. The order of the polynomial is automatically calculated - * from the total number of constraints. - * <p> - * The construction takes O(n^3) time. - * - * @param points an array of constraints, the first corresponding to function value - * constraints, the second to derivative constraints etc. - */ - public PolyInterpolator(double[] ... points) { - int count = 0; - for (int i=0; i < points.length; i++) { - count += points[i].length; - } - if (count == 0) { - throw new IllegalArgumentException("No interpolation points defined."); - } - - this.count = count; - - int[] mul = new int[count]; - Arrays.fill(mul, 1); - - double[][] matrix = new double[count][count]; - int row = 0; - for (int j=0; j < points.length; j++) { - - for (int i=0; i < points[j].length; i++) { - double x = 1; - for (int col = count-1-j; col>= 0; col--) { - matrix[row][col] = x*mul[col]; - x *= points[j][i]; - } - row++; - } - - for (int i=0; i < count; i++) { - mul[i] *= (count-i-j-1); - } - } - assert(row == count); - - interpolationMatrix = inverse(matrix); - } - - - /** - * Generates an interpolation polynomial. The arguments supplied to this method - * are the function values, derivatives, second derivatives etc. in the order - * specified in the constructor (i.e. values first, then derivatives etc). - * <p> - * This method takes O(n^2) time. - * - * @param values the function values, derivatives etc. at positions defined in the - * constructor. - * @return the coefficients of the interpolation polynomial, the highest order - * term first and the constant last. - */ - public double[] interpolator(double... values) { - if (values.length != count) { - throw new IllegalArgumentException("Wrong number of arguments "+values.length+ - " expected "+count); - } - - double[] ret = new double[count]; - - for (int j=0; j < count; j++) { - for (int i=0; i < count; i++) { - ret[j] += interpolationMatrix[j][i] * values[i]; - } - } - - return ret; - } - - - /** - * Interpolate the given values at the point <code>x</code>. This is equivalent - * to generating an interpolation polynomial and evaluating the polynomial at the - * specified point. - * - * @param x point at which to evaluate the interpolation polynomial. - * @param values the function, derivatives etc. at position defined in the - * constructor. - * @return the value of the interpolation. - */ - public double interpolate(double x, double... values) { - return eval(x, interpolator(values)); - } - - - /** - * Evaluate a polynomial at the specified point <code>x</code>. The coefficients are - * assumed to have the highest order coefficient first and the constant term last. - * - * @param x position at which to evaluate the polynomial. - * @param coefficients polynomial coefficients, highest term first and constant last. - * @return the value of the polynomial. - */ - public static double eval(double x, double[] coefficients) { - double v = 1; - double result = 0; - for (int i = coefficients.length-1; i >= 0; i--) { - result += coefficients[i] * v; - v *= x; - } - return result; - } - - - - - private static double[][] inverse(double[][] matrix) { - int n = matrix.length; - - double x[][] = new double[n][n]; - double b[][] = new double[n][n]; - int index[] = new int[n]; - for (int i=0; i<n; ++i) - b[i][i] = 1; - - // Transform the matrix into an upper triangle - gaussian(matrix, index); - - // Update the matrix b[i][j] with the ratios stored - for (int i=0; i<n-1; ++i) - for (int j=i+1; j<n; ++j) - for (int k=0; k<n; ++k) - b[index[j]][k] -= matrix[index[j]][i]*b[index[i]][k]; - - // Perform backward substitutions - for (int i=0; i<n; ++i) { - x[n-1][i] = b[index[n-1]][i]/matrix[index[n-1]][n-1]; - for (int j=n-2; j>=0; --j) { - x[j][i] = b[index[j]][i]; - for (int k=j+1; k<n; ++k) { - x[j][i] -= matrix[index[j]][k]*x[k][i]; - } - x[j][i] /= matrix[index[j]][j]; - } - } - return x; - } - - private static void gaussian(double a[][], - int index[]) { - int n = index.length; - double c[] = new double[n]; - - // Initialize the index - for (int i=0; i<n; ++i) index[i] = i; - - // Find the rescaling factors, one from each row - for (int i=0; i<n; ++i) { - double c1 = 0; - for (int j=0; j<n; ++j) { - double c0 = Math.abs(a[i][j]); - if (c0 > c1) c1 = c0; - } - c[i] = c1; - } - - // Search the pivoting element from each column - int k = 0; - for (int j=0; j<n-1; ++j) { - double pi1 = 0; - for (int i=j; i<n; ++i) { - double pi0 = Math.abs(a[index[i]][j]); - pi0 /= c[index[i]]; - if (pi0 > pi1) { - pi1 = pi0; - k = i; - } - } - - // Interchange rows according to the pivoting order - int itmp = index[j]; - index[j] = index[k]; - index[k] = itmp; - for (int i=j+1; i<n; ++i) { - double pj = a[index[i]][j]/a[index[j]][j]; - - // Record pivoting ratios below the diagonal - a[index[i]][j] = pj; - - // Modify other elements accordingly - for (int l=j+1; l<n; ++l) - a[index[i]][l] -= pj*a[index[j]][l]; - } - } - } - - - - - public static void main(String[] arg) { - - PolyInterpolator p0 = new PolyInterpolator( - new double[] {0.6, 1.1}, - new double[] {0.6, 1.1} - ); - double[] r0 = p0.interpolator(1.5, 1.6, 2, -3); - - PolyInterpolator p1 = new PolyInterpolator( - new double[] {0.6, 1.1}, - new double[] {0.6, 1.1}, - new double[] {0.6} - ); - double[] r1 = p1.interpolator(1.5, 1.6, 2, -3, 0); - - PolyInterpolator p2 = new PolyInterpolator( - new double[] {0.6, 1.1}, - new double[] {0.6, 1.1}, - new double[] {0.6, 1.1} - ); - double[] r2 = p2.interpolator(1.5, 1.6, 2, -3, 0, 0); - - - for (double x=0.6; x <= 1.11; x += 0.01) { - System.out.println(x + " " + eval(x,r0) + " " + eval(x,r1) + " " + eval(x,r2)); - } - - } -} diff --git a/src/net/sf/openrocket/util/PrintProperties.java b/src/net/sf/openrocket/util/PrintProperties.java deleted file mode 100644 index d3d4fe11..00000000 --- a/src/net/sf/openrocket/util/PrintProperties.java +++ /dev/null @@ -1,22 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.SortedSet; -import java.util.TreeSet; - -public class PrintProperties { - - public static void main(String[] args) { - - // Sort the keys - SortedSet<String> keys = new TreeSet<String>(); - for (Object key: System.getProperties().keySet()) { - keys.add((String)key); - } - - for (String key: keys) { - System.out.println(key + "=" + System.getProperty((String)key)); - } - - } - -} diff --git a/src/net/sf/openrocket/util/Quaternion.java b/src/net/sf/openrocket/util/Quaternion.java deleted file mode 100644 index 5f277662..00000000 --- a/src/net/sf/openrocket/util/Quaternion.java +++ /dev/null @@ -1,356 +0,0 @@ -package net.sf.openrocket.util; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.startup.Application; - -/** - * An immutable quaternion class. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class Quaternion { - private static final LogHelper log = Application.getLogger(); - - - //////// Debug section - /* - * Debugging info. If openrocket.debug.quaternioncount is defined, a line is - * printed every 1000000 instantiations (or as many as defined). - */ - private static final boolean COUNT_DEBUG; - private static final int COUNT_DIFF; - static { - String str = System.getProperty("openrocket.debug.quaternioncount"); - int diff = 0; - if (str == null) { - COUNT_DEBUG = false; - COUNT_DIFF = 0; - } else { - COUNT_DEBUG = true; - try { - diff = Integer.parseInt(str); - } catch (NumberFormatException ignore) { - } - if (diff < 1000) - diff = 1000000; - COUNT_DIFF = diff; - } - } - - private static int count = 0; - { - // Debug count - if (COUNT_DEBUG) { - synchronized (Quaternion.class) { - count++; - if ((count % COUNT_DIFF) == 0) { - log.debug("Quaternion instantiated " + count + " times."); - } - } - } - } - - //////// End debug section - - - - private final double w, x, y, z; - private double norm = -1; - - - /** - * Construct a new "one" quaternion. This is equivalent to <code>new Quaternion(1,0,0,0)</code>. - */ - public Quaternion() { - this(1, 0, 0, 0); - } - - - public Quaternion(double w, double x, double y, double z) { - this.w = w; - this.x = x; - this.y = y; - this.z = z; - } - - - /** - * Create a rotation quaternion corresponding to the rotation vector. The rotation axis is - * the direction of vector, and rotation angle is the length of the vector. - * <p> - * The cost of the operation is approximately that of computing the length of the coordinate - * and computing two trigonometric functions. - * - * @param rotation the rotation vector - * @return the quaternion corresponding to the rotation vector - */ - public static Quaternion rotation(Coordinate rotation) { - double length = rotation.length(); - if (length < 0.000001) { - return new Quaternion(1, 0, 0, 0); - } - double sin = Math.sin(length / 2); - double cos = Math.cos(length / 2); - return new Quaternion(cos, - sin * rotation.x / length, sin * rotation.y / length, sin * rotation.z / length); - } - - /** - * Create a rotation quaternion corresponding to the rotation around the provided vector with - * the provided angle. - * - * @param axis the rotation axis - * @param angle the rotation angle - * @return the corresponding quaternion - */ - public static Quaternion rotation(Coordinate axis, double angle) { - Coordinate a = axis.normalize(); - double sin = Math.sin(angle); - double cos = Math.cos(angle); - return new Quaternion(cos, sin * a.x, sin * a.y, sin * a.z); - } - - - public double getW() { - return w; - } - - public double getX() { - return x; - } - - public double getY() { - return y; - } - - public double getZ() { - return z; - } - - - /** - * Check whether any of the quaternion values is NaN. - * - * @return true if w, x, y or z is NaN - */ - public boolean isNaN() { - return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(w); - } - - - /** - * Multiply this quaternion by the other quaternion from the right side. This - * calculates the product <code>result = this * other</code>. - * - * @param other the quaternion to multiply this quaternion by. - * @return this quaternion. - */ - public Quaternion multiplyRight(Quaternion other) { - double newW = (this.w * other.w - this.x * other.x - this.y * other.y - this.z * other.z); - double newX = (this.w * other.x + this.x * other.w + this.y * other.z - this.z * other.y); - double newY = (this.w * other.y + this.y * other.w + this.z * other.x - this.x * other.z); - double newZ = (this.w * other.z + this.z * other.w + this.x * other.y - this.y * other.x); - - return new Quaternion(newW, newX, newY, newZ); - } - - /** - * Multiply this quaternion by the other quaternion from the left side. This - * calculates the product <code>result = other * this</code>. - * - * @param other the quaternion to multiply this quaternion by. - * @return this quaternion. - */ - public Quaternion multiplyLeft(Quaternion other) { - /* other(abcd) * this(wxyz) */ - - double newW = (other.w * this.w - other.x * this.x - other.y * this.y - other.z * this.z); - double newX = (other.w * this.x + other.x * this.w + other.y * this.z - other.z * this.y); - double newY = (other.w * this.y + other.y * this.w + other.z * this.x - other.x * this.z); - double newZ = (other.w * this.z + other.z * this.w + other.x * this.y - other.y * this.x); - - return new Quaternion(newW, newX, newY, newZ); - } - - - - - - /** - * Return a normalized version of this quaternion. If this quaternion is the zero quaternion, throws - * <code>IllegalStateException</code>. - * - * @return a normalized version of this quaternion. - * @throws IllegalStateException if the norm of this quaternion is zero. - */ - public Quaternion normalize() { - double n = norm(); - if (n < 0.0000001) { - throw new IllegalStateException("attempting to normalize zero-quaternion"); - } - return new Quaternion(w / n, x / n, y / n, z / n); - } - - - /** - * Normalize the quaternion if the norm is more than 1ppm from one. - * - * @return this quaternion or a normalized version of this quaternion. - * @throws IllegalStateException if the norm of this quaternion is zero. - */ - public Quaternion normalizeIfNecessary() { - double n2 = norm2(); - if (n2 < 0.999999 || n2 > 1.000001) { - return normalize(); - } else { - return this; - } - } - - - - /** - * Return the norm of this quaternion. - * - * @return the norm of this quaternion sqrt(w^2 + x^2 + y^2 + z^2). - */ - public double norm() { - if (norm < 0) { - norm = MathUtil.safeSqrt(x * x + y * y + z * z + w * w); - } - return norm; - } - - /** - * Return the square of the norm of this quaternion. - * - * @return the square of the norm of this quaternion (w^2 + x^2 + y^2 + z^2). - */ - public double norm2() { - return x * x + y * y + z * z + w * w; - } - - - /** - * Perform a coordinate rotation using this unit quaternion. The result is - * <code>this * coord * this^(-1)</code>. - * <p> - * This method assumes that the norm of this quaternion is one. - * - * @param coord the coordinate to rotate. - * @return the rotated coordinate. - */ - public Coordinate rotate(Coordinate coord) { - double a, b, c, d; - - assert (Math.abs(norm2() - 1) < 0.00001) : "Quaternion not unit length: " + this; - - - // (a,b,c,d) = this * coord = (w,x,y,z) * (0,cx,cy,cz) - a = -x * coord.x - y * coord.y - z * coord.z; // w - b = w * coord.x + y * coord.z - z * coord.y; // x i - c = w * coord.y - x * coord.z + z * coord.x; // y j - d = w * coord.z + x * coord.y - y * coord.x; // z k - - - // return = (a,b,c,d) * (this)^-1 = (a,b,c,d) * (w,-x,-y,-z) - - // Assert that the w-value is zero - assert (Math.abs(a * w + b * x + c * y + d * z) < coord.max() * MathUtil.EPSILON) : ("Should be zero: " + (a * w + b * x + c * y + d * z) + " in " + this + " c=" + coord); - - return new Coordinate( - -a * x + b * w - c * z + d * y, - -a * y + b * z + c * w - d * x, - -a * z - b * y + c * x + d * w, - coord.weight); - } - - /** - * Perform an inverse coordinate rotation using this unit quaternion. The result is - * <code>this^(-1) * coord * this</code>. - * <p> - * This method assumes that the norm of this quaternion is one. - * - * @param coord the coordinate to rotate. - * @return the rotated coordinate. - */ - public Coordinate invRotate(Coordinate coord) { - double a, b, c, d; - - assert (Math.abs(norm2() - 1) < 0.00001) : "Quaternion not unit length: " + this; - - // (a,b,c,d) = (this)^-1 * coord = (w,-x,-y,-z) * (0,cx,cy,cz) - a = +x * coord.x + y * coord.y + z * coord.z; - b = w * coord.x - y * coord.z + z * coord.y; - c = w * coord.y + x * coord.z - z * coord.x; - d = w * coord.z - x * coord.y + y * coord.x; - - - // return = (a,b,c,d) * this = (a,b,c,d) * (w,x,y,z) - assert (Math.abs(a * w - b * x - c * y - d * z) < Math.max(coord.max(), 1) * MathUtil.EPSILON) : ("Should be zero: " + (a * w - b * x - c * y - d * z) + " in " + this + " c=" + coord); - - return new Coordinate( - a * x + b * w + c * z - d * y, - a * y - b * z + c * w + d * x, - a * z + b * y - c * x + d * w, - coord.weight); - } - - - /** - * Rotate the coordinate (0,0,1) using this quaternion. The result is returned - * as a Coordinate. This method is equivalent to calling - * <code>q.rotate(new Coordinate(0,0,1))</code> but requires only about half of the - * multiplications. - * - * @return The coordinate (0,0,1) rotated using this quaternion. - */ - public Coordinate rotateZ() { - return new Coordinate( - 2 * (w * y + x * z), - 2 * (y * z - w * x), - w * w - x * x - y * y + z * z); - } - - - @Override - public String toString() { - return String.format("Quaternion[%f,%f,%f,%f,norm=%f]", w, x, y, z, this.norm()); - } - - public static void main(String[] arg) { - - Quaternion q = new Quaternion(Math.random() - 0.5, Math.random() - 0.5, - Math.random() - 0.5, Math.random() - 0.5); - q.normalize(); - - q = new Quaternion(-0.998717, 0.000000, 0.050649, -0.000000); - - Coordinate coord = new Coordinate(10 * (Math.random() - 0.5), - 10 * (Math.random() - 0.5), 10 * (Math.random() - 0.5)); - - System.out.println("Quaternion: " + q); - System.out.println("Coordinate: " + coord); - coord = q.invRotate(coord); - System.out.println("Rotated: " + coord); - coord = q.rotate(coord); - System.out.println("Back: " + coord); - - - q = new Quaternion(0.237188, 0.570190, -0.514542, 0.594872); - q.normalize(); - Coordinate c = new Coordinate(148578428.914, 8126778.954, -607.741); - - System.out.println("Rotated: " + q.rotate(c)); - - // Coordinate c = new Coordinate(0,1,0); - // Coordinate rot = new Coordinate(Math.PI/4,0,0); - // - // System.out.println("Before: "+c); - // c = rotation(rot).invRotate(c); - // System.out.println("After: "+c); - - - } - -} diff --git a/src/net/sf/openrocket/util/QuaternionMultiply.java b/src/net/sf/openrocket/util/QuaternionMultiply.java deleted file mode 100644 index 099ecf8d..00000000 --- a/src/net/sf/openrocket/util/QuaternionMultiply.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.sf.openrocket.util; - -public class QuaternionMultiply { - - private static class Value { - public int sign = 1; - public String value; - - public Value multiply(Value other) { - Value result = new Value(); - result.sign = this.sign * other.sign; - if (this.value.compareTo(other.value) < 0) - result.value = this.value + "*" + other.value; - else - result.value = other.value + "*" + this.value; - return result; - } - @Override - public String toString() { - String s; - - if (sign < 0) - s = "-"; - else - s = "+"; - - if (sign == 0) - s += " 0"; - else - s += " " + value; - - return s; - } - } - - - - private static Value[] multiply(Value[] first, Value[] second) { - return null; - } - - - public static void main(String[] arg) { - if (arg.length % 4 != 0 || arg.length < 4) { - System.out.println("Must have modulo 4 args, at least 4"); - return; - } - - Value[][] values = new Value[arg.length/4][4]; - - for (int i=0; i<arg.length; i++) { - Value value = new Value(); - - if (arg[i].equals("")) { - value.sign = 0; - } else { - if (arg[i].startsWith("-")) { - value.sign = -1; - value.value = arg[i].substring(1); - } else if (arg[i].startsWith("+")) { - value.sign = 1; - value.value = arg[i].substring(1); - } else { - value.sign = 1; - value.value = arg[i]; - } - } - - values[i/4][i%4] = value; - } - - System.out.println("Multiplying:"); - for (int i=0; i < values.length; i++) { - print(values[i]); - } - System.out.println("Result:"); - - Value[] result = values[0]; - for (int i=1; i < values.length; i++) { - result = multiply(result, values[i]); - } - print(result); - } - - private static void print(Value[] q) { - System.out.println(" " + q[0] + " " + q[1] + " i " + q[2] + " j " + q[3] + " k"); - } - -} - diff --git a/src/net/sf/openrocket/util/Reflection.java b/src/net/sf/openrocket/util/Reflection.java deleted file mode 100644 index b3c66f62..00000000 --- a/src/net/sf/openrocket/util/Reflection.java +++ /dev/null @@ -1,197 +0,0 @@ -package net.sf.openrocket.util; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; - -import net.sf.openrocket.rocketcomponent.RocketComponent; - - -public class Reflection { - - private static final String ROCKETCOMPONENT_PACKAGE = "net.sf.openrocket.rocketcomponent"; - - /** - * Simple wrapper class that converts the Method.invoke() exceptions into suitable - * RuntimeExceptions. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - public static class Method { - private final java.lang.reflect.Method method; - - public Method(java.lang.reflect.Method m) { - if (m == null) { - throw new IllegalArgumentException("method is null"); - } - method = m; - } - - /** - * Same as Method.invoke(), but the possible exceptions are wrapped into - * RuntimeExceptions. - */ - public Object invoke(Object obj, Object... args) { - try { - return method.invoke(obj, args); - } catch (IllegalArgumentException e) { - throw new BugException("Error while invoking method '" + method + "'. " + - "Please report this as a bug.", e); - } catch (IllegalAccessException e) { - throw new BugException("Error while invoking method '" + method + "'. " + - "Please report this as a bug.", e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - } - - /** - * Invoke static method. Equivalent to invoke(null, args...). - */ - public Object invokeStatic(Object... args) { - return invoke(null, args); - } - - /** - * Same as Method.toString(). - */ - @Override - public String toString() { - return method.toString(); - } - } - - - /** - * Handles an InvocationTargetException gracefully. If the cause is an unchecked - * exception it is thrown, otherwise it is encapsulated in a BugException. - * <p> - * This method has a return type of Error in order to allow writing code like: - * <pre>throw Reflection.handleInvocationTargetException(e)</pre> - * This allows the compiler verifying that the call will never succeed correctly - * and ending that branch of execution. - * - * @param e the InvocationTargetException that occurred (not null). - * @return never returns normally. - */ - public static Error handleWrappedException(Exception e) { - Throwable cause = e.getCause(); - if (cause == null) { - throw new BugException("wrapped exception without cause", e); - } - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } - if (cause instanceof Error) { - throw (Error) cause; - } - throw new BugException("wrapped exception occurred", cause); - } - - - - /** - * Find a method from the rocket component classes. - * Throws an exception if method not found. - */ - public static Reflection.Method findMethod( - Class<? extends RocketComponent> componentClass, - String method, Class<?>... params) { - Reflection.Method m = findMethod(ROCKETCOMPONENT_PACKAGE, componentClass, - "", method, params); - if (m == null) { - throw new BugException("Could not find method for componentClass=" - + componentClass + " method=" + method); - } - return m; - } - - - - public static Reflection.Method findMethod(String pack, RocketComponent component, - String method, Class<?>... params) { - return findMethod(pack, component.getClass(), "", method, params); - } - - - public static Reflection.Method findMethod(String pack, RocketComponent component, - String suffix, String method, Class<?>... params) { - return findMethod(pack, component.getClass(), suffix, method, params); - } - - - public static Reflection.Method findMethod(String pack, - Class<? extends RocketComponent> componentClass, - String suffix, String method, Class<?>... params) { - Class<?> currentclass; - String name; - - currentclass = componentClass; - while ((currentclass != null) && (currentclass != Object.class)) { - name = currentclass.getCanonicalName(); - if (name.lastIndexOf('.') >= 0) - name = name.substring(name.lastIndexOf(".") + 1); - name = pack + "." + name + suffix; - - try { - Class<?> c = Class.forName(name); - java.lang.reflect.Method m = c.getMethod(method, params); - return new Reflection.Method(m); - } catch (ClassNotFoundException ignore) { - } catch (NoSuchMethodException ignore) { - } - - currentclass = currentclass.getSuperclass(); - } - return null; - } - - - public static Object construct(String pack, RocketComponent component, String suffix, - Object... params) { - - Class<?> currentclass; - String name; - - currentclass = component.getClass(); - while ((currentclass != null) && (currentclass != Object.class)) { - name = currentclass.getCanonicalName(); - if (name.lastIndexOf('.') >= 0) - name = name.substring(name.lastIndexOf(".") + 1); - name = pack + "." + name + suffix; - - try { - Class<?> c = Class.forName(name); - Class<?>[] paramClasses = new Class<?>[params.length]; - for (int i = 0; i < params.length; i++) { - paramClasses[i] = params[i].getClass(); - } - - // Constructors must be searched manually. Why?! - main: for (Constructor<?> constructor : c.getConstructors()) { - Class<?>[] parameterTypes = constructor.getParameterTypes(); - if (params.length != parameterTypes.length) - continue; - for (int i = 0; i < params.length; i++) { - if (!parameterTypes[i].isInstance(params[i])) - continue main; - } - // Matching constructor found - return constructor.newInstance(params); - } - } catch (ClassNotFoundException ignore) { - } catch (IllegalArgumentException e) { - throw new BugException("Construction of " + name + " failed", e); - } catch (InstantiationException e) { - throw new BugException("Construction of " + name + " failed", e); - } catch (IllegalAccessException e) { - throw new BugException("Construction of " + name + " failed", e); - } catch (InvocationTargetException e) { - throw Reflection.handleWrappedException(e); - } - - currentclass = currentclass.getSuperclass(); - } - throw new BugException("Suitable constructor for component " + component + - " not found"); - } -} diff --git a/src/net/sf/openrocket/util/Rotation2D.java b/src/net/sf/openrocket/util/Rotation2D.java deleted file mode 100644 index 1672a011..00000000 --- a/src/net/sf/openrocket/util/Rotation2D.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.sf.openrocket.util; - -public class Rotation2D { - - public static final Rotation2D ID = new Rotation2D(0.0, 1.0); - - public final double sin, cos; - - - public Rotation2D(double angle) { - this(Math.sin(angle), Math.cos(angle)); - } - - public Rotation2D(double sin, double cos) { - this.sin = sin; - this.cos = cos; - } - - public Coordinate rotateX(Coordinate c) { - return new Coordinate(c.x, cos*c.y - sin*c.z, cos*c.z + sin*c.y, c.weight); - } - - public Coordinate rotateY(Coordinate c) { - return new Coordinate(cos*c.x + sin*c.z, c.y, cos*c.z - sin*c.x, c.weight); - } - - public Coordinate rotateZ(Coordinate c) { - return new Coordinate(cos*c.x - sin*c.y, cos*c.y + sin*c.x, c.z, c.weight); - } - - - public Coordinate invRotateX(Coordinate c) { - return new Coordinate(c.x, cos*c.y + sin*c.z, cos*c.z - sin*c.y, c.weight); - } - - public Coordinate invRotateY(Coordinate c) { - return new Coordinate(cos*c.x - sin*c.z, c.y, cos*c.z + sin*c.x, c.weight); - } - - public Coordinate invRotateZ(Coordinate c) { - return new Coordinate(cos*c.x + sin*c.y, cos*c.y - sin*c.x, c.z, c.weight); - } - -} diff --git a/src/net/sf/openrocket/util/SafetyMutex.java b/src/net/sf/openrocket/util/SafetyMutex.java deleted file mode 100644 index 646c54e4..00000000 --- a/src/net/sf/openrocket/util/SafetyMutex.java +++ /dev/null @@ -1,237 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.LinkedList; - -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.TraceException; -import net.sf.openrocket.startup.Application; - -/** - * A mutex that can be used for verifying thread safety. This class cannot be - * used to perform synchronization, only to detect concurrency issues. This - * class can be used by the main methods of non-thread-safe classes to ensure - * the class is not wrongly used from multiple threads concurrently. - * <p> - * This mutex is not reentrant even for the same thread that has locked it. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public abstract class SafetyMutex { - private static final boolean USE_CHECKS = Application.useSafetyChecks(); - private static final LogHelper log = Application.getLogger(); - - - /** - * Return a new instance of a safety mutex. This returns an actual implementation - * or a bogus implementation depending on whether safety checks are enabled or disabled. - * - * @return a new instance of a safety mutex - */ - public static SafetyMutex newInstance() { - if (USE_CHECKS) { - return new ConcreteSafetyMutex(); - } else { - return new BogusSafetyMutex(); - } - } - - - /** - * Verify that this mutex is unlocked, but don't lock it. This has the same effect - * as <code>mutex.lock(); mutex.unlock();</code> and is useful for methods that return - * immediately (e.g. getters). - * - * @throws ConcurrencyException if this mutex is already locked. - */ - public abstract void verify(); - - - /** - * Lock this mutex. If this mutex is already locked an error is raised and - * a ConcurrencyException is thrown. The location parameter is used to distinguish - * the locking location, and it should be e.g. the method name. - * - * @param location a string describing the location where this mutex was locked (cannot be null). - * - * @throws ConcurrencyException if this mutex is already locked. - */ - public abstract void lock(String location); - - - /** - * Unlock this mutex. If this mutex is not locked at the position of the parameter - * or was locked by another thread than the current thread an error is raised, - * but an exception is not thrown. - * <p> - * This method is guaranteed never to throw an exception, so it can safely be used in finally blocks. - * - * @param location a location string matching that which locked the mutex - * @return whether the unlocking was successful (this normally doesn't need to be checked) - */ - public abstract boolean unlock(String location); - - - - /** - * Bogus implementation of a safety mutex (used when safety checking is not performed). - */ - static class BogusSafetyMutex extends SafetyMutex { - - @Override - public void verify() { - } - - @Override - public void lock(String location) { - } - - @Override - public boolean unlock(String location) { - return true; - } - - } - - /** - * A concrete, working implementation of a safety mutex. - */ - static class ConcreteSafetyMutex extends SafetyMutex { - private static final boolean STORE_LOCKING_LOCATION = (System.getProperty("openrocket.debug.mutexlocation") != null); - - // Package-private for unit testing - static volatile boolean errorReported = false; - - // lockingThread is set when this mutex is locked. - Thread lockingThread = null; - // longingLocation is set when lockingThread is, if STORE_LOCKING_LOCATION is true - TraceException lockingLocation = null; - // Stack of places that have locked this mutex - final LinkedList<String> locations = new LinkedList<String>(); - - - - @Override - public synchronized void verify() { - checkState(true); - if (lockingThread != null && lockingThread != Thread.currentThread()) { - error("Mutex is already locked", true); - } - } - - - - @Override - public synchronized void lock(String location) { - if (location == null) { - throw new IllegalArgumentException("location is null"); - } - checkState(true); - - Thread currentThread = Thread.currentThread(); - if (lockingThread != null && lockingThread != currentThread) { - error("Mutex is already locked", true); - } - - lockingThread = currentThread; - if (STORE_LOCKING_LOCATION) { - lockingLocation = new TraceException("Location where mutex was locked '" + location + "'"); - } - locations.push(location); - } - - - - - @Override - public synchronized boolean unlock(String location) { - try { - - if (location == null) { - Application.getExceptionHandler().handleErrorCondition("location is null"); - location = ""; - } - checkState(false); - - - // Check that the mutex is locked - if (lockingThread == null) { - error("Mutex was not locked", false); - return false; - } - - // Check that the mutex is locked by the current thread - if (lockingThread != Thread.currentThread()) { - error("Mutex is being unlocked from differerent thread than where it was locked", false); - return false; - } - - // Check that the unlock location is correct - String lastLocation = locations.pop(); - if (!location.equals(lastLocation)) { - locations.push(lastLocation); - error("Mutex unlocking location does not match locking location, location=" + location, false); - return false; - } - - // Unlock the mutex if the last one - if (locations.isEmpty()) { - lockingThread = null; - lockingLocation = null; - } - return true; - } catch (Exception e) { - Application.getExceptionHandler().handleErrorCondition("An exception occurred while unlocking a mutex, " + - "locking thread=" + lockingThread + " locations=" + locations, e); - return false; - } - } - - - - /** - * Check that the internal state of the mutex (lockingThread vs. locations) is correct. - */ - private void checkState(boolean throwException) { - /* - * Disallowed states: - * lockingThread == null && !locations.isEmpty() - * lockingThread != null && locations.isEmpty() - */ - if ((lockingThread == null) ^ (locations.isEmpty())) { - // Clear the mutex only after error() has executed (and possibly thrown an exception) - try { - error("Mutex data inconsistency occurred - unlocking mutex", throwException); - } finally { - lockingThread = null; - lockingLocation = null; - locations.clear(); - } - } - } - - - /** - * Raise an error. The first occurrence is passed directly to the exception handler, - * later errors are simply logged. - */ - private void error(String message, boolean throwException) { - message = message + - ", current thread = " + Thread.currentThread() + - ", locking thread=" + lockingThread + - ", locking locations=" + locations; - - ConcurrencyException ex = new ConcurrencyException(message, lockingLocation); - - if (!errorReported) { - errorReported = true; - Application.getExceptionHandler().handleErrorCondition(ex); - } else { - log.error(message, ex); - } - - if (throwException) { - throw ex; - } - } - } -} diff --git a/src/net/sf/openrocket/util/StateChangeListener.java b/src/net/sf/openrocket/util/StateChangeListener.java deleted file mode 100644 index 2d5e5df9..00000000 --- a/src/net/sf/openrocket/util/StateChangeListener.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.EventListener; -import java.util.EventObject; - -public interface StateChangeListener extends EventListener { - - public void stateChanged( EventObject e ); - -} diff --git a/src/net/sf/openrocket/util/Statistics.java b/src/net/sf/openrocket/util/Statistics.java deleted file mode 100644 index f96adf19..00000000 --- a/src/net/sf/openrocket/util/Statistics.java +++ /dev/null @@ -1,9 +0,0 @@ -package net.sf.openrocket.util; - -public interface Statistics { - - public String getStatistics(); - - public void resetStatistics(); - -} diff --git a/src/net/sf/openrocket/util/TestRockets.java b/src/net/sf/openrocket/util/TestRockets.java deleted file mode 100644 index 0dfc5629..00000000 --- a/src/net/sf/openrocket/util/TestRockets.java +++ /dev/null @@ -1,569 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.Random; - -import net.sf.openrocket.material.Material; -import net.sf.openrocket.material.Material.Type; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Bulkhead; -import net.sf.openrocket.rocketcomponent.CenteringRing; -import net.sf.openrocket.rocketcomponent.ExternalComponent; -import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish; -import net.sf.openrocket.rocketcomponent.FinSet.CrossSection; -import net.sf.openrocket.rocketcomponent.FreeformFinSet; -import net.sf.openrocket.rocketcomponent.IllegalFinPointException; -import net.sf.openrocket.rocketcomponent.InnerTube; -import net.sf.openrocket.rocketcomponent.InternalComponent; -import net.sf.openrocket.rocketcomponent.LaunchLug; -import net.sf.openrocket.rocketcomponent.MassComponent; -import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent; -import net.sf.openrocket.rocketcomponent.NoseCone; -import net.sf.openrocket.rocketcomponent.ReferenceType; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.rocketcomponent.RocketComponent.Position; -import net.sf.openrocket.rocketcomponent.Stage; -import net.sf.openrocket.rocketcomponent.Transition; -import net.sf.openrocket.rocketcomponent.Transition.Shape; -import net.sf.openrocket.rocketcomponent.TrapezoidFinSet; -import net.sf.openrocket.rocketcomponent.TubeCoupler; -import net.sf.openrocket.startup.Application; - -public class TestRockets { - - private final String key; - private final Random rnd; - - - public TestRockets(String key) { - - if (key == null) { - Random rnd = new Random(); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < 6; i++) { - int n = rnd.nextInt(62); - if (n < 10) { - sb.append((char) ('0' + n)); - } else if (n < 36) { - sb.append((char) ('A' + n - 10)); - } else { - sb.append((char) ('a' + n - 36)); - } - } - key = sb.toString(); - } - - this.key = key; - this.rnd = new Random(key.hashCode()); - - } - - - /** - * Create a new test rocket based on the value 'key'. The rocket utilizes most of the - * properties and features available. The same key always returns the same rocket, - * but different key values produce slightly different rockets. A key value of - * <code>null</code> generates a rocket using a random key. - * <p> - * The rocket created by this method is not fly-worthy. It is also NOT guaranteed - * that later versions would produce exactly the same rocket! - * - * @return a rocket design. - */ - public Rocket makeTestRocket() { - - Rocket rocket = new Rocket(); - setBasics(rocket); - rocket.setCustomReferenceLength(rnd(0.05)); - rocket.setDesigner("Designer " + key); - rocket.setReferenceType((ReferenceType) randomEnum(ReferenceType.class)); - rocket.setRevision("Rocket revision " + key); - rocket.setName(key); - - - Stage stage = new Stage(); - setBasics(stage); - rocket.addChild(stage); - - - NoseCone nose = new NoseCone(); - setBasics(stage); - nose.setAftRadius(rnd(0.03)); - nose.setAftRadiusAutomatic(rnd.nextBoolean()); - nose.setAftShoulderCapped(rnd.nextBoolean()); - nose.setAftShoulderLength(rnd(0.02)); - nose.setAftShoulderRadius(rnd(0.02)); - nose.setAftShoulderThickness(rnd(0.002)); - nose.setClipped(rnd.nextBoolean()); - nose.setThickness(rnd(0.002)); - nose.setFilled(rnd.nextBoolean()); - nose.setForeRadius(rnd(0.1)); // Unset - nose.setLength(rnd(0.15)); - nose.setShapeParameter(rnd(0.5)); - nose.setType((Shape) randomEnum(Shape.class)); - stage.addChild(nose); - - - Transition shoulder = new Transition(); - setBasics(shoulder); - shoulder.setAftRadius(rnd(0.06)); - shoulder.setAftRadiusAutomatic(rnd.nextBoolean()); - shoulder.setAftShoulderCapped(rnd.nextBoolean()); - shoulder.setAftShoulderLength(rnd(0.02)); - shoulder.setAftShoulderRadius(rnd(0.05)); - shoulder.setAftShoulderThickness(rnd(0.002)); - shoulder.setClipped(rnd.nextBoolean()); - shoulder.setThickness(rnd(0.002)); - shoulder.setFilled(rnd.nextBoolean()); - shoulder.setForeRadius(rnd(0.03)); - shoulder.setForeRadiusAutomatic(rnd.nextBoolean()); - shoulder.setForeShoulderCapped(rnd.nextBoolean()); - shoulder.setForeShoulderLength(rnd(0.02)); - shoulder.setForeShoulderRadius(rnd(0.02)); - shoulder.setForeShoulderThickness(rnd(0.002)); - shoulder.setLength(rnd(0.15)); - shoulder.setShapeParameter(rnd(0.5)); - shoulder.setThickness(rnd(0.003)); - shoulder.setType((Shape) randomEnum(Shape.class)); - stage.addChild(shoulder); - - - BodyTube body = new BodyTube(); - setBasics(body); - body.setThickness(rnd(0.002)); - body.setFilled(rnd.nextBoolean()); - body.setIgnitionDelay(rnd.nextDouble() * 3); - body.setIgnitionEvent((IgnitionEvent) randomEnum(IgnitionEvent.class)); - body.setLength(rnd(0.3)); - body.setMotorMount(rnd.nextBoolean()); - body.setMotorOverhang(rnd.nextGaussian() * 0.03); - body.setOuterRadius(rnd(0.06)); - body.setOuterRadiusAutomatic(rnd.nextBoolean()); - stage.addChild(body); - - - Transition boattail = new Transition(); - setBasics(boattail); - boattail.setAftRadius(rnd(0.03)); - boattail.setAftRadiusAutomatic(rnd.nextBoolean()); - boattail.setAftShoulderCapped(rnd.nextBoolean()); - boattail.setAftShoulderLength(rnd(0.02)); - boattail.setAftShoulderRadius(rnd(0.02)); - boattail.setAftShoulderThickness(rnd(0.002)); - boattail.setClipped(rnd.nextBoolean()); - boattail.setThickness(rnd(0.002)); - boattail.setFilled(rnd.nextBoolean()); - boattail.setForeRadius(rnd(0.06)); - boattail.setForeRadiusAutomatic(rnd.nextBoolean()); - boattail.setForeShoulderCapped(rnd.nextBoolean()); - boattail.setForeShoulderLength(rnd(0.02)); - boattail.setForeShoulderRadius(rnd(0.05)); - boattail.setForeShoulderThickness(rnd(0.002)); - boattail.setLength(rnd(0.15)); - boattail.setShapeParameter(rnd(0.5)); - boattail.setThickness(rnd(0.003)); - boattail.setType((Shape) randomEnum(Shape.class)); - stage.addChild(boattail); - - - MassComponent mass = new MassComponent(); - setBasics(mass); - mass.setComponentMass(rnd(0.05)); - mass.setLength(rnd(0.05)); - mass.setRadialDirection(rnd(100)); - mass.setRadialPosition(rnd(0.02)); - mass.setRadius(rnd(0.05)); - nose.addChild(mass); - - - - - return rocket; - } - - - private void setBasics(RocketComponent c) { - c.setComment(c.getComponentName() + " comment " + key); - c.setName(c.getComponentName() + " name " + key); - - c.setCGOverridden(rnd.nextBoolean()); - c.setMassOverridden(rnd.nextBoolean()); - c.setOverrideCGX(rnd(0.2)); - c.setOverrideMass(rnd(0.05)); - c.setOverrideSubcomponents(rnd.nextBoolean()); - - if (c.isMassive()) { - // Only massive components are drawn - c.setColor(randomColor()); - c.setLineStyle((LineStyle) randomEnum(LineStyle.class)); - } - - if (c instanceof ExternalComponent) { - ExternalComponent e = (ExternalComponent) c; - e.setFinish((Finish) randomEnum(Finish.class)); - double d = rnd(100); - e.setMaterial(Material.newMaterial(Type.BULK, "Testmat " + d, d, rnd.nextBoolean())); - } - - if (c instanceof InternalComponent) { - InternalComponent i = (InternalComponent) c; - i.setRelativePosition((Position) randomEnum(Position.class)); - i.setPositionValue(rnd(0.3)); - } - } - - - - private double rnd(double scale) { - return (rnd.nextDouble() * 0.2 + 0.9) * scale; - } - - private Color randomColor() { - return new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)); - } - - private <T extends Enum<T>> Enum<T> randomEnum(Class<T> c) { - Enum<T>[] values = c.getEnumConstants(); - if (values.length == 0) - return null; - - return values[rnd.nextInt(values.length)]; - } - - - - - - public Rocket makeSmallFlyable() { - double noseconeLength = 0.10, noseconeRadius = 0.01; - double bodytubeLength = 0.20, bodytubeRadius = 0.01, bodytubeThickness = 0.001; - - int finCount = 3; - double finRootChord = 0.04, finTipChord = 0.05, finSweep = 0.01, finThickness = 0.003, finHeight = 0.03; - - - Rocket rocket; - Stage stage; - NoseCone nosecone; - BodyTube bodytube; - TrapezoidFinSet finset; - - rocket = new Rocket(); - stage = new Stage(); - stage.setName("Stage1"); - - nosecone = new NoseCone(Transition.Shape.ELLIPSOID, noseconeLength, noseconeRadius); - bodytube = new BodyTube(bodytubeLength, bodytubeRadius, bodytubeThickness); - - finset = new TrapezoidFinSet(finCount, finRootChord, finTipChord, finSweep, finHeight); - - - // Stage construction - rocket.addChild(stage); - - - // Component construction - stage.addChild(nosecone); - stage.addChild(bodytube); - - bodytube.addChild(finset); - - Material material = Application.getPreferences().getDefaultComponentMaterial(null, Material.Type.BULK); - nosecone.setMaterial(material); - bodytube.setMaterial(material); - finset.setMaterial(material); - - String id = rocket.newMotorConfigurationID(); - bodytube.setMotorMount(true); - - Motor m = Application.getMotorSetDatabase().findMotors(null, null, "B4", Double.NaN, Double.NaN).get(0); - bodytube.setMotor(id, m); - bodytube.setMotorOverhang(0.005); - rocket.getDefaultConfiguration().setMotorConfigurationID(id); - - rocket.getDefaultConfiguration().setAllStages(); - - - return rocket; - } - - - public static Rocket makeBigBlue() { - Rocket rocket; - Stage stage; - NoseCone nosecone; - BodyTube bodytube; - FreeformFinSet finset; - MassComponent mcomp; - - rocket = new Rocket(); - stage = new Stage(); - stage.setName("Stage1"); - - nosecone = new NoseCone(Transition.Shape.ELLIPSOID, 0.105, 0.033); - nosecone.setThickness(0.001); - bodytube = new BodyTube(0.69, 0.033, 0.001); - - finset = new FreeformFinSet(); - try { - finset.setPoints(new Coordinate[] { - new Coordinate(0, 0), - new Coordinate(0.115, 0.072), - new Coordinate(0.255, 0.072), - new Coordinate(0.255, 0.037), - new Coordinate(0.150, 0) - }); - } catch (IllegalFinPointException e) { - e.printStackTrace(); - } - finset.setThickness(0.003); - finset.setFinCount(4); - - finset.setCantAngle(0 * Math.PI / 180); - System.err.println("Fin cant angle: " + (finset.getCantAngle() * 180 / Math.PI)); - - mcomp = new MassComponent(0.2, 0.03, 0.045 + 0.060); - mcomp.setRelativePosition(Position.TOP); - mcomp.setPositionValue(0); - - // Stage construction - rocket.addChild(stage); - rocket.setPerfectFinish(false); - - - // Component construction - stage.addChild(nosecone); - stage.addChild(bodytube); - - bodytube.addChild(finset); - - bodytube.addChild(mcomp); - - // Material material = new Material("Test material", 500); - // nosecone.setMaterial(material); - // bodytube.setMaterial(material); - // finset.setMaterial(material); - - String id = rocket.newMotorConfigurationID(); - bodytube.setMotorMount(true); - - // Motor m = Application.getMotorSetDatabase().findMotors(null, null, "F12J", Double.NaN, Double.NaN).get(0); - // bodytube.setMotor(id, m); - // bodytube.setMotorOverhang(0.005); - rocket.getDefaultConfiguration().setMotorConfigurationID(id); - - rocket.getDefaultConfiguration().setAllStages(); - - - return rocket; - } - - - - public static Rocket makeIsoHaisu() { - Rocket rocket; - Stage stage; - NoseCone nosecone; - BodyTube tube1, tube2, tube3; - TrapezoidFinSet finset; - TrapezoidFinSet auxfinset; - MassComponent mcomp; - - final double R = 0.07; - - rocket = new Rocket(); - stage = new Stage(); - stage.setName("Stage1"); - - nosecone = new NoseCone(Transition.Shape.OGIVE, 0.53, R); - nosecone.setThickness(0.005); - nosecone.setMassOverridden(true); - nosecone.setOverrideMass(0.588); - stage.addChild(nosecone); - - tube1 = new BodyTube(0.505, R, 0.005); - tube1.setMassOverridden(true); - tube1.setOverrideMass(0.366); - stage.addChild(tube1); - - tube2 = new BodyTube(0.605, R, 0.005); - tube2.setMassOverridden(true); - tube2.setOverrideMass(0.427); - stage.addChild(tube2); - - tube3 = new BodyTube(1.065, R, 0.005); - tube3.setMassOverridden(true); - tube3.setOverrideMass(0.730); - stage.addChild(tube3); - - - LaunchLug lug = new LaunchLug(); - tube1.addChild(lug); - - TubeCoupler coupler = new TubeCoupler(); - coupler.setOuterRadiusAutomatic(true); - coupler.setThickness(0.005); - coupler.setLength(0.28); - coupler.setMassOverridden(true); - coupler.setOverrideMass(0.360); - coupler.setRelativePosition(Position.BOTTOM); - coupler.setPositionValue(-0.14); - tube1.addChild(coupler); - - - // Parachute - MassComponent mass = new MassComponent(0.05, 0.05, 0.280); - mass.setRelativePosition(Position.TOP); - mass.setPositionValue(0.2); - tube1.addChild(mass); - - // Cord - mass = new MassComponent(0.05, 0.05, 0.125); - mass.setRelativePosition(Position.TOP); - mass.setPositionValue(0.2); - tube1.addChild(mass); - - // Payload - mass = new MassComponent(0.40, R, 1.500); - mass.setRelativePosition(Position.TOP); - mass.setPositionValue(0.25); - tube1.addChild(mass); - - - auxfinset = new TrapezoidFinSet(); - auxfinset.setName("CONTROL"); - auxfinset.setFinCount(2); - auxfinset.setRootChord(0.05); - auxfinset.setTipChord(0.05); - auxfinset.setHeight(0.10); - auxfinset.setSweep(0); - auxfinset.setThickness(0.008); - auxfinset.setCrossSection(CrossSection.AIRFOIL); - auxfinset.setRelativePosition(Position.TOP); - auxfinset.setPositionValue(0.28); - auxfinset.setBaseRotation(Math.PI / 2); - tube1.addChild(auxfinset); - - - - - coupler = new TubeCoupler(); - coupler.setOuterRadiusAutomatic(true); - coupler.setLength(0.28); - coupler.setRelativePosition(Position.TOP); - coupler.setPositionValue(0.47); - coupler.setMassOverridden(true); - coupler.setOverrideMass(0.360); - tube2.addChild(coupler); - - - - // Parachute - mass = new MassComponent(0.1, 0.05, 0.028); - mass.setRelativePosition(Position.TOP); - mass.setPositionValue(0.14); - tube2.addChild(mass); - - Bulkhead bulk = new Bulkhead(); - bulk.setOuterRadiusAutomatic(true); - bulk.setMassOverridden(true); - bulk.setOverrideMass(0.050); - bulk.setRelativePosition(Position.TOP); - bulk.setPositionValue(0.27); - tube2.addChild(bulk); - - // Chord - mass = new MassComponent(0.1, 0.05, 0.125); - mass.setRelativePosition(Position.TOP); - mass.setPositionValue(0.19); - tube2.addChild(mass); - - - - InnerTube inner = new InnerTube(); - inner.setOuterRadius(0.08 / 2); - inner.setInnerRadius(0.0762 / 2); - inner.setLength(0.86); - inner.setMassOverridden(true); - inner.setOverrideMass(0.388); - tube3.addChild(inner); - - - CenteringRing center = new CenteringRing(); - center.setInnerRadiusAutomatic(true); - center.setOuterRadiusAutomatic(true); - center.setLength(0.005); - center.setMassOverridden(true); - center.setOverrideMass(0.038); - center.setRelativePosition(Position.BOTTOM); - center.setPositionValue(0); - tube3.addChild(center); - - - center = new CenteringRing(); - center.setInnerRadiusAutomatic(true); - center.setOuterRadiusAutomatic(true); - center.setLength(0.005); - center.setMassOverridden(true); - center.setOverrideMass(0.038); - center.setRelativePosition(Position.TOP); - center.setPositionValue(0.28); - tube3.addChild(center); - - - center = new CenteringRing(); - center.setInnerRadiusAutomatic(true); - center.setOuterRadiusAutomatic(true); - center.setLength(0.005); - center.setMassOverridden(true); - center.setOverrideMass(0.038); - center.setRelativePosition(Position.TOP); - center.setPositionValue(0.83); - tube3.addChild(center); - - - - - - finset = new TrapezoidFinSet(); - finset.setRootChord(0.495); - finset.setTipChord(0.1); - finset.setHeight(0.185); - finset.setThickness(0.005); - finset.setSweep(0.3); - finset.setRelativePosition(Position.BOTTOM); - finset.setPositionValue(-0.03); - finset.setBaseRotation(Math.PI / 2); - tube3.addChild(finset); - - - finset.setCantAngle(0 * Math.PI / 180); - System.err.println("Fin cant angle: " + (finset.getCantAngle() * 180 / Math.PI)); - - - // Stage construction - rocket.addChild(stage); - rocket.setPerfectFinish(false); - - - - String id = rocket.newMotorConfigurationID(); - tube3.setMotorMount(true); - - // Motor m = Application.getMotorSetDatabase().findMotors(null, null, "L540", Double.NaN, Double.NaN).get(0); - // tube3.setMotor(id, m); - // tube3.setMotorOverhang(0.02); - rocket.getDefaultConfiguration().setMotorConfigurationID(id); - - // tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER); - - rocket.getDefaultConfiguration().setAllStages(); - - - return rocket; - } - - - -} diff --git a/src/net/sf/openrocket/util/TextUtil.java b/src/net/sf/openrocket/util/TextUtil.java deleted file mode 100644 index 32dffd84..00000000 --- a/src/net/sf/openrocket/util/TextUtil.java +++ /dev/null @@ -1,167 +0,0 @@ -package net.sf.openrocket.util; - - -public class TextUtil { - private static final char[] HEX = { - '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' - }; - - - /** - * Return the bytes formatted as a hexadecimal string. The length of the - * string will be twice the number of bytes, with no spacing between the bytes - * and lowercase letters utilized. - * - * @param bytes the bytes to convert. - * @return the bytes in hexadecimal notation. - */ - public static final String hexString(byte[] bytes) { - StringBuilder sb = new StringBuilder(bytes.length * 2); - for (byte b : bytes) { - sb.append(HEX[(b >>> 4) & 0xF]); - sb.append(HEX[b & 0xF]); - } - return sb.toString(); - } - - /** - * Return a string of the double value with suitable precision (5 digits). - * The string is the shortest representation of the value including the - * required precision. - * - * @param d the value to present. - * @return a representation with suitable precision. - */ - public static final String doubleToString(double d) { - - // Check for special cases - if (MathUtil.equals(d, 0)) - return "0"; - - if (Double.isNaN(d)) - return "NaN"; - - if (Double.isInfinite(d)) { - if (d < 0) - return "-Inf"; - else - return "Inf"; - } - - - final String sign = (d < 0) ? "-" : ""; - double abs = Math.abs(d); - - // Small and large values always in exponential notation - if (abs < 0.001 || abs >= 100000000) { - return sign + exponentialFormat(abs); - } - - // Check whether decimal or exponential notation is shorter - - String exp = exponentialFormat(abs); - String dec = decimalFormat(abs); - - if (dec.length() <= exp.length()) - return sign + dec; - else - return sign + exp; - } - - - /* - * value must be positive and not zero! - */ - private static String exponentialFormat(double value) { - int exp; - - exp = 0; - while (value < 1.0) { - value *= 10; - exp--; - } - while (value >= 10.0) { - value /= 10; - exp++; - } - - return shortDecimal(value, 4) + "e" + exp; - } - - - /* - * value must be positive and not zero! - */ - private static String decimalFormat(double value) { - if (value >= 10000) - return "" + (int) (value + 0.5); - - int decimals = 1; - double v = value; - while (v < 1000) { - v *= 10; - decimals++; - } - - return shortDecimal(value, decimals); - } - - - - - /* - * value must be positive! - */ - private static String shortDecimal(double value, int decimals) { - - // Calculate rounding and limit values (rounding slightly smaller) - int rounding = 1; - double limit = 0.5; - for (int i = 0; i < decimals; i++) { - rounding *= 10; - limit /= 10; - } - - // Round value - value = (Math.rint(value * rounding) + 0.1) / rounding; - - - int whole = (int) value; - value -= whole; - - - if (value < limit) - return "" + whole; - limit *= 10; - - StringBuilder sb = new StringBuilder(); - sb.append("" + whole); - sb.append('.'); - - - for (int i = 0; i < decimals; i++) { - - value *= 10; - whole = (int) value; - value -= whole; - sb.append((char) ('0' + whole)); - - if (value < limit) - return sb.toString(); - limit *= 10; - - } - - return sb.toString(); - } - - - public static String htmlEncode(String s) { - s = s.replace("&", "&"); - s = s.replace("\"", """); - s = s.replace("<", "<"); - s = s.replace(">", ">"); - return s; - } -} diff --git a/src/net/sf/openrocket/util/Transformation.java b/src/net/sf/openrocket/util/Transformation.java deleted file mode 100644 index 2c546974..00000000 --- a/src/net/sf/openrocket/util/Transformation.java +++ /dev/null @@ -1,279 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; - -/** - * Defines an affine transformation of the form A*x+c, where x and c are Coordinates and - * A is a 3x3 matrix. - * - * The Transformations are immutable. All modification methods return a new transformation. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ - -public class Transformation implements java.io.Serializable { - - - public static final Transformation IDENTITY = - new Transformation(); - - public static final Transformation PROJECT_XY = - new Transformation(new double[][]{{1,0,0},{0,1,0},{0,0,0}}); - public static final Transformation PROJECT_YZ = - new Transformation(new double[][]{{0,0,0},{0,1,0},{0,0,1}}); - public static final Transformation PROJECT_XZ = - new Transformation(new double[][]{{1,0,0},{0,0,0},{0,0,1}}); - - private static final int X = 0; - private static final int Y = 1; - private static final int Z = 2; - - private final Coordinate translate; - private final double[][] rotation = new double[3][3]; - - /** - * Create identity transformation. - */ - public Transformation() { - translate = new Coordinate(0,0,0); - rotation[X][X]=1; - rotation[Y][Y]=1; - rotation[Z][Z]=1; - } - - /** - * Create transformation with only translation. - * @param x Translation in x-axis. - * @param y Translation in y-axis. - * @param z Translation in z-axis. - */ - public Transformation(double x,double y,double z) { - translate = new Coordinate(x,y,z); - rotation[X][X]=1; - rotation[Y][Y]=1; - rotation[Z][Z]=1; - } - - /** - * Create transformation with only translation. - * @param translation The translation term. - */ - public Transformation(Coordinate translation) { - this.translate = translation; - rotation[X][X]=1; - rotation[Y][Y]=1; - rotation[Z][Z]=1; - } - - /** - * Create transformation with given rotation matrix and translation. - * @param rotation - * @param translation - */ - public Transformation(double[][] rotation, Coordinate translation) { - for (int i=0; i<3; i++) - for (int j=0; j<3; j++) - this.rotation[i][j] = rotation[i][j]; - this.translate = translation; - } - - - /** - * Create transformation with given rotation matrix and translation. - * @param rotation - * @param translation - */ - public Transformation(double[][] rotation) { - for (int i=0; i<3; i++) - for (int j=0; j<3; j++) - this.rotation[i][j] = rotation[i][j]; - this.translate = Coordinate.NUL; - } - - - - - - /** - * Transform a coordinate according to this transformation. - * - * @param orig the coordinate to transform. - * @return the result. - */ - public Coordinate transform(Coordinate orig) { - final double x,y,z; - - x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z + translate.x; - y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z + translate.y; - z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z + translate.z; - - return new Coordinate(x,y,z,orig.weight); - } - - - /** - * Transform an array of coordinates. The transformed coordinates are stored - * in the same array, and the array is returned. - * - * @param orig the coordinates to transform. - * @return <code>orig</code>, with the coordinates transformed. - */ - public Coordinate[] transform(Coordinate[] orig) { - for (int i=0; i < orig.length; i++) { - orig[i] = transform(orig[i]); - } - return orig; - } - - /** - * Transforms all coordinates in a Collection. The original coordinate elements are - * removed from the set and replaced with the transformed ones. The Collection given - * must implement the .clear() and .addAll() methods. - * - * @param set Collection of coordinates to transform. - */ - public void transform(Collection<Coordinate> set) { - ArrayList<Coordinate> temp = new ArrayList<Coordinate>(set.size()); - Iterator<Coordinate> iter = set.iterator(); - while (iter.hasNext()) - temp.add(this.transform(iter.next())); - set.clear(); - set.addAll(temp); - } - - /** - * Applies only the linear transformation A*x - * @param orig Coordinate to transform. - */ - public Coordinate linearTransform(Coordinate orig) { - final double x,y,z; - - x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z; - y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z; - z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z; - - return new Coordinate(x,y,z,orig.weight); - } - - /** - * Applies the given transformation before this tranformation. The resulting - * transformation result.transform(c) will equal this.transform(other.transform(c)). - * - * @param other Transformation to apply - * @return The new transformation - */ - public Transformation applyTransformation(Transformation other) { - // other = Ax+b - // this = Cx+d - // C(Ax+b)+d = CAx + Cb+d - - // Translational portion - Transformation combined = new Transformation( - this.linearTransform(other.translate).add(this.translate) - ); - - // Linear portion - for (int i=0; i<3; i++) { - final double x,y,z; - x = rotation[i][X]; - y = rotation[i][Y]; - z = rotation[i][Z]; - combined.rotation[i][X] = - x*other.rotation[X][X] + y*other.rotation[Y][X] + z*other.rotation[Z][X]; - combined.rotation[i][Y] = - x*other.rotation[X][Y] + y*other.rotation[Y][Y] + z*other.rotation[Z][Y]; - combined.rotation[i][Z] = - x*other.rotation[X][Z] + y*other.rotation[Y][Z] + z*other.rotation[Z][Z]; - } - return combined; - } - - - /** - * Rotate around x-axis a given angle. - * @param theta The angle to rotate in radians. - * @return The transformation. - */ - public static Transformation rotate_x(double theta) { - return new Transformation(new double[][]{ - {1,0,0}, - {0,Math.cos(theta),-Math.sin(theta)}, - {0,Math.sin(theta),Math.cos(theta)}}); - } - - /** - * Rotate around y-axis a given angle. - * @param theta The angle to rotate in radians. - * @return The transformation. - */ - public static Transformation rotate_y(double theta) { - return new Transformation(new double[][]{ - {Math.cos(theta),0,Math.sin(theta)}, - {0,1,0}, - {-Math.sin(theta),0,Math.cos(theta)}}); - } - - /** - * Rotate around z-axis a given angle. - * @param theta The angle to rotate in radians. - * @return The transformation. - */ - public static Transformation rotate_z(double theta) { - return new Transformation(new double[][]{ - {Math.cos(theta),-Math.sin(theta),0}, - {Math.sin(theta),Math.cos(theta),0}, - {0,0,1}}); - } - - - - public void print(String... str) { - for (String s: str) { - System.out.println(s); - } - System.out.printf("[%3.2f %3.2f %3.2f] [%3.2f]\n", - rotation[X][X],rotation[X][Y],rotation[X][Z],translate.x); - System.out.printf("[%3.2f %3.2f %3.2f] + [%3.2f]\n", - rotation[Y][X],rotation[Y][Y],rotation[Y][Z],translate.y); - System.out.printf("[%3.2f %3.2f %3.2f] [%3.2f]\n", - rotation[Z][X],rotation[Z][Y],rotation[Z][Z],translate.z); - System.out.println(); - } - - - @Override - public boolean equals(Object other) { - if (!(other instanceof Transformation)) - return false; - Transformation o = (Transformation)other; - for (int i=0; i<3; i++) { - for (int j=0; j<3; j++) { - if (!MathUtil.equals(this.rotation[i][j], o.rotation[i][j])) - return false; - } - } - return this.translate.equals(o.translate); - } - - public static void main(String[] arg) { - Transformation t; - - t = new Transformation(); - t.print("Empty"); - t = new Transformation(1,2,3); - t.print("1,2,3"); - t = new Transformation(new Coordinate(2,3,4)); - t.print("coord 2,3 4"); - - t = Transformation.rotate_y(0.01); - t.print("rotate_y 0.01"); - - t = new Transformation(-1,0,0); - t = t.applyTransformation(Transformation.rotate_y(0.01)); - t = t.applyTransformation(new Transformation(1,0,0)); - t.print("shift-rotate-shift"); - } - -} diff --git a/src/net/sf/openrocket/util/UncloseableInputStream.java b/src/net/sf/openrocket/util/UncloseableInputStream.java deleted file mode 100644 index ca0779fc..00000000 --- a/src/net/sf/openrocket/util/UncloseableInputStream.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.sf.openrocket.util; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * An InputStream filter that prevents closing the source stream. The - * {@link #close()} method is overridden to do nothing. - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class UncloseableInputStream extends FilterInputStream { - - public UncloseableInputStream(InputStream in) { - super(in); - } - - @Override - public void close() throws IOException { - // No-op - } -} diff --git a/src/net/sf/openrocket/util/UniqueID.java b/src/net/sf/openrocket/util/UniqueID.java deleted file mode 100644 index f9987c76..00000000 --- a/src/net/sf/openrocket/util/UniqueID.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.sf.openrocket.util; - -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; - -public class UniqueID { - - private static AtomicInteger nextId = new AtomicInteger(1); - - /** - * Return a positive integer ID unique during this program execution. - * <p> - * The following is guaranteed of the returned ID values: - * <ul> - * <li>The value is unique during this program execution - * <li>The value is positive - * <li>The values are monotonically increasing - * </ul> - * <p> - * This method is thread-safe and fast. - * - * @return a positive integer ID unique in this program execution. - */ - public static int next() { - return nextId.getAndIncrement(); - } - - - /** - * Return a new universally unique ID string. - * - * @return a unique identifier string. - */ - public static String uuid() { - return UUID.randomUUID().toString(); - } - -} diff --git a/src/net/sf/openrocket/util/Utils.java b/src/net/sf/openrocket/util/Utils.java deleted file mode 100644 index 93d320ef..00000000 --- a/src/net/sf/openrocket/util/Utils.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.sf.openrocket.util; - -public class Utils { - - /** - * Null-safe equals method. - * - * @param first the first object to compare - * @param second the second object to compare - * @return whether the two objects are both equal or both <code>null</code> - */ - public static boolean equals(Object first, Object second) { - if (first == null) { - return second == null; - } else { - return first.equals(second); - } - } - - - /** - * Check whether an array contains a specified object. - * - * @param array the array to search - * @param search the object to search for - * @return whether the object was in the array - */ - public static boolean contains(Object[] array, Object search) { - for (Object o : array) { - if (equals(o, search)) { - return true; - } - } - return false; - } - -} diff --git a/src/net/sf/openrocket/util/WorldCoordinate.java b/src/net/sf/openrocket/util/WorldCoordinate.java deleted file mode 100644 index 762b0295..00000000 --- a/src/net/sf/openrocket/util/WorldCoordinate.java +++ /dev/null @@ -1,89 +0,0 @@ -package net.sf.openrocket.util; - -/** - * A WorldCoordinate contains the latitude, longitude and altitude position of a rocket. - */ -public class WorldCoordinate { - - /** Mean Earth radius */ - public static final double REARTH = 6371000.0; - /** Sidearial Earth rotation rate */ - public static final double EROT = 7.2921150e-5; - - - private final double lat, lon, alt; - - /** - * Constructs a new WorldCoordinate - * - * @param lat latitude in degrees north. From -90 to 90, values outside are clamped. - * @param lon longitude in degrees east. From -180 to 180, values outside are reduced to the range. - * @param alt altitude in meters. Unbounded. - */ - public WorldCoordinate(double lat, double lon, double alt) { - this.lat = MathUtil.clamp(Math.toRadians(lat), -Math.PI / 2, Math.PI / 2); - this.lon = MathUtil.reduce180(Math.toRadians(lon)); - this.alt = alt; - } - - - /** - * Returns the altitude. - */ - public double getAltitude() { - return this.alt; - } - - /** - * Returns Longitude in radians - */ - public double getLongitudeRad() { - return this.lon; - } - - /** - * Returns Longitude in degrees - */ - public double getLongitudeDeg() { - return Math.toDegrees(this.lon); - } - - /** - * Returns latitude in radians - */ - public double getLatitudeRad() { - return this.lat; - } - - /** - * Returns latitude in degrees - */ - public double getLatitudeDeg() { - return Math.toDegrees(this.lat); - } - - - - @Override - public String toString() { - return "WorldCoordinate[lat=" + getLatitudeDeg() + ", lon=" + getLongitudeDeg() + ", alt=" + getAltitude() + "]"; - } - - - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof WorldCoordinate)) { - return false; - } - WorldCoordinate other = (WorldCoordinate) obj; - return (MathUtil.equals(this.lat, other.lat) && - MathUtil.equals(this.lon, other.lon) && MathUtil.equals(this.alt, other.alt)); - } - - @Override - public int hashCode() { - return ((int) (1000 * (lat + lon + alt))); - } - -} diff --git a/src/net/sf/openrocket/utils/GraphicalMotorSelector.java b/src/net/sf/openrocket/utils/GraphicalMotorSelector.java deleted file mode 100644 index 1087c5df..00000000 --- a/src/net/sf/openrocket/utils/GraphicalMotorSelector.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.util.Pair; - -public class GraphicalMotorSelector { - - public static void main(String[] args) throws IOException { - - if (args.length == 0) { - System.err.println("MotorPlot <files>"); - System.exit(1); - } - - // Load files - Map<String, List<Pair<String, ThrustCurveMotor>>> map = - new LinkedHashMap<String, List<Pair<String, ThrustCurveMotor>>>(); - - GeneralMotorLoader loader = new GeneralMotorLoader(); - for (String file : args) { - - for (Motor motor : loader.load(new FileInputStream(file), file)) { - ThrustCurveMotor m = (ThrustCurveMotor) motor; - System.out.println("Loaded " + m + " from file " + file); - - Pair<String, ThrustCurveMotor> pair = new Pair<String, ThrustCurveMotor>(file, m); - String key = m.getManufacturer() + ":" + m.getDesignation(); - - List<Pair<String, ThrustCurveMotor>> list = map.get(key); - if (list == null) { - list = new ArrayList<Pair<String, ThrustCurveMotor>>(); - map.put(key, list); - } - - list.add(pair); - } - } - - - // Go through different motors - int count = 0; - for (String key : map.keySet()) { - count++; - List<Pair<String, ThrustCurveMotor>> list = map.get(key); - - - // Select best one of identical motors - List<String> filenames = new ArrayList<String>(); - List<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); - for (Pair<String, ThrustCurveMotor> pair : list) { - String file = pair.getU(); - ThrustCurveMotor m = pair.getV(); - - int index = indexOf(motors, m); - if (index >= 0) { - // Replace previous if this has more delays, a known type or longer comment - ThrustCurveMotor m2 = motors.get(index); - if (m.getStandardDelays().length > m2.getStandardDelays().length || - (m2.getMotorType() == Motor.Type.UNKNOWN && - m.getMotorType() != Motor.Type.UNKNOWN) || - (m.getDescription().trim().length() > - m2.getDescription().trim().length())) { - - filenames.set(index, file); - motors.set(index, m); - - } - } else { - filenames.add(file); - motors.add(m); - } - } - - if (filenames.size() == 0) { - - System.out.println("ERROR selecting from " + list); - System.exit(1); - - } else if (filenames.size() == 1) { - - select(filenames.get(0), list, false); - - } else { - - System.out.println("Choosing from " + filenames + - " (" + count + "/" + map.size() + ")"); - MotorPlot plot = new MotorPlot(filenames, motors); - plot.setVisible(true); - plot.dispose(); - int n = plot.getSelected(); - if (n < 0) { - System.out.println("NONE SELECTED from " + filenames); - } else { - select(filenames.get(n), list, true); - } - - } - - } - - } - - private static void select(String selected, List<Pair<String, ThrustCurveMotor>> list, boolean manual) { - System.out.print("SELECT " + selected + " "); - if (manual) { - System.out.println("(manual)"); - } else if (list.size() == 1) { - System.out.println("(only)"); - } else { - System.out.println("(identical)"); - } - - for (Pair<String, ThrustCurveMotor> pair : list) { - String file = pair.getU(); - if (!file.equals(selected)) { - System.out.println("IGNORE " + file); - } - } - } - - - private static int indexOf(List<ThrustCurveMotor> motors, ThrustCurveMotor motor) { - for (int i = 0; i < motors.size(); i++) { - ThrustCurveMotor m = motors.get(i); - // TODO: Similar? - if (m.equals(motor)) { - if (m.getStandardDelays().length == 0 || motor.getStandardDelays().length == 0 || - Arrays.equals(m.getStandardDelays(), motor.getStandardDelays())) { - return i; - } - } - } - return -1; - } -} diff --git a/src/net/sf/openrocket/utils/LogSpeedTest.java b/src/net/sf/openrocket/utils/LogSpeedTest.java deleted file mode 100644 index 4ca2f9a4..00000000 --- a/src/net/sf/openrocket/utils/LogSpeedTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.sf.openrocket.utils; - -import net.sf.openrocket.logging.DelegatorLogger; -import net.sf.openrocket.logging.LogHelper; -import net.sf.openrocket.logging.LogLevel; - -public class LogSpeedTest { - private static LogHelper log; - - private static final int COUNT = 1000000; - - public static void main(String[] args) { - - System.setProperty("openrocket.log.tracelevel", "user"); - log = new DelegatorLogger(); - - for (LogLevel l : LogLevel.values()) { - for (int i = 0; i < 3; i++) { - long t0 = System.currentTimeMillis(); - test(l); - long t1 = System.currentTimeMillis(); - System.out.println("Level " + l + ": " + (t1 - t0) + " ms for " + COUNT + " lines"); - } - } - - } - - - private static void test(LogLevel level) { - for (int i = 0; i < COUNT; i++) { - log.log(level, "Message " + i); - } - } - -} diff --git a/src/net/sf/openrocket/utils/MotorCheck.java b/src/net/sf/openrocket/utils/MotorCheck.java deleted file mode 100644 index 5710486d..00000000 --- a/src/net/sf/openrocket/utils/MotorCheck.java +++ /dev/null @@ -1,89 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.file.motor.MotorLoader; -import net.sf.openrocket.motor.Manufacturer; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; - -public class MotorCheck { - - // Warn if less that this many points - public static final int WARN_POINTS = 6; - - - public static void main(String[] args) { - MotorLoader loader = new GeneralMotorLoader(); - - // Load files - for (String file : args) { - System.out.print("Checking " + file + "... "); - System.out.flush(); - - boolean ok = true; - - List<Motor> motors = null; - try { - InputStream stream = new FileInputStream(file); - motors = loader.load(stream, file); - stream.close(); - } catch (IOException e) { - System.out.println("ERROR: " + e.getMessage()); - e.printStackTrace(System.out); - ok = false; - } - - String base = file.split("_")[0]; - Manufacturer mfg = Manufacturer.getManufacturer(base); - - if (motors != null) { - if (motors.size() == 0) { - System.out.println("ERROR: File contained no motors"); - ok = false; - } else { - for (Motor motor : motors) { - ThrustCurveMotor m = (ThrustCurveMotor) motor; - double sum = 0; - sum += m.getAverageThrustEstimate(); - sum += m.getBurnTimeEstimate(); - sum += m.getTotalImpulseEstimate(); - // sum += m.getTotalTime(); - sum += m.getDiameter(); - sum += m.getLength(); - sum += m.getEmptyCG().weight; - sum += m.getEmptyCG().x; - sum += m.getLaunchCG().weight; - sum += m.getLaunchCG().x; - sum += m.getMaxThrustEstimate(); - if (Double.isInfinite(sum) || Double.isNaN(sum)) { - System.out.println("ERROR: Invalid motor values"); - ok = false; - } - - if (m.getManufacturer() != mfg) { - System.out.println("ERROR: Inconsistent manufacturer " + - m.getManufacturer() + " (file name indicates " + mfg - + ")"); - ok = false; - } - - int points = ((ThrustCurveMotor) m).getTimePoints().length; - if (points < WARN_POINTS) { - System.out.println("WARNING: Only " + points + " data points"); - ok = false; - } - } - } - } - - if (ok) { - System.out.println("OK"); - } - } - } -} diff --git a/src/net/sf/openrocket/utils/MotorCompare.java b/src/net/sf/openrocket/utils/MotorCompare.java deleted file mode 100644 index dcb16aeb..00000000 --- a/src/net/sf/openrocket/utils/MotorCompare.java +++ /dev/null @@ -1,336 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.file.motor.MotorLoader; -import net.sf.openrocket.motor.Manufacturer; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; - -public class MotorCompare { - - /** Maximum allowed difference in maximum thrust */ - private static final double MAX_THRUST_MARGIN = 0.30; - /** Maximum allowed difference in total impulse */ - private static final double TOTAL_IMPULSE_MARGIN = 0.20; - /** Maximum allowed difference in mass values */ - private static final double MASS_MARGIN = 0.20; - - /** Number of time points in thrust curve to compare */ - private static final int DIVISIONS = 100; - /** Maximum difference in thrust for a time point to be considered invalid */ - private static final double THRUST_MARGIN = 0.20; - /** Number of invalid time points allowed */ - private static final int ALLOWED_INVALID_POINTS = 20; - - /** Minimum number of thrust curve points allowed (incl. start and end points) */ - private static final int MIN_POINTS = 7; - - - public static void main(String[] args) throws IOException { - final double maxThrust; - final double maxTime; - int maxDelays; - int maxPoints; - int maxCommentLen; - - double min, max; - double diff; - - int[] goodness; - - boolean bad = false; - List<String> cause = new ArrayList<String>(); - - MotorLoader loader = new GeneralMotorLoader(); - List<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); - List<String> files = new ArrayList<String>(); - - // Load files - System.out.printf("Files :"); - for (String file : args) { - System.out.printf("\t%s", file); - List<Motor> m = null; - try { - InputStream stream = new FileInputStream(file); - m = loader.load(stream, file); - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - System.out.print("(ERR:" + e.getMessage() + ")"); - } - if (m != null) { - motors.addAll((List) m); - for (int i = 0; i < m.size(); i++) - files.add(file); - } - } - System.out.println(); - - compare(motors, files); - } - - - - - - public static void compare(List<ThrustCurveMotor> motors, List<String> files) { - final double maxThrust; - final double maxTime; - int maxDelays; - int maxPoints; - int maxCommentLen; - - double min, max; - double diff; - - int[] goodness; - - boolean bad = false; - List<String> cause = new ArrayList<String>(); - - - if (motors.size() == 0) { - System.err.println("No motors loaded."); - System.out.println("ERROR: No motors loaded.\n"); - return; - - } - - if (motors.size() == 1) { - System.out.println("Best (ONLY): " + files.get(0)); - System.out.println(); - return; - } - - final int n = motors.size(); - goodness = new int[n]; - - - for (String s : files) { - System.out.print("\t" + s); - } - System.out.println(); - - - // Designations - System.out.printf("Designation:"); - String des = motors.get(0).getDesignation(); - for (Motor m : motors) { - System.out.printf("\t%s", m.getDesignation()); - if (!m.getDesignation().equals(des)) { - cause.add("Designation"); - bad = true; - } - } - System.out.println(); - - // Manufacturers - System.out.printf("Manufacture:"); - Manufacturer mfg = motors.get(0).getManufacturer(); - for (ThrustCurveMotor m : motors) { - System.out.printf("\t%s", m.getManufacturer()); - if (m.getManufacturer() != mfg) { - cause.add("Manufacturer"); - bad = true; - } - } - System.out.println(); - - - // Max. thrust - max = 0; - min = Double.MAX_VALUE; - System.out.printf("Max.thrust :"); - for (Motor m : motors) { - double f = m.getMaxThrustEstimate(); - System.out.printf("\t%.2f", f); - max = Math.max(max, f); - min = Math.min(min, f); - } - diff = (max - min) / min; - if (diff > MAX_THRUST_MARGIN) { - bad = true; - cause.add("Max thrust"); - } - System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); - maxThrust = (min + max) / 2; - - - // Total time - max = 0; - min = Double.MAX_VALUE; - System.out.printf("Burn time :"); - for (Motor m : motors) { - double t = m.getBurnTimeEstimate(); - System.out.printf("\t%.2f", t); - max = Math.max(max, t); - min = Math.min(min, t); - } - diff = (max - min) / min; - System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); - maxTime = max; - - - // Total impulse - max = 0; - min = Double.MAX_VALUE; - System.out.printf("Impulse :"); - for (Motor m : motors) { - double f = m.getTotalImpulseEstimate(); - System.out.printf("\t%.2f", f); - max = Math.max(max, f); - min = Math.min(min, f); - } - diff = (max - min) / min; - if (diff > TOTAL_IMPULSE_MARGIN) { - bad = true; - cause.add("Total impulse"); - } - System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); - - - // Initial mass - max = 0; - min = Double.MAX_VALUE; - System.out.printf("Init mass :"); - for (Motor m : motors) { - double f = m.getLaunchCG().weight; - System.out.printf("\t%.2f", f * 1000); - max = Math.max(max, f); - min = Math.min(min, f); - } - diff = (max - min) / min; - if (diff > MASS_MARGIN) { - bad = true; - cause.add("Initial mass"); - } - System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); - - - // Empty mass - max = 0; - min = Double.MAX_VALUE; - System.out.printf("Empty mass :"); - for (Motor m : motors) { - double f = m.getEmptyCG().weight; - System.out.printf("\t%.2f", f * 1000); - max = Math.max(max, f); - min = Math.min(min, f); - } - diff = (max - min) / min; - if (diff > MASS_MARGIN) { - bad = true; - cause.add("Empty mass"); - } - System.out.printf("\t(discrepancy %.1f%%)\n", 100.0 * diff); - - - // Delays - maxDelays = 0; - System.out.printf("Delays :"); - for (ThrustCurveMotor m : motors) { - System.out.printf("\t%d", m.getStandardDelays().length); - maxDelays = Math.max(maxDelays, m.getStandardDelays().length); - } - System.out.println(); - - - // Data points - maxPoints = 0; - System.out.printf("Points :"); - for (Motor m : motors) { - System.out.printf("\t%d", ((ThrustCurveMotor) m).getTimePoints().length); - maxPoints = Math.max(maxPoints, ((ThrustCurveMotor) m).getTimePoints().length); - } - System.out.println(); - - - // Comment length - maxCommentLen = 0; - System.out.printf("Comment len:"); - for (Motor m : motors) { - System.out.printf("\t%d", m.getDescription().length()); - maxCommentLen = Math.max(maxCommentLen, m.getDescription().length()); - } - System.out.println(); - - - if (bad) { - String str = "ERROR: "; - for (int i = 0; i < cause.size(); i++) { - str += cause.get(i); - if (i < cause.size() - 1) - str += ", "; - } - str += " differs"; - System.out.println(str); - System.out.println(); - return; - } - - // Check consistency - // TODO: Does not check consistency - // int invalidPoints = 0; - // for (int i = 0; i < DIVISIONS; i++) { - // double t = maxTime * i / (DIVISIONS - 1); - // min = Double.MAX_VALUE; - // max = 0; - // // System.out.printf("%.2f:", t); - // for (Motor m : motors) { - // double f = m.getThrust(t); - // // System.out.printf("\t%.2f", f); - // min = Math.min(min, f); - // max = Math.max(max, f); - // } - // diff = (max - min) / maxThrust; - // // System.out.printf("\t(diff %.1f%%)\n", diff*100); - // if (diff > THRUST_MARGIN) - // invalidPoints++; - // } - // - // if (invalidPoints > ALLOWED_INVALID_POINTS) { - // System.out.println("ERROR: " + invalidPoints + "/" + DIVISIONS - // + " points have thrust differing over " + (THRUST_MARGIN * 100) + "%"); - // System.out.println(); - // return; - // } - - - // Check goodness - for (int i = 0; i < n; i++) { - ThrustCurveMotor m = motors.get(i); - if (m.getStandardDelays().length == maxDelays) - goodness[i] += 1000; - if (((ThrustCurveMotor) m).getTimePoints().length == maxPoints) - goodness[i] += 100; - if (m.getDescription().length() == maxCommentLen) - goodness[i] += 10; - if (files.get(i).matches(".*\\.[rR][sS][eE]$")) - goodness[i] += 1; - } - int best = 0; - for (int i = 1; i < n; i++) { - if (goodness[i] > goodness[best]) - best = i; - } - - - // Verify enough points - int pts = ((ThrustCurveMotor) motors.get(best)).getTimePoints().length; - if (pts < MIN_POINTS) { - System.out.println("WARNING: Best has only " + pts + " data points"); - } - - System.out.println("Best (" + goodness[best] + "): " + files.get(best)); - System.out.println(); - - - } - -} diff --git a/src/net/sf/openrocket/utils/MotorCompareAll.java b/src/net/sf/openrocket/utils/MotorCompareAll.java deleted file mode 100644 index f3a6f8a7..00000000 --- a/src/net/sf/openrocket/utils/MotorCompareAll.java +++ /dev/null @@ -1,59 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.text.Collator; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.file.motor.MotorLoader; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.util.Pair; - -public class MotorCompareAll { - - /* - * Usage: - * - * java MotorCompareAll *.eng *.rse - */ - public static void main(String[] args) throws IOException { - - Map<String, Pair<List<ThrustCurveMotor>, List<String>>> map = - new HashMap<String, Pair<List<ThrustCurveMotor>, List<String>>>(); - - MotorLoader loader = new GeneralMotorLoader(); - - for (String filename : args) { - - List<ThrustCurveMotor> motors = (List) loader.load(new FileInputStream(filename), filename); - - for (ThrustCurveMotor m : motors) { - String key = m.getManufacturer() + ":" + m.getDesignation(); - Pair<List<ThrustCurveMotor>, List<String>> pair = map.get(key); - if (pair == null) { - pair = new Pair<List<ThrustCurveMotor>, List<String>> - (new ArrayList<ThrustCurveMotor>(), new ArrayList<String>()); - map.put(key, pair); - } - pair.getU().add(m); - pair.getV().add(filename); - } - } - - Collator collator = Collator.getInstance(); - - List<String> keys = new ArrayList<String>(map.keySet()); - Collections.sort(keys, collator); - for (String basename : keys) { - Pair<List<ThrustCurveMotor>, List<String>> pair = map.get(basename); - System.err.println(basename + ": " + pair.getV()); - MotorCompare.compare(pair.getU(), pair.getV()); - } - } - -} diff --git a/src/net/sf/openrocket/utils/MotorCorrelation.java b/src/net/sf/openrocket/utils/MotorCorrelation.java deleted file mode 100644 index 3394a59e..00000000 --- a/src/net/sf/openrocket/utils/MotorCorrelation.java +++ /dev/null @@ -1,145 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.file.motor.MotorLoader; -import net.sf.openrocket.logging.LogLevel; -import net.sf.openrocket.models.atmosphere.AtmosphericConditions; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.MotorInstance; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.MathUtil; - -public class MotorCorrelation { - - /** - * Return a measure of motor similarity. The measure is a value between 0.0 and 1.0. - * The larger the value, the more similar the motor thrust curves are, for value 1.0 they - * are identical. - * <p> - * This method takes into account the thrust curve shape, average thrust, burn time and - * total impulse of the motor. The similarity is the minimum of all of these. - * - * @param motor1 the first motor - * @param motor2 the second motor - * @return the similarity of the two motors - */ - public static double similarity(Motor motor1, Motor motor2) { - double d; - - d = crossCorrelation(motor1, motor2); - d = Math.min(d, diff(motor1.getAverageThrustEstimate(), motor2.getAverageThrustEstimate())); - d = Math.min(d, 2 * diff(motor1.getBurnTimeEstimate(), motor2.getBurnTimeEstimate())); - d = Math.min(d, diff(motor1.getTotalImpulseEstimate(), motor2.getTotalImpulseEstimate())); - - return d; - } - - - private static double diff(double a, double b) { - double min = Math.min(a, b); - double max = Math.max(a, b); - - if (MathUtil.equals(max, 0)) - return 1.0; - return min / max; - } - - - /** - * Compute the cross-correlation of the thrust curves of the two motors. The result is - * a double between 0 and 1 (inclusive). The closer the return value is to one the more - * similar the thrust curves are. - * - * @param motor1 the first motor. - * @param motor2 the second motor. - * @return the scaled cross-correlation of the two thrust curves. - */ - public static double crossCorrelation(Motor motor1, Motor motor2) { - MotorInstance m1 = motor1.getInstance(); - MotorInstance m2 = motor2.getInstance(); - - AtmosphericConditions cond = new AtmosphericConditions(); - - double t; - double auto1 = 0; - double auto2 = 0; - double cross = 0; - for (t = 0; t < 1000; t += 0.01) { - m1.step(t, 0, cond); - m2.step(t, 0, cond); - - double t1 = m1.getThrust(); - double t2 = m2.getThrust(); - - if (t1 < 0 || t2 < 0) { - throw new BugException("Negative thrust, t1=" + t1 + " t2=" + t2); - } - - auto1 += t1 * t1; - auto2 += t2 * t2; - cross += t1 * t2; - } - - double auto = Math.max(auto1, auto2); - - if (MathUtil.equals(auto, 0)) { - return 1.0; - } - - return cross / auto; - } - - - - - public static void main(String[] args) { - Application.setLogOutputLevel(LogLevel.WARN); - - MotorLoader loader = new GeneralMotorLoader(); - List<Motor> motors = new ArrayList<Motor>(); - List<String> files = new ArrayList<String>(); - - // Load files - for (String file : args) { - List<Motor> m = null; - try { - InputStream stream = new FileInputStream(file); - m = loader.load(stream, file); - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - System.exit(1); - } - if (m != null) { - motors.addAll(m); - for (int i = 0; i < m.size(); i++) - files.add(file); - } - } - - // Output motor digests - final int count = motors.size(); - for (int i = 0; i < count; i++) { - System.out.println(files.get(i) + ": " + MotorDigest.digestMotor((ThrustCurveMotor) motors.get(i))); - } - - // Cross-correlate every pair - for (int i = 0; i < count; i++) { - for (int j = i + 1; j < count; j++) { - System.out.println(files.get(i) + " " + files.get(j) + " : " + - crossCorrelation(motors.get(i), motors.get(j))); - } - } - - } - -} \ No newline at end of file diff --git a/src/net/sf/openrocket/utils/MotorDigester.java b/src/net/sf/openrocket/utils/MotorDigester.java deleted file mode 100644 index 754d4075..00000000 --- a/src/net/sf/openrocket/utils/MotorDigester.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.file.motor.MotorLoader; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.ThrustCurveMotor; - -public class MotorDigester { - - public static void main(String[] args) { - final MotorLoader loader = new GeneralMotorLoader(); - final boolean printFileNames; - - if (args.length == 0) { - System.err.println("Usage: MotorDigester <files>"); - printFileNames = false; - System.exit(1); - } else if (args.length == 1) { - printFileNames = false; - } else { - printFileNames = true; - } - - - for (String file : args) { - - List<Motor> motors = null; - try { - InputStream stream = new FileInputStream(file); - motors = loader.load(stream, file); - stream.close(); - } catch (IOException e) { - System.err.println("ERROR: " + e.getMessage()); - e.printStackTrace(); - continue; - } - - for (Motor m : motors) { - if (!(m instanceof ThrustCurveMotor)) { - System.err.println(file + ": Not ThrustCurveMotor: " + m); - continue; - } - - String digest = MotorDigest.digestMotor((ThrustCurveMotor) m); - if (printFileNames) { - System.out.print(file + ": "); - } - System.out.println(digest); - } - } - - } - -} diff --git a/src/net/sf/openrocket/utils/MotorPlot.java b/src/net/sf/openrocket/utils/MotorPlot.java deleted file mode 100644 index f3b74310..00000000 --- a/src/net/sf/openrocket/utils/MotorPlot.java +++ /dev/null @@ -1,178 +0,0 @@ -package net.sf.openrocket.utils; - -import java.awt.Color; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFrame; -import javax.swing.JPanel; -import javax.swing.JTabbedPane; -import javax.swing.JTextArea; -import javax.swing.SwingUtilities; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.gui.util.GUIUtil; -import net.sf.openrocket.l10n.Translator; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.ThrustCurveMotor; -import net.sf.openrocket.startup.Application; - -import org.jfree.chart.ChartFactory; -import org.jfree.chart.ChartPanel; -import org.jfree.chart.JFreeChart; -import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; -import org.jfree.data.xy.XYSeries; -import org.jfree.data.xy.XYSeriesCollection; - -public class MotorPlot extends JDialog { - - private int selected = -1; - private static final Translator trans = Application.getTranslator(); - - public MotorPlot(List<String> filenames, List<ThrustCurveMotor> motors) { - //// Motor plot - super((JFrame) null, trans.get("MotorPlot.title.Motorplot"), true); - - JTabbedPane tabs = new JTabbedPane(); - for (int i = 0; i < filenames.size(); i++) { - JPanel pane = createPlotPanel((ThrustCurveMotor) motors.get(i)); - - //// Select button - JButton button = new JButton(trans.get("MotorPlot.but.Select")); - final int number = i; - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - selected = number; - MotorPlot.this.setVisible(false); - } - }); - pane.add(button, "wrap", 0); - - tabs.addTab(filenames.get(i), pane); - } - - this.add(tabs); - - this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); - this.setLocationByPlatform(true); - this.validate(); - this.pack(); - } - - - private JPanel createPlotPanel(ThrustCurveMotor motor) { - JPanel panel = new JPanel(new MigLayout()); - - - XYSeries series = new XYSeries("", false, true); - double[] time = motor.getTimePoints(); - double[] thrust = motor.getThrustPoints(); - - for (int i = 0; i < time.length; i++) { - series.add(time[i], thrust[i]); - } - - // Create the chart using the factory to get all default settings - JFreeChart chart = ChartFactory.createXYLineChart( - //// Motor thrust curve - trans.get("MotorPlot.Chart.Motorthrustcurve"), - //// Time / s - trans.get("MotorPlot.Chart.Time"), - //// Thrust / N - trans.get("MotorPlot.Chart.Thrust"), - new XYSeriesCollection(series), - PlotOrientation.VERTICAL, - true, - true, - false - ); - - ((XYLineAndShapeRenderer) chart.getXYPlot().getRenderer()).setShapesVisible(true); - - ChartPanel chartPanel = new ChartPanel(chart, - false, // properties - true, // save - false, // print - true, // zoom - true); // tooltips - chartPanel.setMouseWheelEnabled(true); - chartPanel.setEnforceFileExtensions(true); - chartPanel.setInitialDelay(500); - - chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - - panel.add(chartPanel, "grow, wrap para"); - - - JTextArea area = new JTextArea(5, 40); - StringBuilder sb = new StringBuilder(); - //// Designation: - sb.append("MotorPlot.txt.Designation" + " ").append(motor.getDesignation()).append(" "); - //// Manufacturer: - sb.append("MotorPlot.txt.Manufacturer" + " ").append(motor.getManufacturer()).append(" "); - //// Type: - sb.append("MotorPlot.txt.Type" + " ").append(motor.getMotorType()).append('\n'); - //// Delays: - sb.append("MotorPlot.txt.Delays" +" ").append(Arrays.toString(motor.getStandardDelays())).append('\n'); - //// Comment:\n - sb.append("MotorPlot.txt.Comment" + " ").append(motor.getDescription()); - area.setText(sb.toString()); - panel.add(area, "grow, wrap"); - - - return panel; - } - - - - public int getSelected() { - return selected; - } - - - public static void main(String[] args) throws IOException { - if (args.length == 0) { - System.err.println("MotorPlot <files>"); - System.exit(1); - } - - final List<String> filenames = new ArrayList<String>(); - final List<ThrustCurveMotor> motors = new ArrayList<ThrustCurveMotor>(); - - GeneralMotorLoader loader = new GeneralMotorLoader(); - for (String file : args) { - for (Motor m : loader.load(new FileInputStream(file), file)) { - filenames.add(file); - motors.add((ThrustCurveMotor) m); - } - } - - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - GUIUtil.setBestLAF(); - - MotorPlot plot = new MotorPlot(filenames, motors); - plot.setVisible(true); - } - - }); - - } - - - - -} diff --git a/src/net/sf/openrocket/utils/MotorPrinter.java b/src/net/sf/openrocket/utils/MotorPrinter.java deleted file mode 100644 index 0ca4e987..00000000 --- a/src/net/sf/openrocket/utils/MotorPrinter.java +++ /dev/null @@ -1,60 +0,0 @@ -package net.sf.openrocket.utils; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.List; - -import net.sf.openrocket.file.motor.GeneralMotorLoader; -import net.sf.openrocket.file.motor.MotorLoader; -import net.sf.openrocket.motor.Motor; -import net.sf.openrocket.motor.MotorDigest; -import net.sf.openrocket.motor.ThrustCurveMotor; - -public class MotorPrinter { - - public static void main(String[] args) throws IOException { - - MotorLoader loader = new GeneralMotorLoader(); - - System.out.println(); - for (String arg : args) { - InputStream stream = new FileInputStream(arg); - - List<Motor> motors = loader.load(stream, arg); - - System.out.println("*** " + arg + " ***"); - System.out.println(); - for (Motor motor : motors) { - ThrustCurveMotor m = (ThrustCurveMotor) motor; - System.out.println(" Manufacturer: " + m.getManufacturer()); - System.out.println(" Designation: " + m.getDesignation()); - System.out.println(" Delays: " + - Arrays.toString(m.getStandardDelays())); - System.out.printf(" Nominal time: %.2f s\n", m.getBurnTimeEstimate()); - // System.out.printf(" Total time: %.2f s\n", m.getTotalTime()); - System.out.printf(" Avg. thrust: %.2f N\n", m.getAverageThrustEstimate()); - System.out.printf(" Max. thrust: %.2f N\n", m.getMaxThrustEstimate()); - System.out.printf(" Total impulse: %.2f Ns\n", m.getTotalImpulseEstimate()); - System.out.println(" Diameter: " + m.getDiameter() * 1000 + " mm"); - System.out.println(" Length: " + m.getLength() * 1000 + " mm"); - System.out.println(" Digest: " + MotorDigest.digestMotor(m)); - - if (m instanceof ThrustCurveMotor) { - ThrustCurveMotor tc = (ThrustCurveMotor) m; - System.out.println(" Data points: " + tc.getTimePoints().length); - for (int i = 0; i < m.getTimePoints().length; i++) { - double time = m.getTimePoints()[i]; - double thrust = m.getThrustPoints()[i]; - System.out.printf(" t=%.3f F=%.3f\n", time, thrust); - } - } - - System.out.println(" Comment:"); - System.out.println(m.getDescription()); - System.out.println(); - } - } - } -} diff --git a/src/net/sf/openrocket/utils/RocksimConverter.java b/src/net/sf/openrocket/utils/RocksimConverter.java deleted file mode 100644 index 91530a30..00000000 --- a/src/net/sf/openrocket/utils/RocksimConverter.java +++ /dev/null @@ -1,85 +0,0 @@ -package net.sf.openrocket.utils; - -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.StorageOptions; -import net.sf.openrocket.file.RocketLoadException; -import net.sf.openrocket.file.RocketLoader; -import net.sf.openrocket.file.RocketSaver; -import net.sf.openrocket.file.openrocket.OpenRocketSaver; -import net.sf.openrocket.gui.util.SwingPreferences; -import net.sf.openrocket.l10n.ResourceBundleTranslator; -import net.sf.openrocket.logging.LogLevel; -import net.sf.openrocket.startup.Application; - -import java.io.File; -import java.io.IOException; -import java.util.Locale; - -/** - * Utility that loads Rocksim file formats and saves them in ORK format. - * File is saved with the .rkt extension replaced with .ork. - * - * Usage: - * java -cp OpenRocket.jar net.sf.openrocket.utils.RocksimConverter <files...> - * - * @author Sampo Niskanen <sampo.niskanen@iki.fi> - */ -public class RocksimConverter { - - - public static void main(String[] args) { - - setup(); - - RocketLoader loader = new net.sf.openrocket.file.rocksim.importt.RocksimLoader(); - RocketSaver saver = new OpenRocketSaver(); - - for (String inputFile : args) { - System.out.println("Converting " + inputFile + "..."); - - if (!inputFile.matches(".*\\.[rR][kK][tT]$")) { - System.err.println("ERROR: File '" + inputFile + "' does not end in .rkt, skipping."); - continue; - } - - String outputFile = inputFile.replaceAll("\\.[rR][kK][tT]$", ".ork"); - - File input = new File(inputFile); - File output = new File(outputFile); - - if (!input.isFile()) { - System.err.println("ERROR: File '" + inputFile + "' does not exist, skipping."); - continue; - } - if (output.exists()) { - System.err.println("ERROR: File '" + outputFile + "' already exists, skipping."); - continue; - } - - try { - StorageOptions opts = new StorageOptions(); - opts.setCompressionEnabled(true); - opts.setSimulationTimeSkip(StorageOptions.SIMULATION_DATA_NONE); - opts.setExplicitlySet(true); - - OpenRocketDocument document = loader.load(input); - saver.save(output, document, opts); - - } catch (RocketLoadException e) { - System.err.println("ERROR: Error loading '" + inputFile + "': " + e.getMessage()); - } catch (IOException e) { - System.err.println("ERROR: Error saving '" + outputFile + "': " + e.getMessage()); - } - - } - - } - - private static void setup() { - Locale.setDefault(Locale.US); - Application.setBaseTranslator(new ResourceBundleTranslator("l10n.messages")); - - Application.setLogOutputLevel(LogLevel.WARN); - Application.setPreferences(new SwingPreferences()); - } -} diff --git a/src/net/sf/openrocket/utils/TestFunctionOptimizer.java b/src/net/sf/openrocket/utils/TestFunctionOptimizer.java deleted file mode 100644 index e5f9da71..00000000 --- a/src/net/sf/openrocket/utils/TestFunctionOptimizer.java +++ /dev/null @@ -1,119 +0,0 @@ -package net.sf.openrocket.utils; - -import net.sf.openrocket.optimization.general.Function; -import net.sf.openrocket.optimization.general.FunctionOptimizer; -import net.sf.openrocket.optimization.general.OptimizationController; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.ParallelExecutorCache; -import net.sf.openrocket.optimization.general.ParallelFunctionCache; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer; - - - - -public class TestFunctionOptimizer { - - private static final int LOOP_COUNT = 1000000; - - private volatile int evaluations = 0; - private volatile int aborted = 0; - private volatile int stepCount = 0; - - - - private void go(final ParallelFunctionCache functionCache, - final FunctionOptimizer optimizer, final Point optimum, final int maxSteps) throws OptimizationException { - - Function function = new Function() { - @Override - public double evaluate(Point p) throws InterruptedException { - if (loop(LOOP_COUNT)) { - evaluations++; - return p.sub(optimum).length2(); - } else { - aborted++; - return Double.NaN; - } - } - }; - - OptimizationController control = new OptimizationController() { - - @Override - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - stepCount++; - // System.out.println("CSV " + count + ", " + evaluations + ", " + newPoint.sub(optimum).length()); - // System.out.println("Steps: " + count + " Function evaluations: " + evaluations); - // System.out.println("Distance: " + newPoint.sub(optimum).length() + " " + newPoint + " value=" + newValue); - return stepCount < maxSteps; - } - }; - ; - - functionCache.setFunction(function); - optimizer.setFunctionCache(functionCache); - optimizer.optimize(new Point(optimum.dim(), 0.5), control); - System.err.println("Result: " + optimizer.getOptimumPoint() + " value=" + optimizer.getOptimumValue()); - System.err.println("Steps: " + stepCount + " Evaluations: " + evaluations); - } - - - public static double counter; - - private static boolean loop(int count) { - counter = 1.0; - for (int i = 0; i < count; i++) { - counter += Math.sin(counter); - if (i % 1024 == 0) { - if (Thread.interrupted()) { - return false; - } - } - } - return true; - } - - - public static void main(String[] args) throws InterruptedException, OptimizationException { - - System.err.println("Number of processors: " + Runtime.getRuntime().availableProcessors()); - - for (int i = 0; i < 20; i++) { - long t0 = System.currentTimeMillis(); - loop(LOOP_COUNT); - long t1 = System.currentTimeMillis(); - System.err.println("Loop delay at startup: " + (t1 - t0) + "ms"); - } - System.err.println(); - - for (int threadCount = 1; threadCount <= 10; threadCount++) { - - System.err.println("THREAD COUNT: " + threadCount); - TestFunctionOptimizer test = new TestFunctionOptimizer(); - - ParallelExecutorCache executor = new ParallelExecutorCache(threadCount); - MultidirectionalSearchOptimizer optimizer = new MultidirectionalSearchOptimizer(); - long t0 = System.currentTimeMillis(); - test.go(executor, optimizer, new Point(0.2, 0.3, 0.85), 30); - long t1 = System.currentTimeMillis(); - - System.err.println("Optimization took " + (t1 - t0) + "ms"); - System.err.println("" + test.stepCount + " steps, " + test.evaluations + - " function evaluations, " + test.aborted + " aborted evaluations"); - System.err.println("Statistics: " + optimizer.getStatistics()); - - executor.getExecutor().shutdownNow(); - Thread.sleep(1000); - - t0 = System.currentTimeMillis(); - loop(LOOP_COUNT); - t1 = System.currentTimeMillis(); - System.err.println("Loop delay afterwards: " + (t1 - t0) + "ms"); - System.err.println(); - } - } - - - -} diff --git a/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java b/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java deleted file mode 100644 index e09a0ee8..00000000 --- a/src/net/sf/openrocket/utils/TestFunctionOptimizerLoop.java +++ /dev/null @@ -1,101 +0,0 @@ -package net.sf.openrocket.utils; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import net.sf.openrocket.optimization.general.Function; -import net.sf.openrocket.optimization.general.FunctionOptimizer; -import net.sf.openrocket.optimization.general.OptimizationController; -import net.sf.openrocket.optimization.general.OptimizationException; -import net.sf.openrocket.optimization.general.ParallelExecutorCache; -import net.sf.openrocket.optimization.general.Point; -import net.sf.openrocket.optimization.general.multidim.MultidirectionalSearchOptimizer; -import net.sf.openrocket.util.MathUtil; - - -public class TestFunctionOptimizerLoop { - - private static final double PRECISION = 0.01; - - private Point optimum; - private int stepCount = 0; - private int evaluations = 0; - - - - private void go(final FunctionOptimizer optimizer, final Point optimum, final int maxSteps, ExecutorService executor) throws OptimizationException { - - Function function = new Function() { - @Override - public double evaluate(Point p) throws InterruptedException { - evaluations++; - return p.sub(optimum).length2(); - } - }; - - OptimizationController control = new OptimizationController() { - - @Override - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - stepCount++; - if (stepCount % 1000 == 0) { - System.err.println("WARNING: Over " + stepCount + " steps required for optimum=" + optimum + - " position=" + newPoint); - } - double distance = newPoint.sub(optimum).length(); - return distance >= PRECISION; - } - }; - ; - - ParallelExecutorCache cache = new ParallelExecutorCache(executor); - cache.setFunction(function); - optimizer.setFunctionCache(cache); - optimizer.optimize(new Point(optimum.dim(), 0.5), control); - } - - - public static void main(String[] args) throws OptimizationException { - - System.err.println("PRECISION = " + PRECISION); - - ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100)); - - for (int dim = 1; dim <= 10; dim++) { - - List<Integer> stepCount = new ArrayList<Integer>(); - List<Integer> functionCount = new ArrayList<Integer>(); - - MultidirectionalSearchOptimizer optimizer = new MultidirectionalSearchOptimizer(); - for (int count = 0; count < 200; count++) { - TestFunctionOptimizerLoop test = new TestFunctionOptimizerLoop(); - double[] point = new double[dim]; - for (int i = 0; i < dim; i++) { - point[i] = Math.random(); - } - // point[0] = 0.7; - test.go(optimizer, new Point(point), 20, executor); - stepCount.add(test.stepCount); - functionCount.add(test.evaluations); - } - - // System.err.println("StepCount = " + stepCount); - - System.out.printf("dim=%d Steps avg=%5.2f dev=%5.2f median=%.1f " + - "Evaluations avg=%5.2f dev=%5.2f median=%.1f\n", - dim, MathUtil.average(stepCount), MathUtil.stddev(stepCount), MathUtil.median(stepCount), - MathUtil.average(functionCount), MathUtil.stddev(functionCount), MathUtil.median(functionCount)); - System.out.println("stat: " + optimizer.getStatistics()); - - } - - executor.shutdownNow(); - } - - - -}