From: plaa Date: Sun, 7 Aug 2011 16:54:36 +0000 (+0000) Subject: major optimization updates X-Git-Tag: upstream/1.1.7^2~6 X-Git-Url: https://git.gag.com/?a=commitdiff_plain;h=8320c04afa30e2aa0150adc870d02abeedb01066;p=debian%2Fopenrocket major optimization updates git-svn-id: https://openrocket.svn.sourceforge.net/svnroot/openrocket/trunk@147 180e2498-e6e9-4542-8430-84ac67f01cd8 --- diff --git a/ChangeLog b/ChangeLog index 8baecf09..3b3ed8f1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,11 @@ +2011-08-07 Sampo Niskanen + + * Optimization implementation + +2011-07-29 Sampo Niskanen + + * [BUG] NaN in transition radius calculation + 2011-07-22 Sampo Niskanen * Released version 1.1.6. diff --git a/l10n/messages.properties b/l10n/messages.properties index 9cfea75d..8d71278d 100644 --- a/l10n/messages.properties +++ b/l10n/messages.properties @@ -79,6 +79,7 @@ error.writing.desc = An error occurred while writing to the file: ! Labels used in buttons of dialog windows +# TODO: Rename these to "btn.xxx" button.ok = OK button.cancel = Cancel button.close = Close @@ -415,6 +416,12 @@ SimExpPan.ExportingVar.desc3 = variables out of SimExpPan.Col.Variable = Variable SimExpPan.Col.Unit = Unit + +CsvOptionPanel.separator.space = SPACE +CsvOptionPanel.separator.tab = TAB + + + ! MotorPlot MotorPlot.title.Motorplot = Motor plot MotorPlot.but.Select = Select @@ -919,7 +926,7 @@ TCMotorSelPan.delayBox.None = None PlotDialog.title.Flightdataplot = Flight data plot PlotDialog.Chart.Simulatedflight = Simulated flight PlotDialog.CheckBox.Showdatapoints = Show data points -PlotDialog.lbl.Chart = Click+drag down+right to zoom in, up+left to zoom out +PlotDialog.lbl.Chart = Click and drag down+right to zoom in, up+left to zoom out ! "main" prefix is used for the main application dialog @@ -964,6 +971,8 @@ main.menu.analyze = Analyze main.menu.analyze.desc = Rocket analysis main.menu.analyze.componentAnalysis = Component analysis main.menu.analyze.componentAnalysis.desc = Analyze the rocket components separately +main.menu.analyze.optimization = Rocket optimization +main.menu.analyze.optimization.desc = General rocket design optimization main.menu.help = Help main.menu.help.desc = Information about OpenRocket @@ -1296,17 +1305,177 @@ EllipticalFinSet.Ellipticalfinset = Elliptical fin set ! Modifiers -optimization.modifier.nosecone.length = Nose cone length -optimization.modifier.nosecone.diameter = Nose cone diameter -optimization.modifier.nosecone.thickness = Nose cone thickness - -optimization.modifier.transition.length = Transition length -optimization.modifier.transition.forediameter = Transition fore diameter -optimization.modifier.transition.aftdiameter = Transition aft diameter -optimization.modifier.transition.thickness = Transition thickness - -optimization.modifier.bodytube.length = Body tube length -optimization.modifier.bodytube.outerDiameter = Body tube outer diameter -optimization.modifier.bodytube.innerDiameter = Body tube inner diameter -optimization.modifier.bodytube.thickness = Body tube thickness - +optimization.modifier.nosecone.length = Length +optimization.modifier.nosecone.length.desc = Optimize the nose cone length. +optimization.modifier.nosecone.diameter = Diameter +optimization.modifier.nosecone.diameter.desc = Optimize the nose cone base diameter. +optimization.modifier.nosecone.thickness = Thickness +optimization.modifier.nosecone.thickness.desc = Optimize the nose cone wall thickness. +optimization.modifier.nosecone.shapeparameter = Shape parameter +optimization.modifier.nosecone.shapeparameter.desc = Optimize the nose cone shape parameter. + +optimization.modifier.transition.length = Length +optimization.modifier.transition.length.desc = Optimize the transition length. +optimization.modifier.transition.forediameter = Fore diameter +optimization.modifier.transition.forediameter.desc = Optimize the transition fore diameter. +optimization.modifier.transition.aftdiameter = Aft diameter +optimization.modifier.transition.aftdiameter.desc = Optimize the transition aft diameter. +optimization.modifier.transition.thickness = Thickness +optimization.modifier.transition.thickness.desc = Optimize the transition wall thickness. +optimization.modifier.transition.shapeparameter = Shape parameter +optimization.modifier.transition.shapeparameter.desc = Optimize the transition shape parameter. + +optimization.modifier.bodytube.length = Length +optimization.modifier.bodytube.length.desc = Optimize the body tube length. +optimization.modifier.bodytube.outerDiameter = Outer diameter +optimization.modifier.bodytube.outerDiameter.desc = Optimize the body tube outer diameter while maintaining the wall thickness. +optimization.modifier.bodytube.thickness = Thickness +optimization.modifier.bodytube.thickness.desc = Optimize the body tube wall thickness. + +optimization.modifier.trapezoidfinset.rootChord = Root chord +optimization.modifier.trapezoidfinset.rootChord.desc = Optimize the root chord length of the fin set (length of fin at the rocket body). +optimization.modifier.trapezoidfinset.tipChord = Tip chord +optimization.modifier.trapezoidfinset.tipChord.desc = Optimize the tip chord length of the fin set (length of fin at outer edge). +optimization.modifier.trapezoidfinset.sweep = Sweep +optimization.modifier.trapezoidfinset.sweep.desc = Optimize the sweep of the fin set (distance that the leading edge sweeps backwards). +optimization.modifier.trapezoidfinset.height = Height +optimization.modifier.trapezoidfinset.height.desc = Optimize the height (semi-span) of the fin set. + +optimization.modifier.ellipticalfinset.length = Root chord +optimization.modifier.ellipticalfinset.length.desc = Optimize the root chord length of the fin set. +optimization.modifier.ellipticalfinset.height = Height +optimization.modifier.ellipticalfinset.height = Optimize the height (semi-span) of the fin set. + +optimization.modifier.finset.cant = Cant angle +optimization.modifier.finset.cant.desc = Optimize the cant angle of the fin set. +optimization.modifier.finset.position = Position +optimization.modifier.finset.position.desc = Optimize the fin set position along the rocket body. + +optimization.modifier.launchlug.length = Length +optimization.modifier.launchlug.length.desc = Optimize the launch lug length. +optimization.modifier.launchlug.outerDiameter = Outer diameter +optimization.modifier.launchlug.outerDiameter.desc = Optimize the outer diameter of the launch lug. +optimization.modifier.launchlug.thickness = Thickness +optimization.modifier.launchlug.thickness.desc = Optimize the launch lug thickness while keeping the outer diameter constant. +optimization.modifier.launchlug.position = Position +optimization.modifier.launchlug.position.desc = Optimize the launch lug position along the rocket body. + + +optimization.modifier.internalcomponent.position = Position +optimization.modifier.internalcomponent.position.desc = Optimize the position of the component relative to the parent component. + +optimization.modifier.masscomponent.mass = Mass +optimization.modifier.masscomponent.mass.desc = Optimize the mass of the mass component. + +optimization.modifier.parachute.diameter = Diameter +optimization.modifier.parachute.diameter.desc = Optimize the parachute canopy diameter. +optimization.modifier.parachute.coefficient = Drag coefficient +optimization.modifier.parachute.coefficient.desc = Optimize the drag coefficient of the parachute. Typical parachutes have a drag coefficient of about 0.8. + +optimization.modifier.streamer.length = Length +optimization.modifier.streamer.length.desc = Optimize the length of the streamer. +optimization.modifier.streamer.width = Width +optimization.modifier.streamer.width.desc = Optimize the width of the streamer. +optimization.modifier.streamer.aspectRatio = Aspect ratio +optimization.modifier.streamer.aspectRatio.desc = Optimize the aspect ratio of the streamer (length/width). You should NOT select streamer length or width at the same time with the aspect ratio. +optimization.modifier.streamer.coefficient = Drag coefficient +optimization.modifier.streamer.coefficient.desc = Optimize the drag coefficient of the streamer. + +optimization.modifier.recoverydevice.deployDelay = Deployment delay +optimization.modifier.recoverydevice.deployDelay.desc = Optimize the deployment delay of the recovery device. +optimization.modifier.recoverydevice.deployAltitude = Deployment altitude +optimization.modifier.recoverydevice.deployAltitude.desc = Optimize the deployment altitude of the recovery device. + +optimization.modifier.rocketcomponent.overrideMass = Override mass +optimization.modifier.rocketcomponent.overrideMass.desc = Optimize the overridden mass of the component. +optimization.modifier.rocketcomponent.overrideCG = Override CG +optimization.modifier.rocketcomponent.overrideCG.desc = Optimize the overridden center of gravity of the component. + +optimization.modifier.motormount.overhang = Motor overhang +optimization.modifier.motormount.overhang.desc = Optimize the motor overhang. +optimization.modifier.motormount.delay = Motor ignition delay +optimization.modifier.motormount.delay.desc = Optimize the motor ignition delay. + + + + +! General rocket design optimization dialog + +GeneralOptimizationDialog.title = Rocket optimization +GeneralOptimizationDialog.goal.maximize = Maximize value +GeneralOptimizationDialog.goal.minimize = Minimize value +GeneralOptimizationDialog.goal.seek = Seek value of +GeneralOptimizationDialog.btn.start = Start optimization +GeneralOptimizationDialog.btn.stop = Stop optimization +GeneralOptimizationDialog.lbl.paramsToOptimize = Parameters to optimize: +GeneralOptimizationDialog.btn.add = Add +GeneralOptimizationDialog.btn.add.ttip = Add the selected parameter to the optimization +GeneralOptimizationDialog.btn.remove = Remove +GeneralOptimizationDialog.btn.remove.ttip = Remove the selected parameter from the optimization +GeneralOptimizationDialog.btn.removeAll = Remove all +GeneralOptimizationDialog.btn.removeAll.ttip = Remove all parameters from the optimization +GeneralOptimizationDialog.lbl.availableParams = Available parameters: +GeneralOptimizationDialog.lbl.optimizationOpts = Optimization options +GeneralOptimizationDialog.lbl.optimizeSim = Optimize simulation: +GeneralOptimizationDialog.lbl.optimizeSim.ttip = Select which simulation to optimize +GeneralOptimizationDialog.lbl.optimizeValue = Optimized value: +GeneralOptimizationDialog.lbl.optimizeValue.ttip = Select what value is to be optimized +GeneralOptimizationDialog.lbl.optimizeGoal = Optimization goal: +GeneralOptimizationDialog.lbl.optimizeGoal.ttip = Select the goal of the optimization +GeneralOptimizationDialog.lbl.optimizeGoalValue.ttip = Custom value to seek +GeneralOptimizationDialog.lbl.requireStability = Required stability +GeneralOptimizationDialog.lbl.requireMinStability = Minimum stability: +GeneralOptimizationDialog.lbl.requireMinStability.ttip = Require a minimum static stability margin for the design +GeneralOptimizationDialog.lbl.requireMaxStability = Maximum stability: +GeneralOptimizationDialog.lbl.requireMaxStability.ttip = Require a maximum static stability margin for the design +GeneralOptimizationDialog.status.bestValue = Best value: +GeneralOptimizationDialog.status.bestValue.ttip = Best optimization value found so far. +GeneralOptimizationDialog.status.stepCount = Step count: +GeneralOptimizationDialog.status.stepCount.ttip = Number of optimization steps that have been performed. +GeneralOptimizationDialog.status.evalCount = Evaluations: +GeneralOptimizationDialog.status.evalCount.ttip = Total number of function evaluations (simulations) that have been performed. +GeneralOptimizationDialog.status.stepSize = Step size: +GeneralOptimizationDialog.status.stepSize.ttip = Current optimization step size (relative to the optimization parameter ranges) +GeneralOptimizationDialog.btn.plotPath = Plot path +GeneralOptimizationDialog.btn.plotPath.ttip = Plot the optimization path (one and two dimensional optimization only) +GeneralOptimizationDialog.btn.save = Save path +GeneralOptimizationDialog.btn.save.ttip = Save the results of the function evaluations (simulations) as a CSV file. +GeneralOptimizationDialog.btn.apply = Apply optimization +GeneralOptimizationDialog.btn.apply.ttip = Apply the optimization results to the rocket design +GeneralOptimizationDialog.btn.reset = Reset +GeneralOptimizationDialog.btn.reset.ttip = Reset the rocket design to the current rocket design +GeneralOptimizationDialog.btn.close = Close +GeneralOptimizationDialog.btn.close.ttip = Close the dialog without modifying the rocket design +GeneralOptimizationDialog.error.selectParams.text = First select some parameters to optimize from the available parameters. +GeneralOptimizationDialog.error.selectParams.title = Select optimization parameters +GeneralOptimizationDialog.error.optimizationFailure.text = The optimization failed to run: +GeneralOptimizationDialog.error.optimizationFailure.title = Optimization failed +GeneralOptimizationDialog.undoText = Apply optimization +GeneralOptimizationDialog.basicSimulationName = Basic simulation +GeneralOptimizationDialog.noSimulationName = No simulation +GeneralOptimizationDialog.table.col.parameter = Parameter +GeneralOptimizationDialog.table.col.current = Current +GeneralOptimizationDialog.table.col.min = Minimum +GeneralOptimizationDialog.table.col.max = Maximum +GeneralOptimizationDialog.export.header = Include header line +GeneralOptimizationDialog.export.header.ttip = Include a header line as the first line containing the field descriptions. +GeneralOptimizationDialog.export.stability = Stability + + +! Dialog for plotting optimization results +OptimizationPlotDialog.title = Optimization results +OptimizationPlotDialog.lbl.zoomInstructions = Click and drag down+right to zoom in, up+left to zoom out +OptimizationPlotDialog.plot1d.title = Optimization result +OptimizationPlotDialog.plot1d.series = Optimization result +OptimizationPlotDialog.plot2d.title = Optimization path +OptimizationPlotDialog.plot2d.path = Optimization path +OptimizationPlotDialog.plot2d.evals = Evaluations +OptimizationPlotDialog.plot.ttip.stability = Stability: + +! Optimization parameters +MaximumAltitudeParameter.name = Apogee altitude +MaximumVelocityParameter.name = Maximum velocity +MaximumAccelerationParameter.name = Maximum acceleration +StabilityParameter.name = Stability +GroundHitVelocityParameter.name = Ground hit speed +LandingDistanceParameter.name = Landing distance diff --git a/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java b/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java index 4b243389..ecafae1e 100644 --- a/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java +++ b/src/net/sf/openrocket/aerodynamics/BarrowmanCalculator.java @@ -12,10 +12,10 @@ 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.rocketcomponent.ExternalComponent.Finish; import net.sf.openrocket.util.Coordinate; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.PolyInterpolator; @@ -268,7 +268,7 @@ public class BarrowmanCalculator extends AbstractAerodynamicCalculator { // System.out.printf("constant Cf=%f ",Cf); } else if (Re < 5.39e5) { // Fully laminar - Cf = 1.328 / Math.sqrt(Re); + Cf = 1.328 / MathUtil.safeSqrt(Re); // System.out.printf("basic Cf=%f ",Cf); } else { // Transitional diff --git a/src/net/sf/openrocket/aerodynamics/FlightConditions.java b/src/net/sf/openrocket/aerodynamics/FlightConditions.java index 64e81455..45b9d20f 100644 --- a/src/net/sf/openrocket/aerodynamics/FlightConditions.java +++ b/src/net/sf/openrocket/aerodynamics/FlightConditions.java @@ -57,7 +57,7 @@ public class FlightConditions implements Cloneable, ChangeSource, Monitorable { * Sqrt(1 - M^2) for M<1 * Sqrt(M^2 - 1) for M>1 */ - private double beta = Math.sqrt(1 - mach * mach); + private double beta = MathUtil.safeSqrt(1 - mach * mach); /** Current roll rate. */ @@ -122,7 +122,7 @@ public class FlightConditions implements Cloneable, ChangeSource, Monitorable { */ public void setRefArea(double area) { refArea = area; - refLength = Math.sqrt(area / Math.PI) * 2; + refLength = MathUtil.safeSqrt(area / Math.PI) * 2; fireChangeEvent(); } @@ -237,9 +237,9 @@ public class FlightConditions implements Cloneable, ChangeSource, Monitorable { this.mach = mach; if (mach < 1) - this.beta = Math.sqrt(1 - mach * mach); + this.beta = MathUtil.safeSqrt(1 - mach * mach); else - this.beta = Math.sqrt(mach * mach - 1); + this.beta = MathUtil.safeSqrt(mach * mach - 1); fireChangeEvent(); } @@ -378,6 +378,7 @@ public class FlightConditions implements Cloneable, ChangeSource, Monitorable { * * @return the number of times this object has been modified since instantiation. */ + @Override public int getModID() { return modID + modIDadd + this.atmosphericConditions.getModID(); } diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java index 2b3d7a6a..bd8ee75e 100644 --- a/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java +++ b/src/net/sf/openrocket/aerodynamics/barrowman/FinSetCalc.java @@ -1,6 +1,6 @@ package net.sf.openrocket.aerodynamics.barrowman; -import static java.lang.Math.*; +import static java.lang.Math.pow; import static net.sf.openrocket.util.MathUtil.pow2; import java.util.Arrays; @@ -462,7 +462,7 @@ public class FinSetCalc extends RocketComponentCalc { double[] k3 = new double[n]; for (int i = 0; i < n; i++) { double M = CNA_SUPERSONIC + i * 0.1; - double beta = sqrt(M * M - 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)); @@ -487,7 +487,7 @@ public class FinSetCalc extends RocketComponentCalc { // Subsonic case if (mach <= CNA_SUBSONIC) { - return 2 * Math.PI * pow2(span) / (1 + sqrt(1 + (1 - pow2(mach)) * + return 2 * Math.PI * pow2(span) / (1 + MathUtil.safeSqrt(1 + (1 - pow2(mach)) * pow2(pow2(span) / (finArea * cosGamma)))) / ref; } @@ -501,7 +501,7 @@ public class FinSetCalc extends RocketComponentCalc { double subV, superV; double subD, superD; - double sq = sqrt(1 + (1 - pow2(CNA_SUBSONIC)) * pow2(span * span / (finArea * cosGamma))); + 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)); diff --git a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java b/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java index 42bce547..23178a33 100644 --- a/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java +++ b/src/net/sf/openrocket/aerodynamics/barrowman/SymmetricComponentCalc.java @@ -317,14 +317,14 @@ public class SymmetricComponentCalc extends RocketComponentCalc { p = (param - 0.5) * 4; } else { int1 = x34Interpolator; - int2 = calculateOgiveNoseInterpolator(0, 1 / Math.sqrt(1 + 4 * pow2(fineness))); + 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 / Math.sqrt(1 + 4 * pow2(fineness))); + int1 = calculateOgiveNoseInterpolator(0, 1 / MathUtil.safeSqrt(1 + 4 * pow2(fineness))); int2 = parabolic12Interpolator; p = param * 2; } else if (param <= 0.75) { @@ -429,7 +429,7 @@ public class SymmetricComponentCalc extends RocketComponentCalc { // 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 / Math.sqrt(m * m - 1))); + 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/document/OpenRocketDocument.java b/src/net/sf/openrocket/document/OpenRocketDocument.java index d0daa17a..c935bd29 100644 --- a/src/net/sf/openrocket/document/OpenRocketDocument.java +++ b/src/net/sf/openrocket/document/OpenRocketDocument.java @@ -26,14 +26,20 @@ import net.sf.openrocket.util.Icons; /** * Class describing an entire OpenRocket document, including a rocket and - * simulations. This class also handles undo/redo operations for the rocket structure. + * 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(); private static final Translator trans = Application.getTranslator(); - + /** * The minimum number of undo levels that are stored. */ @@ -49,6 +55,8 @@ public class OpenRocketDocument implements ComponentChangeListener { /** Whether an undo error has already been reported to the user */ private static boolean undoErrorReported = false; + + private final Rocket rocket; private final Configuration configuration; @@ -496,6 +504,27 @@ public class OpenRocketDocument implements ComponentChangeListener { } } + + + /** + * 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 addDocumentChangeListener(DocumentChangeListener listener) { diff --git a/src/net/sf/openrocket/document/Simulation.java b/src/net/sf/openrocket/document/Simulation.java index 709ce464..3c1e2c01 100644 --- a/src/net/sf/openrocket/document/Simulation.java +++ b/src/net/sf/openrocket/document/Simulation.java @@ -15,10 +15,10 @@ 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.GUISimulationConditions; 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; @@ -67,7 +67,7 @@ public class Simulation implements ChangeSource, Cloneable { /** The conditions to use */ // TODO: HIGH: Change to use actual conditions class?? - private GUISimulationConditions conditions; + private SimulationOptions options; private ArrayList simulationListeners = new ArrayList(); @@ -83,7 +83,7 @@ public class Simulation implements ChangeSource, Cloneable { /** The conditions actually used in the previous simulation, or null */ - private GUISimulationConditions simulatedConditions = null; + private SimulationOptions simulatedConditions = null; private String simulatedMotors = null; private FlightData simulatedData = null; private int simulatedRocketID = -1; @@ -99,14 +99,14 @@ public class Simulation implements ChangeSource, Cloneable { this.rocket = rocket; this.status = Status.NOT_SIMULATED; - conditions = new GUISimulationConditions(rocket); - conditions.setMotorConfigurationID( + options = new SimulationOptions(rocket); + options.setMotorConfigurationID( rocket.getDefaultConfiguration().getMotorConfigurationID()); - conditions.addChangeListener(new ConditionListener()); + options.addChangeListener(new ConditionListener()); } - public Simulation(Rocket rocket, Status status, String name, GUISimulationConditions conditions, + public Simulation(Rocket rocket, Status status, String name, SimulationOptions options, List listeners, FlightData data) { if (rocket == null) @@ -115,8 +115,8 @@ public class Simulation implements ChangeSource, Cloneable { throw new IllegalArgumentException("status cannot be null"); if (name == null) throw new IllegalArgumentException("name cannot be null"); - if (conditions == null) - throw new IllegalArgumentException("conditions cannot be null"); + if (options == null) + throw new IllegalArgumentException("options cannot be null"); this.rocket = rocket; @@ -130,8 +130,8 @@ public class Simulation implements ChangeSource, Cloneable { this.name = name; - this.conditions = conditions; - conditions.addChangeListener(new ConditionListener()); + this.options = options; + options.addChangeListener(new ConditionListener()); if (listeners != null) { this.simulationListeners.addAll(listeners); @@ -141,7 +141,7 @@ public class Simulation implements ChangeSource, Cloneable { if (data != null && this.status != Status.NOT_SIMULATED) { simulatedData = data; if (this.status == Status.LOADED) { - simulatedConditions = conditions.clone(); + simulatedConditions = options.clone(); simulatedRocketID = rocket.getModID(); } } @@ -169,21 +169,21 @@ public class Simulation implements ChangeSource, Cloneable { public Configuration getConfiguration() { mutex.verify(); Configuration c = new Configuration(rocket); - c.setMotorConfigurationID(conditions.getMotorConfigurationID()); + c.setMotorConfigurationID(options.getMotorConfigurationID()); c.setAllStages(); return c; } /** - * Returns the simulation conditions attached to this simulation. The conditions + * 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 GUISimulationConditions getConditions() { + public SimulationOptions getOptions() { mutex.verify(); - return conditions; + return options; } @@ -245,7 +245,7 @@ public class Simulation implements ChangeSource, Cloneable { if (status == Status.UPTODATE || status == Status.LOADED) { if (rocket.getFunctionalModID() != simulatedRocketID || - !conditions.equals(simulatedConditions)) + !options.equals(simulatedConditions)) return Status.OUTDATED; } @@ -277,11 +277,9 @@ public class Simulation implements ChangeSource, Cloneable { throw new IllegalStateException("Cannot instantiate simulator.", e); } catch (IllegalAccessException e) { throw new IllegalStateException("Cannot access simulator instance?! BUG!", e); - } catch (NullPointerException e) { - throw new IllegalStateException("Simulator null", e); } - SimulationConditions simulationConditions = conditions.toSimulationConditions(); + SimulationConditions simulationConditions = options.toSimulationConditions(); for (SimulationListener l : additionalListeners) { simulationConditions.getSimulationListenerList().add(l); } @@ -306,7 +304,7 @@ public class Simulation implements ChangeSource, Cloneable { 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 = conditions.clone(); + simulatedConditions = options.clone(); simulatedMotors = getConfiguration().getMotorConfigurationDescription(); simulatedRocketID = rocket.getFunctionalModID(); @@ -324,7 +322,7 @@ public class Simulation implements ChangeSource, Cloneable { * * @return the conditions used in the previous simulation, or null. */ - public GUISimulationConditions getSimulatedConditions() { + public SimulationOptions getSimulatedConditions() { mutex.verify(); return simulatedConditions; } @@ -373,6 +371,7 @@ public class Simulation implements ChangeSource, Cloneable { /** * 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. @@ -385,7 +384,7 @@ public class Simulation implements ChangeSource, Cloneable { copy.mutex = SafetyMutex.newInstance(); copy.status = Status.NOT_SIMULATED; - copy.conditions = this.conditions.clone(); + copy.options = this.options.clone(); copy.simulationListeners = this.simulationListeners.clone(); copy.listeners = new ArrayList(); copy.simulatedConditions = null; @@ -416,7 +415,7 @@ public class Simulation implements ChangeSource, Cloneable { Simulation copy = new Simulation(newRocket); copy.name = this.name; - copy.conditions.copyFrom(this.conditions); + copy.options.copyFrom(this.options); copy.simulationListeners = this.simulationListeners.clone(); copy.simulationStepperClass = this.simulationStepperClass; copy.aerodynamicCalculatorClass = this.aerodynamicCalculatorClass; diff --git a/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java b/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java index d6cf6160..7719e0e2 100644 --- a/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java +++ b/src/net/sf/openrocket/file/openrocket/OpenRocketLoader.java @@ -69,7 +69,7 @@ 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.GUISimulationConditions; +import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.util.BugException; @@ -1257,12 +1257,12 @@ class SingleSimulationHandler extends ElementHandler { status = Simulation.Status.OUTDATED; } - GUISimulationConditions conditions; + SimulationOptions conditions; if (conditionHandler != null) { conditions = conditionHandler.getConditions(); } else { warnings.add("Simulation conditions not defined, using defaults."); - conditions = new GUISimulationConditions(doc.getRocket()); + conditions = new SimulationOptions(doc.getRocket()); } if (name == null) @@ -1284,14 +1284,14 @@ class SingleSimulationHandler extends ElementHandler { class SimulationConditionsHandler extends ElementHandler { - private GUISimulationConditions conditions; + private SimulationOptions conditions; private AtmosphereHandler atmosphereHandler; public SimulationConditionsHandler(Rocket rocket) { - conditions = new GUISimulationConditions(rocket); + conditions = new SimulationOptions(rocket); } - public GUISimulationConditions getConditions() { + public SimulationOptions getConditions() { return conditions; } @@ -1418,7 +1418,7 @@ class AtmosphereHandler extends ElementHandler { } - public void storeSettings(GUISimulationConditions cond, WarningSet warnings) { + public void storeSettings(SimulationOptions cond, WarningSet warnings) { if (!Double.isNaN(pressure)) { cond.setLaunchPressure(pressure); } diff --git a/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java b/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java index 85085259..bf237284 100644 --- a/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java +++ b/src/net/sf/openrocket/file/openrocket/OpenRocketSaver.java @@ -25,7 +25,7 @@ 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.GUISimulationConditions; +import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.MathUtil; @@ -291,7 +291,7 @@ public class OpenRocketSaver extends RocketSaver { private void saveSimulation(Simulation simulation, double timeSkip) throws IOException { - GUISimulationConditions cond = simulation.getConditions(); + SimulationOptions cond = simulation.getOptions(); writeln(""); indent++; diff --git a/src/net/sf/openrocket/gui/adaptors/BooleanModel.java b/src/net/sf/openrocket/gui/adaptors/BooleanModel.java index a88f4b89..4e877aff 100644 --- a/src/net/sf/openrocket/gui/adaptors/BooleanModel.java +++ b/src/net/sf/openrocket/gui/adaptors/BooleanModel.java @@ -37,17 +37,21 @@ import net.sf.openrocket.util.Reflection; * * @author Sampo Niskanen */ - public class BooleanModel extends AbstractAction implements ChangeListener, 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(); @@ -61,6 +65,34 @@ public class BooleanModel extends AbstractAction implements ChangeListener, Inva 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; @@ -109,24 +141,41 @@ public class BooleanModel extends AbstractAction implements ChangeListener, Inva } public boolean getValue() { - 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); + + 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); - 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); + + 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); } } @@ -271,7 +320,11 @@ public class BooleanModel extends AbstractAction implements ChangeListener, Inva @Override public String toString() { if (toString == null) { - toString = "BooleanModel[" + source.getClass().getSimpleName() + ":" + valueName + "]"; + 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/DoubleModel.java b/src/net/sf/openrocket/gui/adaptors/DoubleModel.java index ce81e107..e1fc3071 100644 --- a/src/net/sf/openrocket/gui/adaptors/DoubleModel.java +++ b/src/net/sf/openrocket/gui/adaptors/DoubleModel.java @@ -261,7 +261,7 @@ public class DoubleModel implements ChangeListener, ChangeSource, Invalidatable // Use quadratic scale // Further solution of the quadratic equation // a*x^2 + b*x + c-value == 0 - x = (Math.sqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2); + x = (MathUtil.safeSqrt(quad1 * quad1 - 4 * quad2 * (quad0 - value)) - quad1) / (2 * quad2); } return (int) (x * MAX); } diff --git a/src/net/sf/openrocket/gui/components/CsvOptionPanel.java b/src/net/sf/openrocket/gui/components/CsvOptionPanel.java new file mode 100644 index 00000000..b856e87c --- /dev/null +++ b/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.util.Prefs; + +/** + * 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(Prefs.getString(Prefs.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(Prefs.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(Prefs.getString(Prefs.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() { + Prefs.putString(Prefs.EXPORT_FIELD_SEPARATOR, getFieldSeparator()); + Prefs.putString(Prefs.EXPORT_COMMENT_CHARACTER, getCommentCharacter()); + for (int i = 0; i < options.length; i++) { + Prefs.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 index 616b3adc..1e37bb6f 100644 --- a/src/net/sf/openrocket/gui/components/DescriptionArea.java +++ b/src/net/sf/openrocket/gui/components/DescriptionArea.java @@ -16,14 +16,33 @@ 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); } diff --git a/src/net/sf/openrocket/gui/components/DoubleCellEditor.java b/src/net/sf/openrocket/gui/components/DoubleCellEditor.java new file mode 100644 index 00000000..6ff86822 --- /dev/null +++ b/src/net/sf/openrocket/gui/components/DoubleCellEditor.java @@ -0,0 +1,42 @@ +package net.sf.openrocket.gui.components; + +import java.awt.Component; + +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 Object getCellEditorValue() { + return model.getValue(); + } + +} diff --git a/src/net/sf/openrocket/gui/components/SimulationExportPanel.java b/src/net/sf/openrocket/gui/components/SimulationExportPanel.java index 62f79d6d..cd73c5f9 100644 --- a/src/net/sf/openrocket/gui/components/SimulationExportPanel.java +++ b/src/net/sf/openrocket/gui/components/SimulationExportPanel.java @@ -8,16 +8,12 @@ import java.util.Arrays; import javax.swing.BorderFactory; import javax.swing.JButton; -import javax.swing.JCheckBox; -import javax.swing.JComboBox; import javax.swing.JFileChooser; 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.filechooser.FileFilter; import javax.swing.table.AbstractTableModel; import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; @@ -32,32 +28,20 @@ import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; +import net.sf.openrocket.util.FileHelper; import net.sf.openrocket.util.GUIUtil; import net.sf.openrocket.util.Prefs; import net.sf.openrocket.util.SaveCSVWorker; 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 FileFilter CSV_FILE_FILTER = new FileFilter() { - @Override - public String getDescription() { - //// Comma Separated Files (*.csv) - return trans.get("SimExpPan.desc"); - } - @Override - public boolean accept(File f) { - if (f.isDirectory()) - return true; - String name = f.getName().toLowerCase(); - return name.endsWith(".csv"); - } - }; - - + 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; @@ -70,38 +54,32 @@ public class SimulationExportPanel extends JPanel { private final FlightDataType[] types; private final Unit[] units; - private final JComboBox fieldSeparator; - private final JCheckBox simulationComments; - private final JCheckBox fieldNameComments; - private final JCheckBox eventComments; - private final JComboBox commentCharacter; + private final CsvOptionPanel csvOptions; public SimulationExportPanel(Simulation sim) { super(new MigLayout("fill, flowy")); - - JLabel label; + JPanel panel; JButton button; - String tip; - + this.simulation = sim; // TODO: MEDIUM: Only exports primary branch final FlightData data = simulation.getSimulatedData(); - + // Check that data exists - if (data == null || data.getBranchCount() == 0 || + 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); @@ -112,16 +90,16 @@ public class SimulationExportPanel extends JPanel { 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, + table.setDefaultRenderer(Object.class, new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Object.class))); - table.setDefaultRenderer(Boolean.class, + table.setDefaultRenderer(Boolean.class, new SelectionBackgroundCellRenderer(table.getDefaultRenderer(Boolean.class))); table.setRowSelectionAllowed(false); table.setColumnSelectionAllowed(false); @@ -146,7 +124,7 @@ public class SimulationExportPanel extends JPanel { col = columnModel.getColumn(2); col.setPreferredWidth(100); - + table.addMouseListener(new GUIUtil.BooleanTableClickListener(table)); // Add table @@ -174,85 +152,31 @@ public class SimulationExportPanel extends JPanel { }); 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"); - // 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")); - //// The string used to separate the fields in the exported file.
- //// Use ',' for a Comma Separated Values (CSV) file. - tip = trans.get("SimExpPan.lbl.longA1") + - trans.get("SimExpPan.lbl.longA2"); - label.setToolTipText(tip); - panel.add(label); - - fieldSeparator = new JComboBox(new String[] { ",", ";", SPACE, TAB }); - fieldSeparator.setEditable(true); - fieldSeparator.setSelectedItem(Prefs.getString(Prefs.EXPORT_FIELD_SEPARATOR, - ",")); - fieldSeparator.setToolTipText(tip); - panel.add(fieldSeparator); - - this.add(panel, "spany, split, growx 1"); - - - - - // Comments separator panel - panel = new JPanel(new MigLayout("fill")); - //// Comments - panel.setBorder(BorderFactory.createTitledBorder(trans.get("SimExpPan.border.Comments"))); - - //// Include simulation description - simulationComments = new JCheckBox(trans.get("SimExpPan.checkbox.Includesimudesc")); - //// Include a comment at the beginning of the file describing the simulation. - simulationComments.setToolTipText(trans.get("SimExpPan.checkbox.ttip.Includesimudesc")); - simulationComments.setSelected(Prefs.getBoolean(Prefs.EXPORT_SIMULATION_COMMENT, - true)); - panel.add(simulationComments, "wrap"); - - //// Include field descriptions - fieldNameComments = new JCheckBox(trans.get("SimExpPan.checkbox.Includefielddesc")); - //// Include a comment line with the descriptions of the exported variables. - fieldNameComments.setToolTipText(trans.get("SimExpPan.checkbox.ttip.Includefielddesc")); - fieldNameComments.setSelected(Prefs.getBoolean(Prefs.EXPORT_FIELD_NAME_COMMENT, true)); - panel.add(fieldNameComments, "wrap"); - - - eventComments = new JCheckBox(trans.get("SimExpPan.checkbox.Incflightevents")); - eventComments.setToolTipText(trans.get("SimExpPan.checkbox.ttip.Incflightevents")); - eventComments.setSelected(Prefs.getBoolean(Prefs.EXPORT_EVENT_COMMENTS, true)); - panel.add(eventComments, "wrap"); - - - label = new JLabel(trans.get("SimExpPan.lbl.Commentchar")); - tip = trans.get("SimExpPan.lbl.ttip.Commentchar"); - label.setToolTipText(tip); - panel.add(label, "split 2"); - - commentCharacter = new JComboBox(new String[] { "#", "%", ";" }); - commentCharacter.setEditable(true); - commentCharacter.setSelectedItem(Prefs.getString(Prefs.EXPORT_COMMENT_CHARACTER, "#")); - commentCharacter.setToolTipText(tip); - panel.add(commentCharacter); - - this.add(panel, "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() { @@ -268,7 +192,7 @@ public class SimulationExportPanel extends JPanel { private void doExport() { JFileChooser chooser = new JFileChooser(); - chooser.setFileFilter(CSV_FILE_FILTER); + chooser.setFileFilter(FileHelper.CSV_FILE_FILTER); chooser.setCurrentDirectory(Prefs.getDefaultDirectory()); if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) @@ -278,49 +202,33 @@ public class SimulationExportPanel extends JPanel { if (file == null) return; - if (file.getName().indexOf('.') < 0) { - String name = file.getAbsolutePath(); - name = name + ".csv"; - file = new File(name); - } - - if (file.exists()) { - int ret = JOptionPane.showConfirmDialog(this, - //// File - trans.get("SimExpPan.Fileexists.desc1") + file.getName() + - //// \" exists. Overwrite? - trans.get("SimExpPan.Fileexists.desc2"), - //// File exists - trans.get("SimExpPan.Fileexists.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE); - if (ret != JOptionPane.YES_OPTION) - return; + file = FileHelper.ensureExtension(file, "csv"); + if (!FileHelper.confirmWrite(file, this)) { + return; } + - String commentChar = commentCharacter.getSelectedItem().toString(); - String fieldSep = fieldSeparator.getSelectedItem().toString(); - boolean simulationComment = simulationComments.isSelected(); - boolean fieldComment = fieldNameComments.isSelected(); - boolean eventComment = eventComments.isSelected(); + 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; Prefs.setDefaultDirectory(chooser.getCurrentDirectory()); - for (int i=0; i < selected.length; i++) { + for (int i = 0; i < selected.length; i++) { Prefs.setExportSelected(types[i], selected[i]); if (selected[i]) n++; } - Prefs.putString(Prefs.EXPORT_FIELD_SEPARATOR, fieldSep); - Prefs.putString(Prefs.EXPORT_COMMENT_CHARACTER, commentChar); - Prefs.putBoolean(Prefs.EXPORT_EVENT_COMMENTS, eventComment); - Prefs.putBoolean(Prefs.EXPORT_FIELD_NAME_COMMENT, fieldComment); - Prefs.putBoolean(Prefs.EXPORT_SIMULATION_COMMENT, simulationComment); - + FlightDataType[] fieldTypes = new FlightDataType[n]; Unit[] fieldUnits = new Unit[n]; int pos = 0; - for (int i=0; i < selected.length; i++) { + for (int i = 0; i < selected.length; i++) { if (selected[i]) { fieldTypes[pos] = types[i]; fieldUnits[pos] = units[i]; @@ -334,9 +242,9 @@ public class SimulationExportPanel extends JPanel { fieldSep = "\t"; } - - SaveCSVWorker.export(file, simulation, branch, fieldTypes, fieldUnits, fieldSep, - commentChar, simulationComment, fieldComment, eventComment, + + SaveCSVWorker.export(file, simulation, branch, fieldTypes, fieldUnits, fieldSep, + commentChar, simulationComment, fieldComment, eventComment, SwingUtilities.getWindowAncestor(this)); } @@ -346,7 +254,7 @@ public class SimulationExportPanel extends JPanel { int n = 0; String str; - for (int i=0; i < selected.length; i++) { + for (int i = 0; i < selected.length; i++) { if (selected[i]) n++; } @@ -357,21 +265,21 @@ public class SimulationExportPanel extends JPanel { } else { //// Exporting //// variables out of - str = trans.get("SimExpPan.ExportingVar.desc2") + " "+n+" " + - trans.get("SimExpPan.ExportingVar.desc3") + " " + total + "."; + 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) { @@ -382,7 +290,7 @@ public class SimulationExportPanel extends JPanel { public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { - Component component = renderer.getTableCellRendererComponent(table, + Component component = renderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); if (selected[row]) { @@ -406,12 +314,12 @@ public class SimulationExportPanel extends JPanel { 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; @@ -447,10 +355,10 @@ public class SimulationExportPanel extends JPanel { throw new IndexOutOfBoundsException("column=" + column); } } - + @Override public Object getValueAt(int row, int column) { - + switch (column) { case SELECTED: return selected[row]; @@ -462,34 +370,34 @@ public class SimulationExportPanel extends JPanel { return units[row]; default: - throw new IndexOutOfBoundsException("column="+column); + throw new IndexOutOfBoundsException("column=" + column); } } - + @Override public void setValueAt(Object value, int row, int column) { switch (column) { case SELECTED: - selected[row] = (Boolean)value; + selected[row] = (Boolean) value; this.fireTableRowsUpdated(row, row); updateSelectedCount(); break; - + case NAME: break; - + case UNIT: - units[row] = (Unit)value; + units[row] = (Unit) value; break; - + default: - throw new IndexOutOfBoundsException("column="+column); + throw new IndexOutOfBoundsException("column=" + column); } } - + @Override public boolean isCellEditable(int row, int column) { switch (column) { @@ -503,7 +411,7 @@ public class SimulationExportPanel extends JPanel { return types[row].getUnitGroup().getUnitCount() > 1; default: - throw new IndexOutOfBoundsException("column="+column); + throw new IndexOutOfBoundsException("column=" + column); } } diff --git a/src/net/sf/openrocket/gui/components/UnitCellEditor.java b/src/net/sf/openrocket/gui/components/UnitCellEditor.java index 7afa28ab..2ff47abc 100644 --- a/src/net/sf/openrocket/gui/components/UnitCellEditor.java +++ b/src/net/sf/openrocket/gui/components/UnitCellEditor.java @@ -20,11 +20,10 @@ import net.sf.openrocket.unit.UnitGroup; * * @author Sampo Niskanen */ -public abstract class UnitCellEditor extends AbstractCellEditor -implements TableCellEditor, ActionListener { - - private final JComboBox editor; +public abstract class UnitCellEditor extends AbstractCellEditor + implements TableCellEditor, ActionListener { + private final JComboBox editor; public UnitCellEditor() { editor = new JComboBox(); @@ -36,12 +35,12 @@ implements TableCellEditor, ActionListener { @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()) { + for (Unit u : group.getUnits()) { editor.addItem(u); } @@ -49,22 +48,22 @@ implements TableCellEditor, ActionListener { 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. * diff --git a/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java b/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java index 0edfddae..006b30f4 100644 --- a/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java +++ b/src/net/sf/openrocket/gui/dialogs/MotorDatabaseLoadingDialog.java @@ -1,5 +1,6 @@ package net.sf.openrocket.gui.dialogs; +import java.awt.SplashScreen; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -17,10 +18,15 @@ import net.sf.openrocket.logging.LogHelper; import net.sf.openrocket.startup.Application; import net.sf.openrocket.util.GUIUtil; +/** + * 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 @@ -44,7 +50,8 @@ public class MotorDatabaseLoadingDialog extends JDialog { /** * Check whether the motor database is loaded and block until it is. - * An uncloseable modal dialog window is opened while loading. + * 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 */ @@ -53,31 +60,54 @@ public class MotorDatabaseLoadingDialog extends JDialog { if (db.isLoaded()) return; - 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; + 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"); - @Override - public void actionPerformed(ActionEvent e) { + db.setInUse(); + int count = 0; + while (!db.isLoaded()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // No-op + } + count++; - if (db.isLoaded()) { - log.debug("Database loaded, closing dialog"); - dialog.setVisible(false); - } else if (count % 10 == 0) { + if (count % 10 == 0) { log.debug("Database not loaded, count=" + count); } } - }); - - db.setInUse(); - timer.start(); - dialog.setVisible(true); - timer.stop(); + + } - log.debug("Motor database now loaded"); + log.info("Motor database now loaded"); } } diff --git a/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java b/src/net/sf/openrocket/gui/dialogs/optimization/FunctionEvaluationData.java new file mode 100644 index 00000000..6162e965 --- /dev/null +++ b/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/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java b/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java index a68412e1..fe0e3774 100644 --- a/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java +++ b/src/net/sf/openrocket/gui/dialogs/optimization/GeneralOptimizationDialog.java @@ -1,66 +1,1047 @@ 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.JTree; +import javax.swing.JSpinner; +import javax.swing.JTable; +import javax.swing.JToggleButton; +import javax.swing.ListSelectionModel; +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.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.FileHelper; import net.sf.openrocket.util.GUIUtil; +import net.sf.openrocket.util.Named; +import net.sf.openrocket.util.Prefs; +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 final Rocket rocketCopy; + 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, "Rocket optimization"); + super(parent, trans.get("title")); this.baseDocument = document; - this.rocketCopy = document.getRocket().copyWithOriginalID(); + this.documentCopy = document.copy(); loadOptimizationParameters(); loadSimulationModifiers(); - + JPanel sub; + JLabel label; + JScrollPane scroll; + String tip; + JPanel panel = new JPanel(new MigLayout("fill")); - JTree tree = new SimulationModifierTree(rocketCopy, simulationModifiers); - JScrollPane scroll = new JScrollPane(tree); - panel.add(scroll, "width 300lp, height 300lp"); + 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"); + 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(optimizationPath, evaluationHistory, + 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(); + } + + @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) { + bestValue = pointValue.getParameterValue().getValue(); + } else { + log.error("History does not contain point " + newPoint); + 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) { + newSelected.add(newModifiers.get(index)); + } + } + } + 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.addAll(OptimizationServiceHelper.getOptimizableParameters(baseDocument)); + optimizationParameters.clear(); + optimizationParameters.addAll(OptimizationServiceHelper.getOptimizableParameters(documentCopy)); if (optimizationParameters.isEmpty()) { throw new BugException("No rocket optimization parameters found, distribution built wrong."); @@ -76,8 +1057,9 @@ public class GeneralOptimizationDialog extends JDialog { private void loadSimulationModifiers() { + simulationModifiers.clear(); - for (SimulationModifier m : OptimizationServiceHelper.getSimulationModifiers(baseDocument)) { + for (SimulationModifier m : OptimizationServiceHelper.getSimulationModifiers(documentCopy)) { Object key = m.getRelatedObject(); List list = simulationModifiers.get(key); if (list == null) { @@ -99,6 +1081,464 @@ public class GeneralOptimizationDialog extends JDialog { } + + + 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() { + + if (updating) { + return; + } + + updating = true; + + + // First enable all components if optimization not running + if (!running) { + for (JComponent c : disableComponents) { + c.setEnabled(true); + } + } + + + // "Add" button + SimulationModifier mod = getSelectedAvailableModifier(); + if (mod != null && !selectedModifiers.contains(mod)) { + addButton.setEnabled(true); + } else { + addButton.setEnabled(false); + } + + // "Remove" button + removeButton.setEnabled(selectedModifierTable.getSelectedRow() >= 0); + + // "Remove all" button + removeAllButton.setEnabled(!selectedModifiers.isEmpty()); + + + // Optimization goal + String selected = (String) optimizationGoalCombo.getSelectedItem(); + if (GOAL_SEEK.equals(selected)) { + optimizationGoalSpinner.setVisible(true); + optimizationGoalUnitSelector.setVisible(true); + } else { + optimizationGoalSpinner.setVisible(false); + optimizationGoalUnitSelector.setVisible(false); + } + + + // Minimum/maximum stability options + minimumStabilitySpinner.setEnabled(minimumStabilitySelected.isSelected()); + minimumStabilityUnitSelector.setEnabled(minimumStabilitySelected.isSelected()); + maximumStabilitySpinner.setEnabled(maximumStabilitySelected.isSelected()); + maximumStabilityUnitSelector.setEnabled(maximumStabilitySelected.isSelected()); + + + // Plot button (enabled if path exists and dimensionality is 1 or 2) + plotButton.setEnabled(!optimizationPath.isEmpty() && (selectedModifiers.size() == 1 || selectedModifiers.size() == 2)); + + // Save button (enabled if path exists) + saveButton.setEnabled(!optimizationPath.isEmpty()); + + + // Last disable all components if optimization is running + if (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(Prefs.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) { + + 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/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java new file mode 100644 index 00000000..da91712a --- /dev/null +++ b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationPlotDialog.java @@ -0,0 +1,356 @@ +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.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.GUIUtil; +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.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; + +/** + * 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); + } + + + + /** + * 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 + + + 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 series = 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(); + series.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 + + 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(series)); + 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/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationStepData.java new file mode 100644 index 00000000..ec1ece5a --- /dev/null +++ b/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/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java new file mode 100644 index 00000000..b7d18445 --- /dev/null +++ b/src/net/sf/openrocket/gui/dialogs/optimization/OptimizationWorker.java @@ -0,0 +1,229 @@ +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.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); + + 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/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java b/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java index 73296bbd..1e7e47f0 100644 --- a/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java +++ b/src/net/sf/openrocket/gui/dialogs/optimization/SimulationModifierTree.java @@ -8,8 +8,10 @@ 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; @@ -17,6 +19,7 @@ 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. @@ -28,28 +31,39 @@ import net.sf.openrocket.rocketcomponent.RocketComponent; */ 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) { - super(createModifierTree(rocket, simulationModifiers)); + 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(); } - - private static DefaultMutableTreeNode createModifierTree(Rocket rocket, - Map> simulationModifiers) { + /** + * 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); - return baseNode; + this.setModel(new DefaultTreeModel(baseNode)); } @@ -83,8 +97,11 @@ public class SimulationModifierTree extends BasicTree { } + /** + * Expand the rocket components, but not the modifiers. + */ @SuppressWarnings("rawtypes") - private void expandComponents() { + public void expandComponents() { DefaultMutableTreeNode baseNode = (DefaultMutableTreeNode) this.getModel().getRoot(); Enumeration enumeration = baseNode.breadthFirstEnumeration(); @@ -135,13 +152,30 @@ public class SimulationModifierTree extends BasicTree { 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) { - setForeground(Color.BLACK); + + if (selectedModifiers.contains(object)) { + setForeground(Color.GRAY); + } else { + setForeground(Color.BLACK); + } setFont(modifierFont); setText(((SimulationModifier) object).getName()); + setToolTipText(((SimulationModifier) object).getDescription()); } return this; diff --git a/src/net/sf/openrocket/gui/main/BasicFrame.java b/src/net/sf/openrocket/gui/main/BasicFrame.java index a9cbbfc0..e4b20408 100644 --- a/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.concurrent.ExecutionException; import javax.swing.Action; +import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JComponent; @@ -80,7 +81,6 @@ 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.main.componenttree.ComponentTree; -import net.sf.openrocket.gui.optimization.OptimizationTestDialog; import net.sf.openrocket.gui.scalefigure.RocketPanel; import net.sf.openrocket.l10n.Translator; import net.sf.openrocket.logging.LogHelper; @@ -367,8 +367,8 @@ public class BasicFrame extends JFrame { scroll.setBorder(null); scroll.setViewportBorder(null); - TitledBorder border = new TitledBorder(trans.get("BasicFrame.title.Addnewcomp")); - border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD)); + TitledBorder border = BorderFactory.createTitledBorder(trans.get("BasicFrame.title.Addnewcomp")); + GUIUtil.changeFontStyle(border, Font.BOLD); scroll.setBorder(border); panel.add(scroll, "grow"); @@ -648,6 +648,19 @@ public class BasicFrame extends JFrame { 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) { @@ -951,25 +964,6 @@ public class BasicFrame extends JFrame { menu.add(item); - item = new JMenuItem("General optimization test"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - new GeneralOptimizationDialog(document, BasicFrame.this).setVisible(true); - } - }); - menu.add(item); - - - item = new JMenuItem("Optimization test"); - item.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - new OptimizationTestDialog(BasicFrame.this, document).setVisible(true); - } - }); - menu.add(item); - return menu; } @@ -1370,13 +1364,6 @@ public class BasicFrame extends JFrame { * */ public void printAction() { - if (!Prefs.getBoolean("printing.experimental.communicated", false)) { - log.info("Showing printing is experimental warning to the user"); - JOptionPane.showMessageDialog(this, "Printing is an currently an experimental feature " + - "and might not fully work on all platforms", - "Experimental feature", JOptionPane.WARNING_MESSAGE); - Prefs.putBoolean("printing.experimental.communicated", true); - } new PrintDialog(this, document).setVisible(true); } diff --git a/src/net/sf/openrocket/gui/main/SimulationEditDialog.java b/src/net/sf/openrocket/gui/main/SimulationEditDialog.java index 501f79b0..90561b49 100644 --- a/src/net/sf/openrocket/gui/main/SimulationEditDialog.java +++ b/src/net/sf/openrocket/gui/main/SimulationEditDialog.java @@ -48,7 +48,7 @@ import net.sf.openrocket.rocketcomponent.Configuration; import net.sf.openrocket.simulation.FlightData; import net.sf.openrocket.simulation.FlightDataBranch; import net.sf.openrocket.simulation.RK4SimulationStepper; -import net.sf.openrocket.simulation.GUISimulationConditions; +import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.FlightDataType; import net.sf.openrocket.simulation.listeners.SimulationListener; import net.sf.openrocket.simulation.listeners.example.CSVSaveListener; @@ -81,7 +81,7 @@ public class SimulationEditDialog extends JDialog { private final Window parentWindow; private final Simulation simulation; - private final GUISimulationConditions conditions; + private final SimulationOptions conditions; private final Configuration configuration; private static final Translator trans = Application.getTranslator(); @@ -96,7 +96,7 @@ public class SimulationEditDialog extends JDialog { this.parentWindow = parent; this.simulation = s; - this.conditions = simulation.getConditions(); + this.conditions = simulation.getOptions(); configuration = simulation.getConfiguration(); JPanel mainPanel = new JPanel(new MigLayout("fill","[grow, fill]")); @@ -496,7 +496,7 @@ public class SimulationEditDialog extends JDialog { sub.add(label); m = new DoubleModel(conditions,"LaunchRodAngle", UnitGroup.UNITS_ANGLE, - 0, GUISimulationConditions.MAX_LAUNCH_ROD_ANGLE); + 0, SimulationOptions.MAX_LAUNCH_ROD_ANGLE); spin = new JSpinner(m.getSpinnerModel()); spin.setEditor(new SpinnerEditor(spin)); @@ -507,7 +507,7 @@ public class SimulationEditDialog extends JDialog { unit.setToolTipText(tip); sub.add(unit,"growx"); slider = new BasicSlider(m.getSliderModel(0, Math.PI/9, - GUISimulationConditions.MAX_LAUNCH_ROD_ANGLE)); + SimulationOptions.MAX_LAUNCH_ROD_ANGLE)); slider.setToolTipText(tip); sub.add(slider,"w 75lp, wrap"); diff --git a/src/net/sf/openrocket/gui/main/SimulationRunDialog.java b/src/net/sf/openrocket/gui/main/SimulationRunDialog.java index 96693eb6..1788515e 100644 --- a/src/net/sf/openrocket/gui/main/SimulationRunDialog.java +++ b/src/net/sf/openrocket/gui/main/SimulationRunDialog.java @@ -48,8 +48,8 @@ import net.sf.openrocket.util.Prefs; 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; @@ -95,6 +95,12 @@ public class SimulationRunDialog extends JDialog { 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]; @@ -127,7 +133,7 @@ public class SimulationRunDialog extends JDialog { panel.add(altLabel, "growx, wrap rel"); //// Velocity: - panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") +" ")); + panel.add(new JLabel(trans.get("SimuRunDlg.lbl.Velocity") + " ")); velLabel = new JLabel(""); panel.add(velLabel, "growx, wrap para"); @@ -272,7 +278,7 @@ public class SimulationRunDialog extends JDialog { double launchBurn = 0; double otherBurn = 0; Configuration config = simulation.getConfiguration(); - String id = simulation.getConditions().getMotorConfigurationID(); + String id = simulation.getOptions().getMotorConfigurationID(); Iterator iterator = config.motorIterator(); while (iterator.hasNext()) { MotorMount m = iterator.next(); @@ -410,7 +416,7 @@ public class SimulationRunDialog extends JDialog { DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, new Object[] { //// An exception occurred during the simulation: - trans.get("SimuRunDlg.msg.AnException1"), + trans.get("SimuRunDlg.msg.AnException1"), t.getMessage(), simulation.getSimulationListeners().isEmpty() ? trans.get("SimuRunDlg.msg.AnException2") : "" @@ -422,10 +428,10 @@ public class SimulationRunDialog extends JDialog { t.printStackTrace(); DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, new Object[] { - //// A computation error occurred during the simulation. - trans.get("SimuRunDlg.msg.AssertionError1"), - //// Please report this as a bug along with the details below. - trans.get("SimuRunDlg.msg.AssertionError2") + //// A computation error occurred during the simulation. + trans.get("SimuRunDlg.msg.AssertionError1"), + //// Please report this as a bug along with the details below. + trans.get("SimuRunDlg.msg.AssertionError2") }, stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE); @@ -435,9 +441,9 @@ public class SimulationRunDialog extends JDialog { DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this, new Object[] { //// An unknown error was encountered during the simulation. - trans.get("SimuRunDlg.msg.unknownerror1"), + trans.get("SimuRunDlg.msg.unknownerror1"), //// The program may be unstable, you should save all your designs and restart OpenRocket now! - trans.get("SimuRunDlg.msg.unknownerror2") + trans.get("SimuRunDlg.msg.unknownerror2") }, stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE); diff --git a/src/net/sf/openrocket/gui/optimization/OptimizationTestDialog.java b/src/net/sf/openrocket/gui/optimization/OptimizationTestDialog.java deleted file mode 100644 index 337b971b..00000000 --- a/src/net/sf/openrocket/gui/optimization/OptimizationTestDialog.java +++ /dev/null @@ -1,218 +0,0 @@ -package net.sf.openrocket.gui.optimization; - -import static net.sf.openrocket.util.MathUtil.pow2; - -import java.awt.Window; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; -import net.sf.openrocket.document.OpenRocketDocument; -import net.sf.openrocket.document.Simulation; -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; -import net.sf.openrocket.optimization.rocketoptimization.RocketOptimizationFunction; -import net.sf.openrocket.optimization.rocketoptimization.SimulationDomain; -import net.sf.openrocket.optimization.rocketoptimization.SimulationModifier; -import net.sf.openrocket.optimization.rocketoptimization.domains.StabilityDomain; -import net.sf.openrocket.optimization.rocketoptimization.goals.MaximizationGoal; -import net.sf.openrocket.optimization.rocketoptimization.modifiers.GenericComponentModifier; -import net.sf.openrocket.optimization.rocketoptimization.parameters.MaximumAltitudeParameter; -import net.sf.openrocket.rocketcomponent.BodyTube; -import net.sf.openrocket.rocketcomponent.Rocket; -import net.sf.openrocket.rocketcomponent.RocketComponent; -import net.sf.openrocket.unit.UnitGroup; -import net.sf.openrocket.util.GUIUtil; - -public class OptimizationTestDialog extends JDialog { - - private final OpenRocketDocument document; - - public OptimizationTestDialog(Window parent, OpenRocketDocument document) { - super(parent, "Optimization", ModalityType.APPLICATION_MODAL); - - this.document = document; - - JPanel panel = new JPanel(new MigLayout("fill")); - this.add(panel); - - JButton button = new JButton("Test optimize"); - button.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - try { - doOptimize(); - } catch (OptimizationException e1) { - e1.printStackTrace(); - } - } - }); - panel.add(button, "wrap para"); - - - - JButton close = new JButton("Close"); - close.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - OptimizationTestDialog.this.dispose(); - } - }); - panel.add(close); - - - GUIUtil.setDisposableDialogOptions(this, close); - } - - - private void doOptimize() throws OptimizationException { - Simulation sim = document.getSimulation(0); - Rocket rocket = sim.getRocket(); - - RocketComponent body = null; - - for (RocketComponent c : rocket) { - if (c instanceof BodyTube) { - body = c; - break; - } - } - - Point initial; - - SimulationDomain domain; - // domain= new IdentitySimulationDomain(); - domain = new StabilityDomain(2, false); - - SimulationModifier mod1 = new GenericComponentModifier("Test", body, - UnitGroup.UNITS_LENGTH, 1.0, BodyTube.class, body.getID(), "Length"); - mod1.setMinValue(0.1); - mod1.setMaxValue(0.7); - - SimulationModifier mod2 = new GenericComponentModifier("Test", body, - UnitGroup.UNITS_LENGTH, 2.0, BodyTube.class, body.getID(), "OuterRadius"); - mod2.setMinValue(0.01); - mod2.setMaxValue(0.10); - - OptimizationController controller = new OptimizationController() { - int step = 0; - - @Override - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - step++; - System.out.println("STEP " + step + " oldValue=" + oldValue + " newValue=" + newValue + - " oldPoint=" + oldPoint + " newPoint=" + newPoint + - " stepSize=" + stepSize); - return step < 20; - } - }; - - - initial = new Point(mod1.getCurrentScaledValue(sim), mod2.getCurrentScaledValue(sim)); - - - Function function = new RocketOptimizationFunction(sim, new MaximumAltitudeParameter(), - new MaximizationGoal(), domain, mod1, mod2); - /* - function = new Function() { - @Override - public double evaluate(Point point) throws InterruptedException, OptimizationException { - // y = ax^2 + bx + c - // y' = 2ax + b - // 2a * pi/4 + b = 0 - // b = -a*pi/2 - // a=-1 -> b = pi/2 - - - double x = point.get(0); - double y = -x * x + Math.PI / 2 * x; - System.out.println("Evaluating at x=" + x + " value=" + y); - return y; - } - }; - */ - - ParallelFunctionCache cache = new ParallelExecutorCache(1); - cache.setFunction(function); - - FunctionOptimizer optimizer = new MultidirectionalSearchOptimizer(cache); - - optimizer.optimize(initial, controller); - } - - - //////////////////////////////////////////////////////////////////////////////////////////////////////// - - - private static int evalCount = 0; - - public static void main(String[] args) throws OptimizationException { - Point initial; - - - - OptimizationController controller = new OptimizationController() { - int step = 0; - - @Override - public boolean stepTaken(Point oldPoint, double oldValue, Point newPoint, double newValue, double stepSize) { - step++; - System.out.println("STEP " + step + " oldValue=" + oldValue + " newValue=" + newValue + - " oldPoint=" + oldPoint + " newPoint=" + newPoint + - " stepSize=" + stepSize); - return step < 20; - } - }; - - - initial = new Point(0.5, 0.5); - - - Function function = new Function() { - @Override - public double evaluate(Point point) throws InterruptedException, OptimizationException { - // y = ax^2 + bx + c - // y' = 2ax + b - // 2a * pi/4 + b = 0 - // b = -a*pi/2 - // a=-1 -> b = pi/2 - - evalCount++; - - // double x = point.get(0); - // double y = x * x - Math.PI / 2 * x; - // System.out.println("Evaluating at x=" + x + " value=" + y); - // return y; - - double x = point.get(0); - double y = point.get(1); - double z = 4 * pow2((x - 0.3231)) + 2 * pow2(y - 0.8923); - - System.out.println("Evaluation " + evalCount + ": x=" + x + " y=" + y + " z=" + z); - - return z; - } - }; - - - ParallelFunctionCache cache = new ParallelExecutorCache(); - cache.setFunction(function); - - FunctionOptimizer optimizer = new MultidirectionalSearchOptimizer(cache); - - optimizer.optimize(initial, controller); - - - System.out.println("Total evaluation count: " + evalCount); - } -} diff --git a/src/net/sf/openrocket/gui/plot/PlotConfiguration.java b/src/net/sf/openrocket/gui/plot/PlotConfiguration.java index 93122736..bd58782f 100644 --- a/src/net/sf/openrocket/gui/plot/PlotConfiguration.java +++ b/src/net/sf/openrocket/gui/plot/PlotConfiguration.java @@ -19,7 +19,7 @@ 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(); @@ -676,7 +676,7 @@ public class PlotConfiguration implements Cloneable { continue; double d = (max - min) / axis.getRangeLength(); - d = Math.sqrt(d); // Prioritize small ranges + d = MathUtil.safeSqrt(d); // Prioritize small ranges goodness += d * 100.0; } diff --git a/src/net/sf/openrocket/gui/plot/PlotDialog.java b/src/net/sf/openrocket/gui/plot/PlotDialog.java deleted file mode 100644 index da775482..00000000 --- a/src/net/sf/openrocket/gui/plot/PlotDialog.java +++ /dev/null @@ -1,577 +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.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.unit.UnitGroup; -import net.sf.openrocket.util.BugException; -import net.sf.openrocket.util.GUIUtil; -import net.sf.openrocket.util.MathUtil; -import net.sf.openrocket.util.Prefs; - -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; - -public class PlotDialog 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 PlotDialog(Window parent, Simulation simulation, PlotConfiguration config) { - //// Flight data plot - super(parent, trans.get("PlotDialog.title.Flightdataplot")); - this.setModalityType(ModalityType.DOCUMENT_MODAL); - - final boolean initialShowPoints = Prefs.getBoolean(Prefs.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(); - Prefs.putBoolean(Prefs.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) { - PlotDialog.this.dispose(); - } - }); - panel.add(button, "right"); - - this.setLocationByPlatform(true); - this.pack(); - - GUIUtil.setDisposableDialogOptions(this, button); - } - - 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 PlotDialog(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/SimulationPlotDialog.java b/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java new file mode 100644 index 00000000..c68617b7 --- /dev/null +++ b/src/net/sf/openrocket/gui/plot/SimulationPlotDialog.java @@ -0,0 +1,582 @@ +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.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.unit.UnitGroup; +import net.sf.openrocket.util.BugException; +import net.sf.openrocket.util.GUIUtil; +import net.sf.openrocket.util.MathUtil; +import net.sf.openrocket.util.Prefs; + +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 = Prefs.getBoolean(Prefs.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(); + Prefs.putBoolean(Prefs.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); + } + + 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 index eb646dc5..664d32da 100644 --- a/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java +++ b/src/net/sf/openrocket/gui/plot/SimulationPlotPanel.java @@ -33,9 +33,14 @@ import net.sf.openrocket.unit.Unit; import net.sf.openrocket.util.GUIUtil; import net.sf.openrocket.util.Icons; +/** + * 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; @@ -238,7 +243,7 @@ public class SimulationPlotPanel extends JPanel { JOptionPane.showMessageDialog(SimulationPlotPanel.this, //// A maximum of 15 plots is allowed. //// Cannot add plot - trans.get("simplotpanel.OptionPane.lbl1"), + trans.get("simplotpanel.OptionPane.lbl1"), trans.get("simplotpanel.OptionPane.lbl2"), JOptionPane.ERROR_MESSAGE); return; @@ -287,7 +292,7 @@ public class SimulationPlotPanel extends JPanel { @Override public void actionPerformed(ActionEvent e) { defaultConfiguration = configuration.clone(); - PlotDialog.showPlot(SwingUtilities.getWindowAncestor(SimulationPlotPanel.this), + SimulationPlotDialog.showPlot(SwingUtilities.getWindowAncestor(SimulationPlotPanel.this), simulation, configuration); } }); @@ -375,7 +380,7 @@ public class SimulationPlotPanel extends JPanel { public void itemStateChanged(ItemEvent e) { if (modifying > 0) return; - Unit unit = (Unit) unitSelector.getSelectedUnit(); + Unit unit = unitSelector.getSelectedUnit(); configuration.setPlotDataUnit(index, unit); } }); diff --git a/src/net/sf/openrocket/gui/print/DesignReport.java b/src/net/sf/openrocket/gui/print/DesignReport.java index e1311444..d149b83c 100644 --- a/src/net/sf/openrocket/gui/print/DesignReport.java +++ b/src/net/sf/openrocket/gui/print/DesignReport.java @@ -456,7 +456,7 @@ public class DesignReport { FlightData flight = null; try { Simulation simulation = Prefs.getBackgroundSimulation(duplicate); - simulation.getConditions().setMotorConfigurationID(motorId); + simulation.getOptions().setMotorConfigurationID(motorId); simulation.simulate(); flight = simulation.getSimulatedData(); } catch (SimulationException e1) { diff --git a/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java b/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java index c1b9caee..53dc895d 100644 --- a/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java +++ b/src/net/sf/openrocket/gui/rocketfigure/SymmetricComponentShapes.java @@ -5,20 +5,21 @@ import java.awt.geom.Path2D; import java.util.ArrayList; import net.sf.openrocket.util.Coordinate; +import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.Transformation; 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); + 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, + public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, Transformation transformation) { - net.sf.openrocket.rocketcomponent.SymmetricComponent c = (net.sf.openrocket.rocketcomponent.SymmetricComponent)component; + net.sf.openrocket.rocketcomponent.SymmetricComponent c = (net.sf.openrocket.rocketcomponent.SymmetricComponent) component; int i; final double delta = 0.0000001; @@ -26,44 +27,44 @@ public class SymmetricComponentShapes extends RocketComponentShapes { 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)); + 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)); + 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 = 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; + 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)); + 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++) { + + 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; @@ -71,29 +72,30 @@ public class SymmetricComponentShapes extends RocketComponentShapes { 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*S, points.get(len-1).y*S); - for (i=len-2; i>=0; i--) { - path.lineTo(points.get(i).x*S, points.get(i).y*S); + path.moveTo(points.get(len - 1).x * S, points.get(len - 1).y * S); + for (i = len - 2; i >= 0; i--) { + path.lineTo(points.get(i).x * S, points.get(i).y * S); } - for (i=0; i ACCEPTABLE_ANGLE); + return (cosAngle(v1, v2, v3) > ACCEPTABLE_ANGLE); } + /* * cosAngle = v1.v2 / |v1|*|v2| = v1.v2 / sqrt(v1.v1*v2.v2) */ @@ -101,7 +103,7 @@ public class SymmetricComponentShapes extends RocketComponentShapes { double cos; double len; cos = Coordinate.dot(v1.sub(v2), v2.sub(v3)); - len = Math.sqrt(v1.sub(v2).length2() * v2.sub(v3).length2()); - return cos/len; + len = MathUtil.safeSqrt(v1.sub(v2).length2() * v2.sub(v3).length2()); + return cos / len; } } diff --git a/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java b/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java index 5a515ba7..63d2f469 100644 --- a/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java +++ b/src/net/sf/openrocket/gui/scalefigure/AbstractScaleFigure.java @@ -13,41 +13,46 @@ import net.sf.openrocket.util.Prefs; public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure { - - // Number of pixels to leave at edges when fitting figure - public static final int BORDER_PIXELS_WIDTH=30; - public static final int BORDER_PIXELS_HEIGHT=20; + // 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 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 = Prefs.getDPI(); this.scaling = 1.0; - this.scale = dpi/0.0254*scaling; + 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; @@ -64,49 +69,61 @@ public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure if (Math.abs(this.scaling - scaling) < 0.01) return; this.scaling = scaling; - this.scale = dpi/0.0254*scaling; + this.scale = dpi / 0.0254 * scaling; updateFigure(); } @Override public void setScaling(Dimension bounds) { double zh = 1, zv = 1; - int w = bounds.width - 2*BORDER_PIXELS_WIDTH -20; - int h = bounds.height - 2*BORDER_PIXELS_HEIGHT -20; + 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 = ((double)w) / getFigureWidth(); - zv = ((double)h) / getFigureHeight(); - - double s = Math.min(zh, zv)/dpi*0.0254 - 0.001; + 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(ChangeListener listener) { - listeners.add(0,listener); + listeners.add(0, listener); } - + @Override public void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } private ChangeEvent changeEvent = null; + protected void fireChangeEvent() { ChangeListener[] list = listeners.toArray(new ChangeListener[0]); - for (ChangeListener l: list) { + for (ChangeListener l : list) { if (changeEvent == null) changeEvent = new ChangeEvent(this); l.stateChanged(changeEvent); } } - + } diff --git a/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java b/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java index 29e2112c..0f08be21 100644 --- a/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java +++ b/src/net/sf/openrocket/gui/scalefigure/FinPointFigure.java @@ -25,7 +25,7 @@ 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; @@ -45,48 +45,48 @@ public class FinPointFigure extends AbstractScaleFigure { 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*BORDER_PIXELS_WIDTH < getWidth()) { - + if (figureWidth * scale + 2 * borderPixelsWidth < getWidth()) { + // Figure fits in the viewport - tx = (getWidth()-figureWidth*scale)/2 - minX*scale; - + tx = (getWidth() - figureWidth * scale) / 2 - minX * scale; + } else { - + // Figure does not fit in viewport - tx = BORDER_PIXELS_WIDTH - minX*scale; + tx = borderPixelsWidth - minX * scale; } - if (figureHeight*scale + 2*BORDER_PIXELS_HEIGHT < getHeight()) { - ty = getHeight() - BORDER_PIXELS_HEIGHT; + if (figureHeight * scale + 2 * borderPixelsHeight < getHeight()) { + ty = getHeight() - borderPixelsHeight; } else { - ty = BORDER_PIXELS_HEIGHT + figureHeight*scale; + ty = borderPixelsHeight + figureHeight * scale; } - if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) { + 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) { + + if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { // Origin has changed, fire event translateX = tx; translateY = ty; @@ -97,121 +97,121 @@ public class FinPointFigure extends AbstractScaleFigure { // Calculate and store the transformation used transform = new AffineTransform(); transform.translate(translateX, translateY); - transform.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE); + 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, + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + 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; - + 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), + g2.setStroke(new BasicStroke((float) (1.0 * EXTRA_SCALE / scale), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL)); - g2.setColor(new Color(0,0,255,30)); - + 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(); + 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); - } - } - - - - + 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.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); - + 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); + 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.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; + 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++) { + 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); + 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); + Point2D.Double p = new Point2D.Double(x, y); try { - transform.inverseTransform(p,p); + transform.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { return -1; } - - for (int i=0; i < handles.length; i++) { + + for (int i = 0; i < handles.length; i++) { if (handles[i].contains(p)) return i; } @@ -224,9 +224,9 @@ public class FinPointFigure extends AbstractScaleFigure { return -1; // Calculate point in shapes' coordinates - Point2D.Double p = new Point2D.Double(x,y); + Point2D.Double p = new Point2D.Double(x, y); try { - transform.inverseTransform(p,p); + transform.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { return -1; } @@ -234,22 +234,22 @@ public class FinPointFigure extends AbstractScaleFigure { 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)); + + 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; + 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); + // 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); + 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; } @@ -259,14 +259,14 @@ public class FinPointFigure extends AbstractScaleFigure { public Point2D.Double convertPoint(double x, double y) { - Point2D.Double p = new Point2D.Double(x,y); + Point2D.Double p = new Point2D.Double(x, y); try { - transform.inverseTransform(p,p); + transform.inverseTransform(p, p); } catch (NoninvertibleTransformException e) { - assert(false): "Should not occur"; - return new Point2D.Double(0,0); + assert (false) : "Should not occur"; + return new Point2D.Double(0, 0); } - + p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE); return p; } @@ -279,9 +279,9 @@ public class FinPointFigure extends AbstractScaleFigure { modID = finset.getRocket().getAerodynamicModID(); calculateDimensions(); } - return new Dimension((int)translateX, (int)translateY); + return new Dimension((int) translateX, (int) translateY); } - + @Override public double getFigureWidth() { if (modID != finset.getRocket().getAerodynamicModID()) { @@ -290,7 +290,7 @@ public class FinPointFigure extends AbstractScaleFigure { } return figureWidth; } - + @Override public double getFigureHeight() { if (modID != finset.getRocket().getAerodynamicModID()) { @@ -299,14 +299,14 @@ public class FinPointFigure extends AbstractScaleFigure { } return figureHeight; } - + private void calculateDimensions() { minX = 0; maxX = 0; maxY = 0; - for (Coordinate c: finset.getFinPoints()) { + for (Coordinate c : finset.getFinPoints()) { if (c.x < minX) minX = c.x; if (c.x > maxX) @@ -322,8 +322,8 @@ public class FinPointFigure extends AbstractScaleFigure { figureHeight = maxY; - Dimension d = new Dimension((int)(figureWidth*scale+2*BORDER_PIXELS_WIDTH), - (int)(figureHeight*scale+2*BORDER_PIXELS_HEIGHT)); + Dimension d = new Dimension((int) (figureWidth * scale + 2 * borderPixelsWidth), + (int) (figureHeight * scale + 2 * borderPixelsHeight)); if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) { setPreferredSize(d); @@ -339,6 +339,6 @@ public class FinPointFigure extends AbstractScaleFigure { repaint(); } - + } diff --git a/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java b/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java index ea1f98c7..2f7556e2 100644 --- a/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java +++ b/src/net/sf/openrocket/gui/scalefigure/RocketFigure.java @@ -1,20 +1,6 @@ package net.sf.openrocket.gui.scalefigure; -import net.sf.openrocket.gui.figureelements.FigureElement; -import net.sf.openrocket.gui.main.ExceptionHandler; -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.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.Prefs; -import net.sf.openrocket.util.Reflection; -import net.sf.openrocket.util.Transformation; - import java.awt.BasicStroke; import java.awt.Color; import java.awt.Dimension; @@ -33,6 +19,20 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; +import net.sf.openrocket.gui.figureelements.FigureElement; +import net.sf.openrocket.gui.main.ExceptionHandler; +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.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.Prefs; +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)}, @@ -40,7 +40,6 @@ import java.util.LinkedHashSet; * * @author Sampo Niskanen */ - public class RocketFigure extends AbstractScaleFigure { private static final long serialVersionUID = 1L; @@ -53,31 +52,31 @@ public class RocketFigure extends AbstractScaleFigure { // Width for drawing normal and selected components public static final double NORMAL_WIDTH = 1.0; public static final double SELECTED_WIDTH = 2.0; - - private final Configuration configuration; + + 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 final ArrayList figureComponents = + new ArrayList(); - private double minX=0, maxX=0, maxR=0; + 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 double figureWidth = 0, figureHeight = 0; + private int figureWidthPx = 0, figureHeightPx = 0; private AffineTransform g2transformation = null; @@ -96,26 +95,36 @@ public class RocketFigure extends AbstractScaleFigure { this.rotation = 0.0; this.transformation = Transformation.rotate_x(0.0); - calculateSize(); 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); + return new Dimension((int) translateX, (int) translateY); } @Override public double getFigureHeight() { return figureHeight; } - + @Override public double getFigureWidth() { return figureWidth; } - + public RocketComponent[] getSelection() { return selection; @@ -154,19 +163,18 @@ public class RocketFigure extends AbstractScaleFigure { public void setType(int type) { if (type != TYPE_BACK && type != TYPE_SIDE) { - throw new IllegalArgumentException("Illegal type: "+type); + throw new IllegalArgumentException("Illegal type: " + type); } if (this.type == type) return; this.type = type; updateFigure(); } - - + /** * Updates the figure shapes and figure size. */ @@ -176,11 +184,11 @@ public class RocketFigure extends AbstractScaleFigure { figureComponents.clear(); calculateSize(); - + // Get shapes for all active components - for (RocketComponent c: configuration) { + for (RocketComponent c : configuration) { Shape[] s = getShapes(c); - for (int i=0; i < s.length; i++) { + for (int i = 0; i < s.length; i++) { figureShapes.add(s[i]); figureComponents.add(c); } @@ -189,7 +197,7 @@ public class RocketFigure extends AbstractScaleFigure { repaint(); fireChangeEvent(); } - + public void addRelativeExtra(FigureElement p) { relativeExtra.add(p); @@ -216,7 +224,7 @@ public class RocketFigure extends AbstractScaleFigure { absoluteExtra.clear(); } - + /** * Paints the rocket on to the Graphics element. *

@@ -227,39 +235,39 @@ public class RocketFigure extends AbstractScaleFigure { @Override public void paintComponent(Graphics g) { super.paintComponent(g); - Graphics2D g2 = (Graphics2D)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*BORDER_PIXELS_WIDTH < getWidth()) { - + if (figureWidthPx + 2 * borderPixelsWidth < getWidth()) { + // Figure fits in the viewport if (type == TYPE_BACK) - tx = getWidth()/2; - else - tx = (getWidth()-figureWidthPx)/2 - minX*scale; - + tx = getWidth() / 2; + else + tx = (getWidth() - figureWidthPx) / 2 - minX * scale; + } else { - + // Figure does not fit in viewport if (type == TYPE_BACK) - tx = BORDER_PIXELS_WIDTH + figureWidthPx/2; - else - tx = BORDER_PIXELS_WIDTH - minX*scale; + tx = borderPixelsWidth + figureWidthPx / 2; + else + tx = borderPixelsWidth - minX * scale; } - - ty = computeTy(figureHeightPx); - - if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) { + + ty = computeTy(figureHeightPx); + + if (Math.abs(translateX - tx) > 1 || Math.abs(translateY - ty) > 1) { // Origin has changed, fire event translateX = tx; translateY = ty; @@ -272,28 +280,28 @@ public class RocketFigure extends AbstractScaleFigure { g2transformation = new AffineTransform(); g2transformation.translate(translateX, translateY); // Mirror position Y-axis upwards - g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE); - + g2transformation.scale(scale / EXTRA_SCALE, -scale / EXTRA_SCALE); + g2.transform(g2transformation); - + // Set rendering hints appropriately - g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, + g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, + g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - + // Draw all shapes - for (int i=0; i < figureShapes.size(); i++) { + 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++) { + for (int j = 0; j < selection.length; j++) { if (c == selection[j]) { selected = true; break; @@ -306,37 +314,37 @@ public class RocketFigure extends AbstractScaleFigure { color = Prefs.getDefaultColor(c.getClass()); } g2.setColor(color); - + LineStyle style = c.getLineStyle(); if (style == null) style = Prefs.getDefaultLineStyle(c.getClass()); float[] dashes = style.getDashes(); - for (int j=0; j l = new LinkedHashSet(); - - for (int i=0; i 0.0001) - zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth(); + zh = (getWidth() - 2 * borderPixelsWidth) / bounds.getWidth(); if (bounds.getHeight() > 0.0001) - zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight(); + zv = (getHeight() - 2 * borderPixelsHeight) / bounds.getHeight(); return Math.min(zh, zv); } - + + /** * Calculates the necessary size of the figure and set the PreferredSize * property accordingly. @@ -507,26 +516,26 @@ public class RocketFigure extends AbstractScaleFigure { switch (type) { case TYPE_SIDE: - figureWidth = maxX-minX; - figureHeight = 2*maxR; + figureWidth = maxX - minX; + figureHeight = 2 * maxR; break; - + case TYPE_BACK: - figureWidth = 2*maxR; - figureHeight = 2*maxR; + figureWidth = 2 * maxR; + figureHeight = 2 * maxR; break; - + default: - assert(false): "Should not occur, type="+type; + 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*BORDER_PIXELS_WIDTH, - figureHeightPx+2*BORDER_PIXELS_HEIGHT); + 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); @@ -538,14 +547,14 @@ public class RocketFigure extends AbstractScaleFigure { public Rectangle2D getDimensions() { switch (type) { case TYPE_SIDE: - return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR); + return new Rectangle2D.Double(minX, -maxR, maxX - minX, 2 * maxR); case TYPE_BACK: - return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR); + return new Rectangle2D.Double(-maxR, -maxR, 2 * maxR, 2 * maxR); default: - throw new BugException("Illegal figure type = "+type); + 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 index c367d7e2..920d4617 100644 --- a/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java +++ b/src/net/sf/openrocket/gui/scalefigure/RocketPanel.java @@ -592,7 +592,7 @@ public class RocketPanel extends JPanel implements TreeSelectionListener, Change Rocket duplicate = (Rocket) configuration.getRocket().copy(); Simulation simulation = Prefs.getBackgroundSimulation(duplicate); - simulation.getConditions().setMotorConfigurationID( + simulation.getOptions().setMotorConfigurationID( configuration.getMotorConfigurationID()); backgroundSimulationWorker = new BackgroundSimulationWorker(simulation); diff --git a/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java b/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java index 161c8353..48440fe3 100644 --- a/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java +++ b/src/net/sf/openrocket/gui/scalefigure/ScaleFigure.java @@ -48,7 +48,7 @@ public interface ScaleFigure extends ChangeSource { */ public double getScaling(); - + /** * Return the scale of the figure on px/m. * @@ -56,7 +56,7 @@ public interface ScaleFigure extends ChangeSource { */ public double getAbsoluteScale(); - + /** * Return the pixel coordinates of the figure origin. * @@ -64,4 +64,19 @@ public interface ScaleFigure extends ChangeSource { */ 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 index aa46b682..75ec602f 100644 --- a/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java +++ b/src/net/sf/openrocket/gui/scalefigure/ScaleScrollPane.java @@ -43,18 +43,18 @@ import net.sf.openrocket.util.BugException; * * @author Sampo Niskanen */ -public class ScaleScrollPane extends JScrollPane +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; @@ -64,25 +64,34 @@ public class ScaleScrollPane extends JScrollPane 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); -// super(component, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, -// JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); if (!(component instanceof ScaleFigure)) { throw new IllegalArgumentException("component must implement ScaleFigure"); } this.component = component; - this.figure = (ScaleFigure)component; + this.figure = (ScaleFigure) component; this.allowFit = allowFit; - rulerUnit = new DoubleModel(0.0,UnitGroup.UNITS_LENGTH); + rulerUnit = new DoubleModel(0.0, UnitGroup.UNITS_LENGTH); rulerUnit.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { @@ -100,10 +109,10 @@ public class ScaleScrollPane extends JScrollPane 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); @@ -135,14 +144,25 @@ public class ScaleScrollPane extends JScrollPane } + /** + * 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."); @@ -169,7 +189,7 @@ public class ScaleScrollPane extends JScrollPane public double getScale() { return figure.getAbsoluteScale(); } - + public void setScaling(double scale) { if (fit) { setFitting(false); @@ -188,54 +208,54 @@ public class ScaleScrollPane extends JScrollPane //////////////// Mouse handlers //////////////// - private int dragStartX=0; - private int dragStartY=0; + 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) { + if (dragRectangle == null) { return; } - - dragRectangle.setLocation(dragStartX-e.getX(),dragStartY-e.getY()); - + + 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 //////////////// @@ -261,9 +281,9 @@ public class ScaleScrollPane extends JScrollPane public void updateSize() { Dimension d = component.getPreferredSize(); if (orientation == HORIZONTAL) { - setPreferredSize(new Dimension(d.width+10,RULER_SIZE)); + setPreferredSize(new Dimension(d.width + 10, RULER_SIZE)); } else { - setPreferredSize(new Dimension(RULER_SIZE,d.height+10)); + setPreferredSize(new Dimension(RULER_SIZE, d.height + 10)); } revalidate(); repaint(); @@ -274,102 +294,102 @@ public class ScaleScrollPane extends JScrollPane if (orientation == HORIZONTAL) { px -= origin.width; } else { -// px = -(px - origin.height); + // px = -(px - origin.height); px -= origin.height; } - return px/figure.getAbsoluteScale(); + return px / figure.getAbsoluteScale(); } private int toPx(double l) { Dimension origin = figure.getOrigin(); - int px = (int)(l * figure.getAbsoluteScale() + 0.5); + int px = (int) (l * figure.getAbsoluteScale() + 0.5); if (orientation == HORIZONTAL) { px += origin.width; } else { px = px + origin.height; -// px += origin.height; + // px += origin.height; } return px; } - @Override + @Override protected void paintComponent(Graphics g) { - super.paintComponent(g); - Graphics2D g2 = (Graphics2D)g; + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g; Rectangle area = g2.getClipBounds(); - - // Fill area with background color + + // Fill area with background color g2.setColor(getBackground()); - g2.fillRect(area.x, area.y, area.width, area.height+100); - + 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(); + 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); + - Tick[] ticks = unit.getTicks(start, end, minor, major); - - - // Set color & hints - g2.setColor(Color.BLACK); + // Set color & hints + g2.setColor(Color.BLACK); g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE); - g2.setRenderingHint(RenderingHints.KEY_RENDERING, + 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); - } - } + + 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/models/wind/PinkNoiseWindModel.java b/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java index 3916f6a9..960325d5 100644 --- a/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java +++ b/src/net/sf/openrocket/models/wind/PinkNoiseWindModel.java @@ -15,9 +15,11 @@ import net.sf.openrocket.util.PinkNoise; */ public class PinkNoiseWindModel implements WindModel { - /** Source for seed numbers, may be overridden by get/setSeed(). */ - private static final Random seedSource = new Random(); + /** 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; @@ -34,7 +36,7 @@ public class PinkNoiseWindModel implements WindModel { private double average = 0; private double standardDeviation = 0; - private int seed; + private final int seed; private PinkNoise randomSource = null; private double time1; @@ -42,12 +44,11 @@ public class PinkNoiseWindModel implements WindModel { /** - * Construct a new wind simulator with a random starting seed value. + * Construct a new wind simulation with a specific seed value. + * @param seed the seed value. */ - public PinkNoiseWindModel() { - synchronized (seedSource) { - seed = seedSource.nextInt(); - } + public PinkNoiseWindModel(int seed) { + this.seed = seed ^ SEED_RANDOMIZATION; } @@ -122,18 +123,6 @@ public class PinkNoiseWindModel implements WindModel { - public int getSeed() { - return seed; - } - - public void setSeed(int seed) { - if (this.seed == seed) - return; - this.seed = seed; - } - - - @Override public Coordinate getWindVelocity(double time, double altitude) { if (time < 0) { diff --git a/src/net/sf/openrocket/optimization/general/Point.java b/src/net/sf/openrocket/optimization/general/Point.java index 47dab113..9b2bece8 100644 --- a/src/net/sf/openrocket/optimization/general/Point.java +++ b/src/net/sf/openrocket/optimization/general/Point.java @@ -139,7 +139,7 @@ public final class Point { */ public double length() { if (length < 0) { - length = Math.sqrt(length2()); + length = MathUtil.safeSqrt(length2()); } return length; } diff --git a/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java b/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java index 0e70bf13..1f15800f 100644 --- a/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java +++ b/src/net/sf/openrocket/optimization/general/multidim/MultidirectionalSearchOptimizer.java @@ -21,6 +21,8 @@ import net.sf.openrocket.util.Statistics; * 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(); @@ -30,6 +32,7 @@ public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Stati private ParallelFunctionCache functionExecutor; private boolean useExpansion = false; + private boolean useCoordinateSearch = false; private int stepCount = 0; private int reflectionAcceptance = 0; @@ -73,7 +76,8 @@ public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Stati List coordinateSearch = new ArrayList(simplex.size()); Point current; double currentValue; - do { + boolean continueOptimization = true; + while (continueOptimization) { log.debug("Starting optimization step with simplex " + simplex + (simplexComputed ? "" : " (not computed)")); @@ -95,12 +99,14 @@ public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Stati * Expansion is unlikely as we're mainly dealing with bounded optimization. */ createReflection(simplex, reflection); - createCoordinateSearch(current, step, coordinateSearch); + if (useCoordinateSearch) + createCoordinateSearch(current, step, coordinateSearch); if (useExpansion) createExpansion(simplex, expansion); functionExecutor.compute(reflection); - functionExecutor.compute(coordinateSearch); + if (useCoordinateSearch) + functionExecutor.compute(coordinateSearch); if (useExpansion) functionExecutor.compute(expansion); @@ -113,7 +119,8 @@ public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Stati log.debug("Reflection was successful, aborting coordinate search, " + (useExpansion ? "computing" : "skipping") + " expansion"); - functionExecutor.abort(coordinateSearch); + if (useCoordinateSearch) + functionExecutor.abort(coordinateSearch); simplex.clear(); simplex.add(current); @@ -160,24 +167,31 @@ public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Stati */ halveStep(simplex); functionExecutor.compute(simplex); - functionExecutor.waitFor(coordinateSearch); - if (accept(coordinateSearch, currentValue)) { + if (useCoordinateSearch) { + functionExecutor.waitFor(coordinateSearch); - 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))); + 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; + reductionFallback++; } - toAbort.removeAll(simplex); - functionExecutor.abort(toAbort); - simplexComputed = false; - coordinateAcceptance++; - } else { - log.debug("Coordinate search unsuccessful, halving step."); + log.debug("Coordinate search not used, halving step."); step /= 2; reductionFallback++; } @@ -186,12 +200,14 @@ public class MultidirectionalSearchOptimizer implements FunctionOptimizer, Stati 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(); } - } while (control.stepTaken(current, currentValue, simplex.get(0), - functionExecutor.getValue(simplex.get(0)), step)); + } } catch (InterruptedException e) { log.info("Optimization was interrupted with InterruptedException"); diff --git a/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java b/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java index 592f4eaa..1a7df333 100644 --- a/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java +++ b/src/net/sf/openrocket/optimization/general/multidim/SearchPattern.java @@ -58,7 +58,7 @@ public class SearchPattern { for (int j = 0; j < i; j++) { value -= MathUtil.pow2(coordinates[j]); } - value = Math.sqrt(value); + value = MathUtil.safeSqrt(value); coordinates[i] = value; pattern.add(new Point(coordinates)); diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java index e8506e8d..1a8f3ff1 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/OptimizableParameter.java @@ -2,6 +2,7 @@ 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 @@ -30,4 +31,11 @@ public interface OptimizableParameter { */ public double computeValue(Simulation simulation) throws OptimizationException; + + /** + * 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/RocketOptimizationFunction.java b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java index b0e8e843..2f224269 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationFunction.java @@ -67,16 +67,13 @@ public class RocketOptimizationFunction implements Function { @Override public double evaluate(Point point) throws InterruptedException, OptimizationException { - System.out.println("Evaluating function at point " + point); - /* * parameterValue is the computed parameter value (e.g. altitude) * goalValue is the value that needs to be minimized */ double goalValue, parameterValue; - - log.verbose("Computing optimization function value at point " + point); + log.debug("Computing optimization function value at point " + point); // Create the new simulation based on the point double[] p = point.asArray(); @@ -92,19 +89,18 @@ public class RocketOptimizationFunction implements Function { // Check whether the point is within the simulation domain - Pair d = domain.getDistanceToDomain(simulation); + Pair d = domain.getDistanceToDomain(simulation); double distance = d.getU(); - double referenceValue = d.getV(); + 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.verbose("Optimization point is outside of domain, distance=" + distance + " goal function value=" + goalValue); - System.out.println("Optimization point is outside of domain, distance=" + distance + " goal function value=" + goalValue); + log.debug("Optimization point is outside of domain, distance=" + distance + " goal function value=" + goalValue); - fireEvent(simulation, point, referenceValue, Double.NaN, goalValue); + fireEvent(simulation, point, referenceValue, null, goalValue); return goalValue; } @@ -122,9 +118,9 @@ public class RocketOptimizationFunction implements Function { } log.verbose("Parameter value at point " + point + " is " + parameterValue + ", goal function value=" + goalValue); - System.out.println("Parameter value at point " + point + " is " + parameterValue + ", goal function value=" + goalValue); - fireEvent(simulation, point, referenceValue, parameterValue, goalValue); + fireEvent(simulation, point, referenceValue, new Value(parameterValue, parameter.getUnitGroup().getDefaultUnit()), + goalValue); return goalValue; } @@ -168,7 +164,7 @@ public class RocketOptimizationFunction implements Function { - private void fireEvent(Simulation simulation, Point p, double domainReference, double parameterValue, double goalValue) + private void fireEvent(Simulation simulation, Point p, Value domainReference, Value parameterValue, double goalValue) throws OptimizationException { if (listeners.isEmpty()) { diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java index dac89c05..05fc429b 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/RocketOptimizationListener.java @@ -14,11 +14,11 @@ 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 the order of "point". - * @param domainReference the domain reference value (or NaN if unavailable) + * @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, double domainReference, double parameterValue, double goalValue); + 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 index d97eaae4..d2a204ec 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationDomain.java @@ -1,6 +1,7 @@ package net.sf.openrocket.optimization.rocketoptimization; import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.unit.Value; import net.sf.openrocket.util.Pair; /** @@ -18,11 +19,12 @@ public interface SimulationDomain { * is NaN, the simulation is outside of the domain. * * @param simulation the simulation to check. - * @return the double parameter is the domain indication; + * @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 a human-readable + * a positive value or NaN if not. The second is the value * indication of the domain (may be null). */ - public Pair getDistanceToDomain(Simulation simulation); + public Pair getDistanceToDomain(Simulation simulation); + } diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java index 3efd5d6d..1989ea07 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/SimulationModifier.java @@ -17,11 +17,16 @@ import net.sf.openrocket.util.ChangeSource; public interface SimulationModifier extends ChangeSource { /** - * Return a name describing this modifier. + * 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 @@ -94,4 +99,13 @@ public interface SimulationModifier extends ChangeSource { */ 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). + */ + @Override + 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 index 2f398be3..fac678ea 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/domains/IdentitySimulationDomain.java @@ -2,6 +2,7 @@ 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; /** @@ -12,8 +13,8 @@ import net.sf.openrocket.util.Pair; public class IdentitySimulationDomain implements SimulationDomain { @Override - public Pair getDistanceToDomain(Simulation simulation) { - return new Pair(-1.0, Double.NaN); + public Pair getDistanceToDomain(Simulation simulation) { + return new Pair(-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 index 0142368d..143ef29a 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/domains/StabilityDomain.java @@ -11,37 +11,53 @@ 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.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; import net.sf.openrocket.util.Prefs; /** - * A simulation domain that limits the requires stability of the rocket. + * A simulation domain that limits the required stability of the rocket. * * @author Sampo Niskanen */ public class StabilityDomain implements SimulationDomain { - /* - * TODO: HIGH: Should this rather inspect stability during flight - */ - - private final double limit; - private final boolean absolute; + private final double minimum; + private final boolean minAbsolute; + private final double maximum; + private final boolean maxAbsolute; - public StabilityDomain(double limit, boolean absolute) { - this.limit = limit; - this.absolute = absolute; + /** + * 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) { + public Pair getDistanceToDomain(Simulation simulation) { Coordinate cp, cg; double cpx, cgx; - double reference; + double absolute; + double relative; /* * These are instantiated each time because this class must be thread-safe. @@ -73,23 +89,51 @@ public class StabilityDomain implements SimulationDomain { // Calculate the reference (absolute or relative) - reference = 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); - } + 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); } - - reference = (cpx - cgx) / diameter; } + relative = absolute / diameter; - System.out.println("DOMAIN: limit=" + limit + " reference=" + reference + " result=" + (limit - reference)); + + Value desc; + if (minAbsolute && maxAbsolute) { + desc = new Value(absolute, UnitGroup.UNITS_LENGTH); + } else { + desc = new Value(relative, UnitGroup.UNITS_STABILITY_CALIBERS); + } - return new Pair(limit - reference, reference); + 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/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java index 5d24e446..f680dda7 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/AbstractSimulationModifier.java @@ -22,6 +22,7 @@ import net.sf.openrocket.util.MathUtil; public abstract class AbstractSimulationModifier implements SimulationModifier { private final String name; + private final String description; private final Object relatedObject; private final UnitGroup unitGroup; @@ -34,14 +35,23 @@ public abstract class AbstractSimulationModifier implements SimulationModifier { /** * Sole constructor. * - * @param modifierName the name of this modifier (returned by {@link #getName()}) - * @param relatedObject the related object (returned by {@link #getRelatedObject()}) - * @param unitGroup the unit group (returned by {@link #getUnitGroup()}) + * @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, Object relatedObject, UnitGroup unitGroup) { + 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); + } } @@ -50,6 +60,11 @@ public abstract class AbstractSimulationModifier implements SimulationModifier { return name; } + @Override + public String getDescription() { + return description; + } + @Override public Object getRelatedObject() { return relatedObject; @@ -155,4 +170,40 @@ public abstract class AbstractSimulationModifier implements SimulationModifier { } } + @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 index b22d9332..bd80889a 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericComponentModifier.java @@ -19,18 +19,19 @@ public class GenericComponentModifier extends GenericModifier { /** * Sole constructor. * - * @param modifierName the name of this modifier (returned by {@link #getName()}) - * @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") + * @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, Object relatedObject, UnitGroup unitGroup, + public GenericComponentModifier(String modifierName, String modifierDescription, Object relatedObject, UnitGroup unitGroup, double multiplier, Class componentClass, String componentId, String methodName) { - super(modifierName, relatedObject, unitGroup, multiplier, componentClass, methodName); + super(modifierName, modifierDescription, relatedObject, unitGroup, multiplier, componentClass, methodName); this.componentClass = componentClass; this.componentId = componentId; diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java index f2cb67f7..ec931108 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/modifiers/GenericModifier.java @@ -29,6 +29,7 @@ public abstract class GenericModifier extends AbstractSimulationModifier { * 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 @@ -36,9 +37,9 @@ public abstract class GenericModifier extends AbstractSimulationModifier { * @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, Object relatedObject, UnitGroup unitGroup, double multiplier, - Class modifiedClass, String methodName) { - super(modifierName, relatedObject, unitGroup); + 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; diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java new file mode 100644 index 00000000..431e9ae5 --- /dev/null +++ b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/GroundHitVelocityParameter.java @@ -0,0 +1,55 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +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.FlightDataType; +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.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 implements OptimizableParameter { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + public double computeValue(Simulation simulation) throws OptimizationException { + try { + log.debug("Running simulation to evaluate ground hit speed"); + simulation.simulate(); + double value = simulation.getSimulatedData().getBranch(0).getLast(FlightDataType.TYPE_VELOCITY_TOTAL); + log.debug("Ground hit speed 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 zero altitude + return Double.NaN; + } catch (SimulationException e) { + // Other exceptions fail + throw new OptimizationException(e); + } + } + + @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 new file mode 100644 index 00000000..4d6f5713 --- /dev/null +++ b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/LandingDistanceParameter.java @@ -0,0 +1,55 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +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.FlightDataType; +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.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 implements OptimizableParameter { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + public double computeValue(Simulation simulation) throws OptimizationException { + try { + log.debug("Running simulation to evaluate rocket landing distance"); + simulation.simulate(); + double value = simulation.getSimulatedData().getBranch(0).getLast(FlightDataType.TYPE_POSITION_XY); + log.debug("Landing distance 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 zero altitude + return Double.NaN; + } catch (SimulationException e) { + // Other exceptions fail + throw new OptimizationException(e); + } + } + + @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 new file mode 100644 index 00000000..51f73d82 --- /dev/null +++ b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAccelerationParameter.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +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.FlightDataType; +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.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 implements OptimizableParameter { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + public double computeValue(Simulation simulation) throws OptimizationException { + try { + log.debug("Running simulation to evaluate maximum acceleration"); + simulation.simulate(new ApogeeEndListener()); + double value = simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_ACCELERATION_TOTAL); + log.debug("Maximum acceleration 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 zero velocity + return Double.NaN; + } catch (SimulationException e) { + // Other exceptions fail + throw new OptimizationException(e); + } + } + + @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 index da40a676..248d1cf9 100644 --- a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java +++ b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumAltitudeParameter.java @@ -1,11 +1,17 @@ package net.sf.openrocket.optimization.rocketoptimization.parameters; import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +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.FlightDataType; +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.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. @@ -14,21 +20,36 @@ import net.sf.openrocket.simulation.listeners.system.ApogeeEndListener; */ public class MaximumAltitudeParameter implements OptimizableParameter { + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + @Override public String getName() { - return "Maximum altitude"; + return trans.get("name"); } @Override public double computeValue(Simulation simulation) throws OptimizationException { try { - System.out.println("Running simulation"); + log.debug("Running simulation to evaluate apogee altitude"); simulation.simulate(new ApogeeEndListener()); - System.out.println("Maximum altitude was " + simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE)); + log.debug("Maximum altitude was " + simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE)); return simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_ALTITUDE); + } 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 zero altitude + return 0.0; } catch (SimulationException e) { + // Other exceptions fail throw new OptimizationException(e); } } + @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 new file mode 100644 index 00000000..58c7a278 --- /dev/null +++ b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/MaximumVelocityParameter.java @@ -0,0 +1,56 @@ +package net.sf.openrocket.optimization.rocketoptimization.parameters; + +import net.sf.openrocket.document.Simulation; +import net.sf.openrocket.l10n.Translator; +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.FlightDataType; +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.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 implements OptimizableParameter { + + private static final LogHelper log = Application.getLogger(); + private static final Translator trans = Application.getTranslator(); + + @Override + public String getName() { + return trans.get("name"); + } + + @Override + public double computeValue(Simulation simulation) throws OptimizationException { + try { + log.debug("Running simulation to evaluate maximum velocity"); + simulation.simulate(new ApogeeEndListener()); + double value = simulation.getSimulatedData().getBranch(0).getMaximum(FlightDataType.TYPE_VELOCITY_TOTAL); + log.debug("Maximum velocity 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 zero velocity + return Double.NaN; + } catch (SimulationException e) { + // Other exceptions fail + throw new OptimizationException(e); + } + } + + @Override + public UnitGroup getUnitGroup() { + return UnitGroup.UNITS_VELOCITY; + } + +} diff --git a/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java new file mode 100644 index 00000000..a9f099c2 --- /dev/null +++ b/src/net/sf/openrocket/optimization/rocketoptimization/parameters/StabilityParameter.java @@ -0,0 +1,111 @@ +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; +import net.sf.openrocket.util.Prefs; + +/** + * 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(Prefs.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/services/DefaultOptimizableParameterService.java b/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java index 00178d0e..65b9ab98 100644 --- a/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java +++ b/src/net/sf/openrocket/optimization/services/DefaultOptimizableParameterService.java @@ -6,7 +6,12 @@ import java.util.List; import net.sf.openrocket.document.OpenRocketDocument; import net.sf.openrocket.optimization.rocketoptimization.OptimizableParameter; +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; /** * Default implementation for optimization parameter service. @@ -20,6 +25,12 @@ public class DefaultOptimizableParameterService implements OptimizableParameterS 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()); return list; } diff --git a/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java b/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java index 977cd186..565330aa 100644 --- a/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java +++ b/src/net/sf/openrocket/optimization/services/DefaultSimulationModifierService.java @@ -7,21 +7,41 @@ 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); @@ -33,23 +53,56 @@ public class DefaultSimulationModifierService implements SimulationModifierServi */ 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"); - addModifier("optimization.modifier.nosecone.thickness", UnitGroup.UNITS_LENGTH, 1.0, NoseCone.class, "Thickness"); + 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"); - addModifier("optimization.modifier.transition.aftdiameter", UnitGroup.UNITS_LENGTH, 2.0, Transition.class, "AftRadius"); - addModifier("optimization.modifier.transition.thickness", UnitGroup.UNITS_LENGTH, 1.0, Transition.class, "Thickness"); + 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"); - addModifier("optimization.modifier.bodytube.thickness", UnitGroup.UNITS_LENGTH, 1.0, BodyTube.class, "Thickness"); + 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) { + Class componentClass, String methodName, String autoMethod) { + + String modifierDescriptionKey = modifierNameKey + ".desc"; List list = definitions.get(componentClass); if (list == null) { @@ -57,7 +110,8 @@ public class DefaultSimulationModifierService implements SimulationModifierServi definitions.put(componentClass, list); } - ModifierDefinition definition = new ModifierDefinition(modifierNameKey, unitGroup, multiplier, componentClass, methodName); + ModifierDefinition definition = new ModifierDefinition(modifierNameKey, modifierDescriptionKey, unitGroup, + multiplier, componentClass, methodName, autoMethod); list.add(definition); } @@ -69,29 +123,182 @@ public class DefaultSimulationModifierService implements SimulationModifierServi 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), c, def.unitGroup, def.multiplier, def.componentClass, - c.getID(), def.methodName); + 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); } } - // TODO: HIGH: Conditional modifiers (overrides) + + // 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); + + } + } - // TODO: Transition / Nose cone shape parameter (conditional) + + // 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 @@ -99,20 +306,23 @@ public class DefaultSimulationModifierService implements SimulationModifierServi 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, UnitGroup unitGroup, double multiplier, - Class componentClass, String methodName) { - super(); + 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/src/net/sf/openrocket/rocketcomponent/Configuration.java b/src/net/sf/openrocket/rocketcomponent/Configuration.java index 9ab9c0b3..be00f1d4 100644 --- a/src/net/sf/openrocket/rocketcomponent/Configuration.java +++ b/src/net/sf/openrocket/rocketcomponent/Configuration.java @@ -328,7 +328,7 @@ public class Configuration implements Cloneable, ChangeSource, ComponentChangeLi /** * Perform a deep-clone. The object references are also cloned and no - * listeners are listening on the cloned object. + * listeners are listening on the cloned object. The rocket instance remains the same. */ @Override public Configuration clone() { diff --git a/src/net/sf/openrocket/rocketcomponent/FinSet.java b/src/net/sf/openrocket/rocketcomponent/FinSet.java index d628478a..5e40810b 100644 --- a/src/net/sf/openrocket/rocketcomponent/FinSet.java +++ b/src/net/sf/openrocket/rocketcomponent/FinSet.java @@ -513,7 +513,7 @@ public abstract class FinSet extends ExternalComponent { double radius = getBodyRadius(); - return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius)); + return fins * (inertia + MathUtil.pow2(MathUtil.safeSqrt(h2) + radius)); } @@ -538,9 +538,9 @@ public abstract class FinSet extends ExternalComponent { double h = getSpan(); if (MathUtil.equals(w * h, 0)) { - h = Math.sqrt(area); + h = MathUtil.safeSqrt(area); } else { - h = Math.sqrt(h * area / w); + h = MathUtil.safeSqrt(h * area / w); } if (fins == 1) diff --git a/src/net/sf/openrocket/rocketcomponent/MassComponent.java b/src/net/sf/openrocket/rocketcomponent/MassComponent.java index 47911aa0..49c139b0 100644 --- a/src/net/sf/openrocket/rocketcomponent/MassComponent.java +++ b/src/net/sf/openrocket/rocketcomponent/MassComponent.java @@ -4,9 +4,15 @@ 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; diff --git a/src/net/sf/openrocket/rocketcomponent/Parachute.java b/src/net/sf/openrocket/rocketcomponent/Parachute.java index 0ce768f9..4403d255 100644 --- a/src/net/sf/openrocket/rocketcomponent/Parachute.java +++ b/src/net/sf/openrocket/rocketcomponent/Parachute.java @@ -8,7 +8,7 @@ import net.sf.openrocket.util.Prefs; public class Parachute extends RecoveryDevice { private static final Translator trans = Application.getTranslator(); - + public static final double DEFAULT_CD = 0.8; private double diameter; @@ -94,7 +94,7 @@ public class Parachute extends RecoveryDevice { public void setArea(double area) { if (MathUtil.equals(getArea(), area)) return; - diameter = Math.sqrt(area / Math.PI) * 2; + diameter = MathUtil.safeSqrt(area / Math.PI) * 2; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } diff --git a/src/net/sf/openrocket/rocketcomponent/Streamer.java b/src/net/sf/openrocket/rocketcomponent/Streamer.java index dd071a40..8d73c457 100644 --- a/src/net/sf/openrocket/rocketcomponent/Streamer.java +++ b/src/net/sf/openrocket/rocketcomponent/Streamer.java @@ -60,7 +60,7 @@ public class Streamer extends RecoveryDevice { ratio = Math.max(ratio, 0.01); double area = getArea(); - stripWidth = Math.sqrt(area / ratio); + stripWidth = MathUtil.safeSqrt(area / ratio); stripLength = ratio * stripWidth; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } @@ -76,7 +76,7 @@ public class Streamer extends RecoveryDevice { return; double ratio = Math.max(getAspectRatio(), 0.01); - stripWidth = Math.sqrt(area / ratio); + stripWidth = MathUtil.safeSqrt(area / ratio); stripLength = ratio * stripWidth; fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE); } diff --git a/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java b/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java index ac8318de..275de205 100644 --- a/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java +++ b/src/net/sf/openrocket/rocketcomponent/SymmetricComponent.java @@ -21,12 +21,12 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial 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 + 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; @@ -38,11 +38,11 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial private Coordinate cg = null; - + public SymmetricComponent() { super(); } - + /** * Return the component radius at position x. @@ -51,15 +51,21 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial * 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); } @@ -76,14 +82,14 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial } - + /** * Return the component wall thickness. */ public double getThickness() { if (filled) - return Math.max(getForeRadius(),getAftRadius()); - return Math.min(thickness,Math.max(getForeRadius(),getAftRadius())); + return Math.max(getForeRadius(), getAftRadius()); + return Math.min(thickness, Math.max(getForeRadius(), getAftRadius())); } @@ -94,11 +100,11 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial public void setThickness(double thickness) { if ((this.thickness == thickness) && !filled) return; - this.thickness = MathUtil.clamp(thickness,0,Math.max(getForeRadius(),getAftRadius())); + this.thickness = MathUtil.clamp(thickness, 0, Math.max(getForeRadius(), getAftRadius())); filled = false; fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } - + /** * Returns whether the component is set as filled. If it is set filled, then the @@ -120,23 +126,23 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE); } - + /** * Adds component bounds at a number of points between 0...length. */ @Override public Collection getComponentBounds() { List list = new ArrayList(20); - for (int n=0; n<=5; n++) { - double x = n*length/5; + for (int n = 0; n <= 5; n++) { + double x = n * length / 5; double r = getRadius(x); - addBound(list,x,r); + addBound(list, x, r); } return list; } - + /** * 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 @@ -231,7 +237,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial @Override public double getLongitudinalUnitInertia() { if (longitudinalInertia < 0) { - if (getComponentVolume() > 0.0000001) // == 0.1cm^3 + if (getComponentVolume() > 0.0000001) // == 0.1cm^3 integrateInertiaVolume(); else integrateInertiaSurface(); @@ -257,7 +263,7 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial * Performs integration over the length of the component and updates the cached variables. */ private void integrate() { - double x,r1,r2; + double x, r1, r2; double cgx; // Check length > 0 @@ -270,12 +276,12 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial 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 + 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; @@ -285,44 +291,44 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial volume = 0; cgx = 0; - for (int n=1; n<=DIVISIONS; n++) { + 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); - 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 0) planCenter /= planArea; - if (volume < 0.0000000001) { // 0.1 mm^3 + if (volume < 0.0000000001) { // 0.1 mm^3 volume = 0; - cg = new Coordinate(length/2, 0, 0, 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()); + cg = new Coordinate(cgx / volume, 0, 0, super.getComponentMass()); } } @@ -352,11 +358,11 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial */ 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 - + + 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; @@ -364,34 +370,34 @@ public abstract class SymmetricComponent extends BodyComponent implements Radial double volume = 0; - for (int n=1; n<=DIVISIONS; n++) { + 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; - + r2 = getRadius(x + l); + final double outer = (r1 + r2) / 2; + // Volume differential elements final double inner; final double dV; - if (filled || r1= 0; x = x * radius / length; - return sqrt(2 * radius * x - x * x); // radius/length * sphere + return MathUtil.safeSqrt(2 * radius * x - x * x); // radius/length * sphere } }, @@ -699,10 +699,10 @@ public class Transition extends SymmetricComponent { assert param <= 2; double theta = Math.acos(1 - 2 * x / length); - if (param == 0) { - return radius * sqrt((theta - sin(2 * theta) / 2) / Math.PI); + if (MathUtil.equals(param, 0)) { + return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2) / Math.PI); } - return radius * sqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI); + return radius * MathUtil.safeSqrt((theta - sin(2 * theta) / 2 + param * pow3(sin(theta))) / Math.PI); } }, diff --git a/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java b/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java index 21ccb266..7bc635c7 100644 --- a/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java +++ b/src/net/sf/openrocket/simulation/BasicEventSimulationEngine.java @@ -16,6 +16,7 @@ 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; @@ -51,7 +52,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { Configuration configuration = setupConfiguration(simulationConditions); MotorInstanceConfiguration motorConfiguration = setupMotorConfiguration(configuration); if (motorConfiguration.getMotorIDs().isEmpty()) { - throw new SimulationLaunchException("No motors defined."); + throw new MotorIgnitionException("No motors defined in the simulation."); } // Initialize the simulation @@ -161,7 +162,9 @@ public class BasicEventSimulationEngine implements SimulationEngine { flightData.addBranch(status.getFlightData()); - log.info("Warnings at the end of simulation: " + flightData.getWarningSet()); + if (!flightData.getWarningSet().isEmpty()) { + log.info("Warnings at the end of simulation: " + flightData.getWarningSet()); + } // TODO: HIGH: Simulate branches return flightData; @@ -494,7 +497,7 @@ public class BasicEventSimulationEngine implements SimulationEngine { // If no motor has ignited, abort if (!status.isMotorIgnited()) { - throw new SimulationLaunchException("No motors ignited."); + throw new MotorIgnitionException("No motors ignited."); } return ret; diff --git a/src/net/sf/openrocket/simulation/GUISimulationConditions.java b/src/net/sf/openrocket/simulation/GUISimulationConditions.java deleted file mode 100644 index d0b83911..00000000 --- a/src/net/sf/openrocket/simulation/GUISimulationConditions.java +++ /dev/null @@ -1,471 +0,0 @@ -package net.sf.openrocket.simulation; - -import java.util.ArrayList; -import java.util.List; -import java.util.Random; - -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - -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.BasicGravityModel; -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.MathUtil; - -// TODO: HIGH: Move somewhere else and clean up -@Deprecated -public class GUISimulationConditions 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; - - private double launchAltitude = 0; - private double launchLatitude = 45; - - private boolean useISA = true; - private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE; - private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE; - private AtmosphericModel atmosphericModel = null; - - - private double timeStep = RK4SimulationStepper.RECOMMENDED_TIME_STEP; - private double maximumAngle = RK4SimulationStepper.RECOMMENDED_ANGLE_STEP; - - private boolean calculateExtras = true; - - - private List listeners = new ArrayList(); - - - - public GUISimulationConditions(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 null. - * - * @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 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; - this.atmosphericModel = null; - fireChangeEvent(); - } - - - - public double getLaunchPressure() { - return launchPressure; - } - - - - public void setLaunchPressure(double launchPressure) { - if (MathUtil.equals(this.launchPressure, launchPressure)) - return; - this.launchPressure = launchPressure; - this.atmosphericModel = null; - fireChangeEvent(); - } - - - /** - * Returns an atmospheric model corresponding to the launch conditions. The - * atmospheric models may be shared between different calls. - * - * @return an AtmosphericModel object. - */ - public AtmosphericModel getAtmosphericModel() { - if (useISA) { - return ISA_ATMOSPHERIC_MODEL; - } - if (atmosphericModel == null) { - atmosphericModel = new ExtendedISAModel(launchAltitude, - launchTemperature, launchPressure); - } - return atmosphericModel; - } - - - 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(); - } - - - - @Override - public GUISimulationConditions clone() { - try { - GUISimulationConditions copy = (GUISimulationConditions) super.clone(); - copy.listeners = new ArrayList(); - return copy; - } catch (CloneNotSupportedException e) { - throw new BugException(e); - } - } - - - public void copyFrom(GUISimulationConditions 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.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; - - 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 GUISimulationConditions)) - return false; - GUISimulationConditions o = (GUISimulationConditions) other; - return ((this.rocket == o.rocket) && - this.motorID == o.motorID && - MathUtil.equals(this.launchAltitude, o.launchAltitude) && - MathUtil.equals(this.launchLatitude, o.launchLatitude) && - 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); - } - - /** - * 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(ChangeListener listener) { - listeners.add(listener); - } - - @Override - public void removeChangeListener(ChangeListener listener) { - listeners.remove(listener); - } - - private final ChangeEvent event = new ChangeEvent(this); - - private void fireChangeEvent() { - ChangeListener[] array = listeners.toArray(new ChangeListener[0]); - - for (int i = array.length - 1; i >= 0; i--) { - array[i].stateChanged(event); - } - } - - - // TODO: HIGH: Clean up - @Deprecated - 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.setLaunchAltitude(getLaunchAltitude()); - conditions.setLaunchLatitude(getLaunchLatitude()); - - PinkNoiseWindModel windModel = new PinkNoiseWindModel(); - // TODO: HIGH: Randomness source for simulation - windModel.setSeed(new Random().nextInt()); - windModel.setAverage(getWindSpeedAverage()); - windModel.setStandardDeviation(getWindSpeedDeviation()); - conditions.setWindModel(windModel); - - conditions.setAtmosphericModel(getAtmosphericModel()); - - BasicGravityModel gravityModel = new BasicGravityModel(getLaunchLatitude()); - 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/RK4SimulationStepper.java b/src/net/sf/openrocket/simulation/RK4SimulationStepper.java index 99fd475e..8d5e27db 100644 --- a/src/net/sf/openrocket/simulation/RK4SimulationStepper.java +++ b/src/net/sf/openrocket/simulation/RK4SimulationStepper.java @@ -21,6 +21,9 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { private static final LogHelper log = Application.getLogger(); + /** Random value with which to XOR the random seed value */ + private static final int SEED_RANDOMIZATION = 0x23E3A01F; + /** * A recommended reasonably accurate time step. @@ -50,8 +53,7 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { private static final double MIN_TIME_STEP = 0.001; - // TODO: HIGH: Randomness source from simulation - private final Random random = new Random(); + private Random random; @@ -59,8 +61,6 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { @Override public RK4SimulationStatus initialize(SimulationStatus original) { - log.info("Performing RK4SimulationStepper initialization"); - RK4SimulationStatus status = new RK4SimulationStatus(); status.copyFrom(original); @@ -72,6 +72,8 @@ public class RK4SimulationStepper extends AbstractSimulationStepper { Math.cos(sim.getLaunchRodAngle()) )); + this.random = new Random(original.getSimulationConditions().getRandomSeed() ^ SEED_RANDOMIZATION); + return status; } diff --git a/src/net/sf/openrocket/simulation/SimulationConditions.java b/src/net/sf/openrocket/simulation/SimulationConditions.java index 16177090..3e582086 100644 --- a/src/net/sf/openrocket/simulation/SimulationConditions.java +++ b/src/net/sf/openrocket/simulation/SimulationConditions.java @@ -57,12 +57,13 @@ public class SimulationConditions implements Monitorable, Cloneable { private List simulationListeners = new ArrayList(); + private int randomSeed = 0; + private int modID = 0; private int modIDadd = 0; - public AerodynamicCalculator getAerodynamicCalculator() { return aerodynamicCalculator; } @@ -240,6 +241,18 @@ public class SimulationConditions implements Monitorable, Cloneable { } + + public int getRandomSeed() { + return randomSeed; + } + + + public void setRandomSeed(int randomSeed) { + this.randomSeed = randomSeed; + this.modID++; + } + + // TODO: HIGH: Make cleaner public List getSimulationListenerList() { return simulationListeners; diff --git a/src/net/sf/openrocket/simulation/SimulationOptions.java b/src/net/sf/openrocket/simulation/SimulationOptions.java new file mode 100644 index 00000000..05a22b1f --- /dev/null +++ b/src/net/sf/openrocket/simulation/SimulationOptions.java @@ -0,0 +1,498 @@ +package net.sf.openrocket.simulation; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +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.BasicGravityModel; +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.MathUtil; + +/** + * 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 + */ +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; + + private double launchAltitude = 0; + private double launchLatitude = 45; + + 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 listeners = new ArrayList(); + + + + 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 null. + * + * @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 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(launchAltitude, 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(); + 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.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) && + this.motorID.equals(o.motorID) && + MathUtil.equals(this.launchAltitude, o.launchAltitude) && + MathUtil.equals(this.launchLatitude, o.launchLatitude) && + 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(ChangeListener listener) { + listeners.add(listener); + } + + @Override + public void removeChangeListener(ChangeListener listener) { + listeners.remove(listener); + } + + private final ChangeEvent event = new ChangeEvent(this); + + private void fireChangeEvent() { + ChangeListener[] array = listeners.toArray(new ChangeListener[0]); + + for (int i = array.length - 1; i >= 0; i--) { + array[i].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.setLaunchAltitude(getLaunchAltitude()); + conditions.setLaunchLatitude(getLaunchLatitude()); + conditions.setRandomSeed(randomSeed); + + PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed); + windModel.setAverage(getWindSpeedAverage()); + windModel.setStandardDeviation(getWindSpeedDeviation()); + conditions.setWindModel(windModel); + + conditions.setAtmosphericModel(getAtmosphericModel()); + + BasicGravityModel gravityModel = new BasicGravityModel(getLaunchLatitude()); + 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/exception/MotorIgnitionException.java b/src/net/sf/openrocket/simulation/exception/MotorIgnitionException.java new file mode 100644 index 00000000..fd9e60ac --- /dev/null +++ b/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 + */ +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/SimulationLaunchException.java b/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java index 1c840e35..df9f3862 100644 --- a/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java +++ b/src/net/sf/openrocket/simulation/exception/SimulationLaunchException.java @@ -7,21 +7,13 @@ package net.sf.openrocket.simulation.exception; * @author Sampo Niskanen */ public class SimulationLaunchException extends SimulationException { - - public SimulationLaunchException() { - - } - + public SimulationLaunchException(String message) { super(message); } - - public SimulationLaunchException(Throwable cause) { - super(cause); - } - + public SimulationLaunchException(String message, Throwable cause) { super(message, cause); } - + } diff --git a/src/net/sf/openrocket/startup/Application.java b/src/net/sf/openrocket/startup/Application.java index a7847d32..bb8b20f7 100644 --- a/src/net/sf/openrocket/startup/Application.java +++ b/src/net/sf/openrocket/startup/Application.java @@ -89,10 +89,6 @@ public final class Application { * @return a translator. */ public static Translator getTranslator() { - if (baseTranslator instanceof DebugTranslator) { - return baseTranslator; - } - Translator t = baseTranslator; t = new ClassBasedTranslator(t, 1); t = new ExceptionSuppressingTranslator(t); diff --git a/src/net/sf/openrocket/unit/CaliberUnit.java b/src/net/sf/openrocket/unit/CaliberUnit.java index c33c4b6f..cc93cb51 100644 --- a/src/net/sf/openrocket/unit/CaliberUnit.java +++ b/src/net/sf/openrocket/unit/CaliberUnit.java @@ -1,16 +1,12 @@ package net.sf.openrocket.unit; -import java.util.Collection; -import java.util.Collections; import java.util.Iterator; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; - 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; @@ -21,19 +17,14 @@ public class CaliberUnit extends GeneralUnit { private final Configuration configuration; private final Rocket rocket; - private double caliber = -1; + private int rocketModId = -1; + private int configurationModId = -1; - - /* Listener for rocket and configuration, resets the caliber to -1. */ - private final ChangeListener listener = new ChangeListener() { - @Override - public void stateChanged(ChangeEvent e) { - caliber = -1; - } - }; + private double caliber = -1; + public CaliberUnit(Configuration configuration) { super(1.0, "cal"); this.configuration = configuration; @@ -42,7 +33,6 @@ public class CaliberUnit extends GeneralUnit { this.rocket = null; } else { this.rocket = configuration.getRocket(); - configuration.addChangeListener(listener); } } @@ -50,53 +40,94 @@ public class CaliberUnit extends GeneralUnit { super(1.0, "cal"); this.configuration = null; this.rocket = rocket; - if (rocket != null) { - rocket.addChangeListener(listener); + } + + 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) { - if (caliber < 0) - calculateCaliber(); + checkCaliber(); return value * caliber; } @Override public double toUnit(double value) { - if (caliber < 0) - calculateCaliber(); + checkCaliber(); return value / caliber; } - // TODO: HIGH: Check caliber calculation method... - private void calculateCaliber() { - caliber = 0; - - Iterator iterator; - if (configuration != null) { - iterator = configuration.iterator(); - } else if (rocket != null) { - iterator = rocket.iterator(false); - } else { - Collection set = Collections.emptyList(); - iterator = set.iterator(); + + 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 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; - caliber = MathUtil.max(caliber, r1, r2); + cal = MathUtil.max(cal, r1, r2); } } - if (caliber <= 0) - caliber = DEFAULT_CALIBER; + if (cal < 0.0001) + cal = DEFAULT_CALIBER; + + return cal; } } diff --git a/src/net/sf/openrocket/unit/UnitGroup.java b/src/net/sf/openrocket/unit/UnitGroup.java index 0f75d827..920e7664 100644 --- a/src/net/sf/openrocket/unit/UnitGroup.java +++ b/src/net/sf/openrocket/unit/UnitGroup.java @@ -31,6 +31,11 @@ public class UnitGroup { 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; @@ -111,6 +116,10 @@ public class UnitGroup { 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")); @@ -204,8 +213,11 @@ public class UnitGroup { UNITS_RELATIVE = new UnitGroup(); UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("" + ZWSP, 0.01, 1.0)); - UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("%", 1, 0.01)); + 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); @@ -309,22 +321,44 @@ public class UnitGroup { } - + /** + * 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); + } + + ////////////////////////////////////////////////////// - private ArrayList units = new ArrayList(); - private int defaultUnit = 0; + protected ArrayList units = new ArrayList(); + protected int defaultUnit = 0; public int getUnitCount() { return units.size(); @@ -391,18 +425,10 @@ public class UnitGroup { return units.indexOf(u); } - public void addUnit(Unit u) { + private void addUnit(Unit u) { units.add(u); } - public void addUnit(int n, Unit u) { - units.add(n, u); - } - - public void removeUnit(int n) { - units.remove(n); - } - public boolean contains(Unit u) { return units.contains(u); } @@ -508,80 +534,33 @@ public class UnitGroup { */ private static class StabilityUnitGroup extends UnitGroup { - private final CaliberUnit caliberUnit; - + public StabilityUnitGroup(double ref) { + this(new CaliberUnit(ref)); + } public StabilityUnitGroup(Rocket rocket) { - caliberUnit = new CaliberUnit(rocket); + this(new CaliberUnit(rocket)); } public StabilityUnitGroup(Configuration config) { - caliberUnit = new CaliberUnit(config); - } - - - //// Modify CaliberUnit to use local variable - - @Override - public Unit getDefaultUnit() { - return getUnit(UNITS_STABILITY.getDefaultUnitIndex()); + this(new CaliberUnit(config)); } - @Override - public Unit getUnit(int n) { - Unit u = UNITS_STABILITY.getUnit(n); - if (u instanceof CaliberUnit) { - return caliberUnit; - } - return u; - } - - @Override - public int getUnitIndex(Unit u) { - if (u instanceof CaliberUnit) { - for (int i = 0; i < UNITS_STABILITY.getUnitCount(); i++) { - if (UNITS_STABILITY.getUnit(i) instanceof CaliberUnit) - return i; + 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); } } - return UNITS_STABILITY.getUnitIndex(u); } - - //// Pass on to UNITS_STABILITY - - @Override - public int getDefaultUnitIndex() { - return UNITS_STABILITY.getDefaultUnitIndex(); - } - @Override public void setDefaultUnit(int n) { + super.setDefaultUnit(n); UNITS_STABILITY.setDefaultUnit(n); } - - @Override - public int getUnitCount() { - return UNITS_STABILITY.getUnitCount(); - } - - - //// Unsupported methods - - @Override - public void addUnit(int n, Unit u) { - throw new UnsupportedOperationException("StabilityUnitGroup must not be modified"); - } - - @Override - public void addUnit(Unit u) { - throw new UnsupportedOperationException("StabilityUnitGroup must not be modified"); - } - - @Override - public void removeUnit(int n) { - throw new UnsupportedOperationException("StabilityUnitGroup must not be modified"); - } } } diff --git a/src/net/sf/openrocket/unit/Value.java b/src/net/sf/openrocket/unit/Value.java index 278b815a..65382523 100644 --- a/src/net/sf/openrocket/unit/Value.java +++ b/src/net/sf/openrocket/unit/Value.java @@ -3,7 +3,7 @@ package net.sf.openrocket.unit; import net.sf.openrocket.util.MathUtil; /** - * A class representing an SI value and a unit. The toString() method yields the + * 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. @@ -12,8 +12,8 @@ import net.sf.openrocket.util.MathUtil; */ public class Value implements Comparable { - private double value; - private Unit unit; + private final double value; + private final Unit unit; /** @@ -44,7 +44,7 @@ public class Value implements Comparable { /** - * Get the value of this object. + * Get the value of this object (in SI units). * * @return the value */ @@ -52,16 +52,8 @@ public class Value implements Comparable { return value; } - /** - * Set the value of this object. - * - * @param value the value to set - */ - public void setValue(double value) { - this.value = value; - } - + /** * Get the value of this object in the current units. * @@ -72,16 +64,6 @@ public class Value implements Comparable { } - /** - * Set the value of this object in the current units. - * - * @param value the value in current units. - */ - public void setUnitValue(double value) { - this.value = unit.fromUnit(value); - } - - /** * Get the unit of this object. * @@ -91,18 +73,6 @@ public class Value implements Comparable { return unit; } - /** - * Set the value of this object. - * - * @param unit the unit to set (null not allowed) - */ - public void setUnit(Unit unit) { - if (unit == null) { - throw new IllegalArgumentException("unit is null"); - } - this.unit = unit; - } - /** * Return a string formatted using the {@link Unit#toStringUnit(double)} method diff --git a/src/net/sf/openrocket/util/Chars.java b/src/net/sf/openrocket/util/Chars.java index 08d2f827..1920bfa8 100644 --- a/src/net/sf/openrocket/util/Chars.java +++ b/src/net/sf/openrocket/util/Chars.java @@ -6,7 +6,7 @@ package net.sf.openrocket.util; * @author Sampo Niskanen */ public class Chars { - + /** The fraction 1/2 */ public static final char FRAC12 = '\u00BD'; /** The fraction 1/4 */ @@ -37,7 +37,7 @@ public class Chars { /** Micro sign (Greek letter mu) */ public static final char MICRO = '\u00B5'; - + /** Alpha */ public static final char ALPHA = '\u03b1'; /** Theta */ @@ -47,4 +47,10 @@ public class Chars { 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/Coordinate.java b/src/net/sf/openrocket/util/Coordinate.java index 909cf273..3bc981aa 100644 --- a/src/net/sf/openrocket/util/Coordinate.java +++ b/src/net/sf/openrocket/util/Coordinate.java @@ -206,7 +206,7 @@ public final class Coordinate implements Serializable { */ public double length() { if (length < 0) { - length = Math.sqrt(x * x + y * y + z * z); + length = MathUtil.safeSqrt(x * x + y * y + z * z); } return length; } diff --git a/src/net/sf/openrocket/util/FileHelper.java b/src/net/sf/openrocket/util/FileHelper.java index 6a8216ff..0929c328 100644 --- a/src/net/sf/openrocket/util/FileHelper.java +++ b/src/net/sf/openrocket/util/FileHelper.java @@ -24,7 +24,7 @@ public final class FileHelper { private static final Translator trans = Application.getTranslator(); - // TODO: MEDIUM: Rename translation keys + // TODO: HIGH: Rename translation keys /** File filter for any rocket designs (*.ork, *.rkt) */ public static final FileFilter ALL_DESIGNS_FILTER = @@ -43,7 +43,12 @@ public final class FileHelper { 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() { diff --git a/src/net/sf/openrocket/util/GUIUtil.java b/src/net/sf/openrocket/util/GUIUtil.java index 10d632e2..831f2381 100644 --- a/src/net/sf/openrocket/util/GUIUtil.java +++ b/src/net/sf/openrocket/util/GUIUtil.java @@ -52,6 +52,7 @@ 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; @@ -265,6 +266,32 @@ public class GUIUtil { } + /** + * 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) { + /* + * There's been an NPE caused by the font changing, this is debug for it. + */ + if (border == null) { + throw new BugException("border is null"); + } + Font font = border.getTitleFont(); + if (font == null) { + throw new BugException("Border font is null"); + } + 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 diff --git a/src/net/sf/openrocket/util/MathUtil.java b/src/net/sf/openrocket/util/MathUtil.java index 58585d77..75b18943 100644 --- a/src/net/sf/openrocket/util/MathUtil.java +++ b/src/net/sf/openrocket/util/MathUtil.java @@ -6,7 +6,12 @@ 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 /** @@ -192,6 +197,25 @@ public class MathUtil { } + /** + * 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); diff --git a/src/net/sf/openrocket/util/MutableCoordinate.java b/src/net/sf/openrocket/util/MutableCoordinate.java deleted file mode 100644 index 02af9357..00000000 --- a/src/net/sf/openrocket/util/MutableCoordinate.java +++ /dev/null @@ -1,312 +0,0 @@ -package net.sf.openrocket.util; - -import java.io.Serializable; - -/** - * 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 - */ -public final class MutableCoordinate implements Serializable { - public static final MutableCoordinate NUL = new MutableCoordinate(0,0,0,0); - public static final MutableCoordinate NaN = new MutableCoordinate(Double.NaN,Double.NaN, - Double.NaN,Double.NaN); - public static final double COMPARISON_DELTA = 0.000001; - private double x,y,z; - private double weight; - - - /* Count and report the number of times a Coordinate is constructed: */ -// private static int count=0; -// { -// count++; -// if ((count % 1000) == 0) { -// System.out.println("Coordinate instantiated "+count+" times"); -// } -// } - - - public MutableCoordinate() { - x=0; - y=0; - z=0; - weight=0; - } - - public MutableCoordinate(double x) { - this.x = x; - this.y = 0; - this.z = 0; - weight = 0; - } - - public MutableCoordinate(double x, double y) { - this.x = x; - this.y = y; - this.z = 0; - weight = 0; - } - - public MutableCoordinate(double x, double y, double z) { - this.x = x; - this.y = y; - this.z = z; - weight = 0; - } - public MutableCoordinate(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); - } - - public boolean isNaN() { - return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight); - } - - public MutableCoordinate setX(double x) { - return new MutableCoordinate(x,this.y,this.z,this.weight); - } - - public MutableCoordinate setY(double y) { - return new MutableCoordinate(this.x,y,this.z,this.weight); - } - - public MutableCoordinate setZ(double z) { - return new MutableCoordinate(this.x,this.y,z,this.weight); - } - - public MutableCoordinate setWeight(double weight) { - return new MutableCoordinate(this.x, this.y, this.z, weight); - } - - public MutableCoordinate setXYZ(MutableCoordinate c) { - return new MutableCoordinate(c.x, c.y, c.z, this.weight); - } - - public double getX() { - return x; - } - public double getY() { - return y; - } - public double getZ() { - return z; - } - - - /** - * Add the coordinate and weight of two coordinates. - * - * @param other the other Coordinate - * @return the sum of the coordinates - */ - public MutableCoordinate add(MutableCoordinate other) { - this.x += other.x; - this.y += other.y; - this.z += other.z; - this.weight += other.weight; - return this; - } - - public MutableCoordinate add(double x, double y, double z) { - this.x += x; - this.y += y; - this.z += z; - return this; - } - - public MutableCoordinate add(double x, double y, double z, double weight) { - return new MutableCoordinate(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 MutableCoordinate sub(MutableCoordinate other) { - return new MutableCoordinate(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 MutableCoordinate sub(double x, double y, double z) { - return new MutableCoordinate(this.x - x, this.y - y, this.z - z, this.weight); - } - - - /** - * Multiply the Coordinate with a scalar. All coordinates and the - * weight are multiplied by the given scalar. - - * @param m Factor to multiply by. - * @return The product. - */ - public MutableCoordinate multiply(double m) { - return new MutableCoordinate(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(MutableCoordinate other) { - return this.x*other.x + this.y*other.y + this.z*other.z; - } - /** - * Dot product of two Coordinates. - */ - public static double dot(MutableCoordinate v1, MutableCoordinate v2) { - return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; - } - - /** - * Distance from the origin to the Coordinate. - */ - public double length() { - return Math.sqrt(x*x+y*y+z*z); - } - - /** - * Square of the distance from the origin to the Coordinate. - */ - public double length2() { - return x*x+y*y+z*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 IllegalStateException. 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 MutableCoordinate normalize() { - double l = length(); - if (l < 0.0000001) { - throw new IllegalStateException("Cannot normalize zero coordinate"); - } - return new MutableCoordinate(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. - *

- * If other is null then this Coordinate is - * returned. - */ - public MutableCoordinate average(MutableCoordinate other) { - double x,y,z,w; - - if (other == null) - return this; - - w = this.weight + other.weight; - if (MathUtil.equals(w, 0)) { - 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 MutableCoordinate(x,y,z,w); - } - - - /** - * Tests whether the coordinates (not weight!) are the same. - * - * Compares only the (x,y,z) coordinates, NOT the weight. Coordinate comparison is - * done to the precision of COMPARISON_DELTA. - * - * @param other Coordinate to compare to. - * @return true if the coordinates are equal - */ - @Override - public boolean equals(Object other) { - if (!(other instanceof MutableCoordinate)) - return false; - - final MutableCoordinate c = (MutableCoordinate)other; - return (MathUtil.equals(this.x, c.x) && - MathUtil.equals(this.y, c.y) && - MathUtil.equals(this.z, c.z)); - } - - /** - * 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); - } - - - - public static void main(String[] arg) { - double a=1.2; - double x; - MutableCoordinate c; - long t1, t2; - - x = 0; - t1 = System.nanoTime(); - for (int i=0; i < 100000000; i++) { - x = x + a; - } - t2 = System.nanoTime(); - System.out.println("Value: "+x); - System.out.println("Plain addition: "+ ((t2-t1+500000)/1000000) + " ms"); - - c = MutableCoordinate.NUL; - t1 = System.nanoTime(); - for (int i=0; i < 100000000; i++) { - c = c.add(a,0,0); - } - t2 = System.nanoTime(); - System.out.println("Value: "+c.x); - System.out.println("Coordinate addition: "+ ((t2-t1+500000)/1000000) + " ms"); - - } - -} diff --git a/src/net/sf/openrocket/util/Prefs.java b/src/net/sf/openrocket/util/Prefs.java index 16e338c4..2d59f776 100644 --- a/src/net/sf/openrocket/util/Prefs.java +++ b/src/net/sf/openrocket/util/Prefs.java @@ -38,7 +38,7 @@ import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.Rocket; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.simulation.FlightDataType; -import net.sf.openrocket.simulation.GUISimulationConditions; +import net.sf.openrocket.simulation.SimulationOptions; import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.startup.Application; import net.sf.openrocket.unit.UnitGroup; @@ -758,7 +758,7 @@ public class Prefs { public static Simulation getBackgroundSimulation(Rocket rocket) { Simulation s = new Simulation(rocket); - GUISimulationConditions cond = s.getConditions(); + SimulationOptions cond = s.getOptions(); cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2); cond.setWindSpeedAverage(1.0); diff --git a/src/net/sf/openrocket/util/Quaternion.java b/src/net/sf/openrocket/util/Quaternion.java index f51eadd2..5f277662 100644 --- a/src/net/sf/openrocket/util/Quaternion.java +++ b/src/net/sf/openrocket/util/Quaternion.java @@ -216,7 +216,7 @@ public class Quaternion { */ public double norm() { if (norm < 0) { - norm = Math.sqrt(x * x + y * y + z * z + w * w); + norm = MathUtil.safeSqrt(x * x + y * y + z * z + w * w); } return norm; } diff --git a/test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java b/test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java index 0205eddf..48cc648b 100644 --- a/test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java +++ b/test/net/sf/openrocket/optimization/rocketoptimization/TestRocketOptimizationFunction.java @@ -5,6 +5,7 @@ import net.sf.openrocket.document.Simulation; import net.sf.openrocket.optimization.general.OptimizationException; import net.sf.openrocket.optimization.general.Point; import net.sf.openrocket.rocketcomponent.Rocket; +import net.sf.openrocket.unit.Unit; import net.sf.openrocket.unit.UnitGroup; import net.sf.openrocket.unit.Value; import net.sf.openrocket.util.Pair; @@ -43,8 +44,9 @@ public class TestRocketOptimizationFunction { final double p1 = 0.4; final double p2 = 0.7; final double ddist = -0.43; - final double dref = 0.33; + final Value dref = new Value(ddist, Unit.NOUNIT2); final double pvalue = 9.81; + final Value pvalueValue = new Value(9.81, Unit.NOUNIT2); final double gvalue = 8.81; final Point point = new Point(p1, p2); @@ -52,8 +54,9 @@ public class TestRocketOptimizationFunction { context.checking(new Expectations() {{ oneOf(modifier1).modify(simulation, p1); oneOf(modifier2).modify(simulation, p2); - oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); + oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); oneOf(parameter).computeValue(simulation); will(returnValue(pvalue)); + oneOf(parameter).getUnitGroup(); will(returnValue(UnitGroup.UNITS_NONE)); oneOf(goal).getMinimizationParameter(pvalue); will(returnValue(gvalue)); oneOf(modifier1).getCurrentSIValue(simulation); will(returnValue(0.2)); oneOf(modifier1).getUnitGroup(); will(returnValue(UnitGroup.UNITS_LENGTH)); @@ -62,7 +65,7 @@ public class TestRocketOptimizationFunction { oneOf(listener).evaluated(point, new Value[] { new Value(0.2, UnitGroup.UNITS_LENGTH.getDefaultUnit()), new Value(0.3, UnitGroup.UNITS_LENGTH.getDefaultUnit()) - }, dref, pvalue, gvalue); + }, dref, pvalueValue, gvalue); }}); // @formatter:on @@ -87,15 +90,16 @@ public class TestRocketOptimizationFunction { final double p1 = 0.4; final double p2 = 0.7; final double ddist = -0.43; - final double dref = 0.33; + final Value dref = new Value(0.33, Unit.NOUNIT2); final double pvalue = 9.81; // @formatter:off context.checking(new Expectations() {{ oneOf(modifier1).modify(simulation, p1); oneOf(modifier2).modify(simulation, p2); - oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); + oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); oneOf(parameter).computeValue(simulation); will(returnValue(pvalue)); + oneOf(parameter).getUnitGroup(); will(returnValue(UnitGroup.UNITS_NONE)); oneOf(goal).getMinimizationParameter(pvalue); will(returnValue(Double.NaN)); }}); // @formatter:on @@ -123,14 +127,14 @@ public class TestRocketOptimizationFunction { final double p1 = 0.4; final double p2 = 0.7; final double ddist = 0.98; - final double dref = 0.33; + final Value dref = new Value(ddist, Unit.NOUNIT2); final Point point = new Point(p1, p2); // @formatter:off context.checking(new Expectations() {{ oneOf(modifier1).modify(simulation, p1); oneOf(modifier2).modify(simulation, p2); - oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); + oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); oneOf(modifier1).getCurrentSIValue(simulation); will(returnValue(0.2)); oneOf(modifier1).getUnitGroup(); will(returnValue(UnitGroup.UNITS_LENGTH)); oneOf(modifier2).getCurrentSIValue(simulation); will(returnValue(0.3)); @@ -138,7 +142,7 @@ public class TestRocketOptimizationFunction { oneOf(listener).evaluated(point, new Value[] { new Value(0.2, UnitGroup.UNITS_LENGTH.getDefaultUnit()), new Value(0.3, UnitGroup.UNITS_LENGTH.getDefaultUnit()) - }, dref, Double.NaN, 1.98E200); + }, dref, null, 1.98E200); }}); // @formatter:on @@ -164,13 +168,13 @@ public class TestRocketOptimizationFunction { final double p1 = 0.4; final double p2 = 0.7; final double ddist = Double.NaN; - final double dref = 0.33; + final Value dref = new Value(0.33, Unit.NOUNIT2); // @formatter:off context.checking(new Expectations() {{ oneOf(modifier1).modify(simulation, p1); oneOf(modifier2).modify(simulation, p2); - oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); + oneOf(domain).getDistanceToDomain(simulation); will(returnValue(new Pair(ddist, dref))); }}); // @formatter:on diff --git a/test/net/sf/openrocket/optimization/rocketoptimization/modifiers/TestGenericModifier.java b/test/net/sf/openrocket/optimization/rocketoptimization/modifiers/TestGenericModifier.java index 6bdcb96e..deacacf4 100644 --- a/test/net/sf/openrocket/optimization/rocketoptimization/modifiers/TestGenericModifier.java +++ b/test/net/sf/openrocket/optimization/rocketoptimization/modifiers/TestGenericModifier.java @@ -22,7 +22,9 @@ public class TestGenericModifier { value = new TestValue(); sim = new Simulation(new Rocket()); - gm = new GenericModifier("Test modifier", null, + Object related = new Object(); + + gm = new GenericModifier("Test modifier", "Description", related, UnitGroup.UNITS_NONE, 2.0, TestValue.class, "value") { @Override protected TestValue getModifiedObject(Simulation simulation) { diff --git a/test/net/sf/openrocket/unit/ValueTest.java b/test/net/sf/openrocket/unit/ValueTest.java index 0c8c61ff..9440c4d0 100644 --- a/test/net/sf/openrocket/unit/ValueTest.java +++ b/test/net/sf/openrocket/unit/ValueTest.java @@ -5,7 +5,7 @@ import static org.junit.Assert.*; import org.junit.Test; public class ValueTest { - + @Test public void testValues() { Value v1, v2; @@ -18,21 +18,21 @@ public class ValueTest { assertTrue(v1.compareTo(v1) == 0); assertTrue(v2.compareTo(v2) == 0); - v2.setUnit(UnitGroup.UNITS_TEMPERATURE.findApproximate("K")); + v2 = new Value(283.15, UnitGroup.UNITS_TEMPERATURE.findApproximate("K")); assertTrue(v1.compareTo(v2) > 0); assertTrue(v2.compareTo(v1) < 0); assertEquals("283 K", v2.toString()); - - v2.setUnit(UnitGroup.UNITS_TEMPERATURE.findApproximate("F")); + + v2 = new Value(283.15, UnitGroup.UNITS_TEMPERATURE.findApproximate("F")); assertTrue(v1.compareTo(v2) < 0); assertTrue(v2.compareTo(v1) > 0); - - v1.setValue(Double.NaN); + + v1 = new Value(Double.NaN, UnitGroup.UNITS_TEMPERATURE.findApproximate("F")); assertTrue(v1.compareTo(v2) > 0); assertTrue(v2.compareTo(v1) < 0); - v2.setValue(Double.NaN); + v2 = new Value(Double.NaN, UnitGroup.UNITS_TEMPERATURE.findApproximate("F")); assertTrue(v1.compareTo(v2) == 0); assertTrue(v1.compareTo(v2) == 0); assertEquals("N/A", v1.toString()); diff --git a/test/net/sf/openrocket/util/TextUtilTest.java b/test/net/sf/openrocket/util/TextUtilTest.java index 3b7385f0..8ac8319d 100644 --- a/test/net/sf/openrocket/util/TextUtilTest.java +++ b/test/net/sf/openrocket/util/TextUtilTest.java @@ -10,153 +10,153 @@ import org.junit.Test; public class TextUtilTest { @Test - public void textHexString() { + public void testHexString() { assertEquals("", TextUtil.hexString(new byte[0])); assertEquals("00", TextUtil.hexString(new byte[] { 0x00 })); assertEquals("ff", TextUtil.hexString(new byte[] { (byte) 0xff })); - - for (int i=0; i <= 0xff; i++) { + + for (int i = 0; i <= 0xff; i++) { assertEquals(String.format("%02x", i), TextUtil.hexString(new byte[] { (byte) i })); } - assertEquals("0f1e2d3c4b5a6978", TextUtil.hexString(new byte[] { + assertEquals("0f1e2d3c4b5a6978", TextUtil.hexString(new byte[] { 0x0f, 0x1e, 0x2d, 0x3c, 0x4b, 0x5a, 0x69, 0x78 })); Random rnd = new Random(); - for (int count=0; count<10; count++) { + for (int count = 0; count < 10; count++) { int n = rnd.nextInt(100); byte[] bytes = new byte[n]; rnd.nextBytes(bytes); StringBuilder sb = new StringBuilder(); - for (byte b: bytes) { + for (byte b : bytes) { sb.append(String.format("%02x", b & 0xFF)); } assertEquals(sb.toString(), TextUtil.hexString(bytes)); } } - + @Test public void specialCaseTest() { - assertEquals("NaN",TextUtil.doubleToString(Double.NaN)); - assertEquals("Inf",TextUtil.doubleToString(Double.POSITIVE_INFINITY)); - assertEquals("-Inf",TextUtil.doubleToString(Double.NEGATIVE_INFINITY)); - assertEquals("0",TextUtil.doubleToString(0.0)); - assertEquals("0",TextUtil.doubleToString(MathUtil.EPSILON/3)); - assertEquals("0",TextUtil.doubleToString(-MathUtil.EPSILON/3)); + assertEquals("NaN", TextUtil.doubleToString(Double.NaN)); + assertEquals("Inf", TextUtil.doubleToString(Double.POSITIVE_INFINITY)); + assertEquals("-Inf", TextUtil.doubleToString(Double.NEGATIVE_INFINITY)); + assertEquals("0", TextUtil.doubleToString(0.0)); + assertEquals("0", TextUtil.doubleToString(MathUtil.EPSILON / 3)); + assertEquals("0", TextUtil.doubleToString(-MathUtil.EPSILON / 3)); } @Test public void longTest() { - assertEquals("3.1416e-5", TextUtil.doubleToString(PI*1e-5)); - assertEquals("3.1416e-4", TextUtil.doubleToString(PI*1e-4)); - assertEquals("0.0031416", TextUtil.doubleToString(PI*1e-3)); - assertEquals("0.031416", TextUtil.doubleToString(PI*1e-2)); - assertEquals("0.31416", TextUtil.doubleToString(PI*1e-1)); - assertEquals("3.1416", TextUtil.doubleToString(PI)); - assertEquals("31.416", TextUtil.doubleToString(PI*1e1)); - assertEquals("314.16", TextUtil.doubleToString(PI*1e2)); - assertEquals("3141.6", TextUtil.doubleToString(PI*1e3)); - assertEquals("31416", TextUtil.doubleToString(PI*1e4)); - assertEquals("314159", TextUtil.doubleToString(PI*1e5)); - assertEquals("3141593", TextUtil.doubleToString(PI*1e6)); - assertEquals("31415927", TextUtil.doubleToString(PI*1e7)); - assertEquals("3.1416e8", TextUtil.doubleToString(PI*1e8)); - assertEquals("3.1416e9", TextUtil.doubleToString(PI*1e9)); - assertEquals("3.1416e10", TextUtil.doubleToString(PI*1e10)); - - assertEquals("-3.1416e-5", TextUtil.doubleToString(-PI*1e-5)); - assertEquals("-3.1416e-4", TextUtil.doubleToString(-PI*1e-4)); - assertEquals("-0.0031416", TextUtil.doubleToString(-PI*1e-3)); - assertEquals("-0.031416", TextUtil.doubleToString(-PI*1e-2)); - assertEquals("-0.31416", TextUtil.doubleToString(-PI*1e-1)); - assertEquals("-3.1416", TextUtil.doubleToString(-PI)); - assertEquals("-31.416", TextUtil.doubleToString(-PI*1e1)); - assertEquals("-314.16", TextUtil.doubleToString(-PI*1e2)); - assertEquals("-3141.6", TextUtil.doubleToString(-PI*1e3)); - assertEquals("-31416", TextUtil.doubleToString(-PI*1e4)); - assertEquals("-314159", TextUtil.doubleToString(-PI*1e5)); - assertEquals("-3141593", TextUtil.doubleToString(-PI*1e6)); - assertEquals("-31415927", TextUtil.doubleToString(-PI*1e7)); - assertEquals("-3.1416e8", TextUtil.doubleToString(-PI*1e8)); - assertEquals("-3.1416e9", TextUtil.doubleToString(-PI*1e9)); - assertEquals("-3.1416e10", TextUtil.doubleToString(-PI*1e10)); - + assertEquals("3.1416e-5", TextUtil.doubleToString(PI * 1e-5)); + assertEquals("3.1416e-4", TextUtil.doubleToString(PI * 1e-4)); + assertEquals("0.0031416", TextUtil.doubleToString(PI * 1e-3)); + assertEquals("0.031416", TextUtil.doubleToString(PI * 1e-2)); + assertEquals("0.31416", TextUtil.doubleToString(PI * 1e-1)); + assertEquals("3.1416", TextUtil.doubleToString(PI)); + assertEquals("31.416", TextUtil.doubleToString(PI * 1e1)); + assertEquals("314.16", TextUtil.doubleToString(PI * 1e2)); + assertEquals("3141.6", TextUtil.doubleToString(PI * 1e3)); + assertEquals("31416", TextUtil.doubleToString(PI * 1e4)); + assertEquals("314159", TextUtil.doubleToString(PI * 1e5)); + assertEquals("3141593", TextUtil.doubleToString(PI * 1e6)); + assertEquals("31415927", TextUtil.doubleToString(PI * 1e7)); + assertEquals("3.1416e8", TextUtil.doubleToString(PI * 1e8)); + assertEquals("3.1416e9", TextUtil.doubleToString(PI * 1e9)); + assertEquals("3.1416e10", TextUtil.doubleToString(PI * 1e10)); + + assertEquals("-3.1416e-5", TextUtil.doubleToString(-PI * 1e-5)); + assertEquals("-3.1416e-4", TextUtil.doubleToString(-PI * 1e-4)); + assertEquals("-0.0031416", TextUtil.doubleToString(-PI * 1e-3)); + assertEquals("-0.031416", TextUtil.doubleToString(-PI * 1e-2)); + assertEquals("-0.31416", TextUtil.doubleToString(-PI * 1e-1)); + assertEquals("-3.1416", TextUtil.doubleToString(-PI)); + assertEquals("-31.416", TextUtil.doubleToString(-PI * 1e1)); + assertEquals("-314.16", TextUtil.doubleToString(-PI * 1e2)); + assertEquals("-3141.6", TextUtil.doubleToString(-PI * 1e3)); + assertEquals("-31416", TextUtil.doubleToString(-PI * 1e4)); + assertEquals("-314159", TextUtil.doubleToString(-PI * 1e5)); + assertEquals("-3141593", TextUtil.doubleToString(-PI * 1e6)); + assertEquals("-31415927", TextUtil.doubleToString(-PI * 1e7)); + assertEquals("-3.1416e8", TextUtil.doubleToString(-PI * 1e8)); + assertEquals("-3.1416e9", TextUtil.doubleToString(-PI * 1e9)); + assertEquals("-3.1416e10", TextUtil.doubleToString(-PI * 1e10)); + } @Test public void shortTest() { double p = 3.1; - assertEquals("3.1e-5", TextUtil.doubleToString(p*1e-5)); - assertEquals("3.1e-4", TextUtil.doubleToString(p*1e-4)); - assertEquals("0.0031", TextUtil.doubleToString(p*1e-3)); - assertEquals("0.031", TextUtil.doubleToString(p*1e-2)); - assertEquals("0.31", TextUtil.doubleToString(p*1e-1)); - assertEquals("3.1", TextUtil.doubleToString(p)); - assertEquals("31", TextUtil.doubleToString(p*1e1)); - assertEquals("310", TextUtil.doubleToString(p*1e2)); - assertEquals("3100", TextUtil.doubleToString(p*1e3)); - assertEquals("31000", TextUtil.doubleToString(p*1e4)); - assertEquals("3.1e5", TextUtil.doubleToString(p*1e5)); - assertEquals("3.1e6", TextUtil.doubleToString(p*1e6)); - assertEquals("3.1e7", TextUtil.doubleToString(p*1e7)); - assertEquals("3.1e8", TextUtil.doubleToString(p*1e8)); - assertEquals("3.1e9", TextUtil.doubleToString(p*1e9)); - assertEquals("3.1e10", TextUtil.doubleToString(p*1e10)); - - assertEquals("-3.1e-5", TextUtil.doubleToString(-p*1e-5)); - assertEquals("-3.1e-4", TextUtil.doubleToString(-p*1e-4)); - assertEquals("-0.0031", TextUtil.doubleToString(-p*1e-3)); - assertEquals("-0.031", TextUtil.doubleToString(-p*1e-2)); - assertEquals("-0.31", TextUtil.doubleToString(-p*1e-1)); - assertEquals("-3.1", TextUtil.doubleToString(-p)); - assertEquals("-31", TextUtil.doubleToString(-p*1e1)); - assertEquals("-310", TextUtil.doubleToString(-p*1e2)); - assertEquals("-3100", TextUtil.doubleToString(-p*1e3)); - assertEquals("-31000", TextUtil.doubleToString(-p*1e4)); - assertEquals("-3.1e5", TextUtil.doubleToString(-p*1e5)); - assertEquals("-3.1e6", TextUtil.doubleToString(-p*1e6)); - assertEquals("-3.1e7", TextUtil.doubleToString(-p*1e7)); - assertEquals("-3.1e8", TextUtil.doubleToString(-p*1e8)); - assertEquals("-3.1e9", TextUtil.doubleToString(-p*1e9)); - assertEquals("-3.1e10", TextUtil.doubleToString(-p*1e10)); - + assertEquals("3.1e-5", TextUtil.doubleToString(p * 1e-5)); + assertEquals("3.1e-4", TextUtil.doubleToString(p * 1e-4)); + assertEquals("0.0031", TextUtil.doubleToString(p * 1e-3)); + assertEquals("0.031", TextUtil.doubleToString(p * 1e-2)); + assertEquals("0.31", TextUtil.doubleToString(p * 1e-1)); + assertEquals("3.1", TextUtil.doubleToString(p)); + assertEquals("31", TextUtil.doubleToString(p * 1e1)); + assertEquals("310", TextUtil.doubleToString(p * 1e2)); + assertEquals("3100", TextUtil.doubleToString(p * 1e3)); + assertEquals("31000", TextUtil.doubleToString(p * 1e4)); + assertEquals("3.1e5", TextUtil.doubleToString(p * 1e5)); + assertEquals("3.1e6", TextUtil.doubleToString(p * 1e6)); + assertEquals("3.1e7", TextUtil.doubleToString(p * 1e7)); + assertEquals("3.1e8", TextUtil.doubleToString(p * 1e8)); + assertEquals("3.1e9", TextUtil.doubleToString(p * 1e9)); + assertEquals("3.1e10", TextUtil.doubleToString(p * 1e10)); + + assertEquals("-3.1e-5", TextUtil.doubleToString(-p * 1e-5)); + assertEquals("-3.1e-4", TextUtil.doubleToString(-p * 1e-4)); + assertEquals("-0.0031", TextUtil.doubleToString(-p * 1e-3)); + assertEquals("-0.031", TextUtil.doubleToString(-p * 1e-2)); + assertEquals("-0.31", TextUtil.doubleToString(-p * 1e-1)); + assertEquals("-3.1", TextUtil.doubleToString(-p)); + assertEquals("-31", TextUtil.doubleToString(-p * 1e1)); + assertEquals("-310", TextUtil.doubleToString(-p * 1e2)); + assertEquals("-3100", TextUtil.doubleToString(-p * 1e3)); + assertEquals("-31000", TextUtil.doubleToString(-p * 1e4)); + assertEquals("-3.1e5", TextUtil.doubleToString(-p * 1e5)); + assertEquals("-3.1e6", TextUtil.doubleToString(-p * 1e6)); + assertEquals("-3.1e7", TextUtil.doubleToString(-p * 1e7)); + assertEquals("-3.1e8", TextUtil.doubleToString(-p * 1e8)); + assertEquals("-3.1e9", TextUtil.doubleToString(-p * 1e9)); + assertEquals("-3.1e10", TextUtil.doubleToString(-p * 1e10)); + p = 3; - assertEquals("3e-5", TextUtil.doubleToString(p*1e-5)); - assertEquals("3e-4", TextUtil.doubleToString(p*1e-4)); - assertEquals("3e-3", TextUtil.doubleToString(p*1e-3)); - assertEquals("0.03", TextUtil.doubleToString(p*1e-2)); - assertEquals("0.3", TextUtil.doubleToString(p*1e-1)); - assertEquals("3", TextUtil.doubleToString(p)); - assertEquals("30", TextUtil.doubleToString(p*1e1)); - assertEquals("300", TextUtil.doubleToString(p*1e2)); - assertEquals("3e3", TextUtil.doubleToString(p*1e3)); - assertEquals("3e4", TextUtil.doubleToString(p*1e4)); - assertEquals("3e5", TextUtil.doubleToString(p*1e5)); - assertEquals("3e6", TextUtil.doubleToString(p*1e6)); - assertEquals("3e7", TextUtil.doubleToString(p*1e7)); - assertEquals("3e8", TextUtil.doubleToString(p*1e8)); - assertEquals("3e9", TextUtil.doubleToString(p*1e9)); - assertEquals("3e10", TextUtil.doubleToString(p*1e10)); - - assertEquals("-3e-5", TextUtil.doubleToString(-p*1e-5)); - assertEquals("-3e-4", TextUtil.doubleToString(-p*1e-4)); - assertEquals("-3e-3", TextUtil.doubleToString(-p*1e-3)); - assertEquals("-0.03", TextUtil.doubleToString(-p*1e-2)); - assertEquals("-0.3", TextUtil.doubleToString(-p*1e-1)); - assertEquals("-3", TextUtil.doubleToString(-p)); - assertEquals("-30", TextUtil.doubleToString(-p*1e1)); - assertEquals("-300", TextUtil.doubleToString(-p*1e2)); - assertEquals("-3e3", TextUtil.doubleToString(-p*1e3)); - assertEquals("-3e4", TextUtil.doubleToString(-p*1e4)); - assertEquals("-3e5", TextUtil.doubleToString(-p*1e5)); - assertEquals("-3e6", TextUtil.doubleToString(-p*1e6)); - assertEquals("-3e7", TextUtil.doubleToString(-p*1e7)); - assertEquals("-3e8", TextUtil.doubleToString(-p*1e8)); - assertEquals("-3e9", TextUtil.doubleToString(-p*1e9)); - assertEquals("-3e10", TextUtil.doubleToString(-p*1e10)); - + assertEquals("3e-5", TextUtil.doubleToString(p * 1e-5)); + assertEquals("3e-4", TextUtil.doubleToString(p * 1e-4)); + assertEquals("3e-3", TextUtil.doubleToString(p * 1e-3)); + assertEquals("0.03", TextUtil.doubleToString(p * 1e-2)); + assertEquals("0.3", TextUtil.doubleToString(p * 1e-1)); + assertEquals("3", TextUtil.doubleToString(p)); + assertEquals("30", TextUtil.doubleToString(p * 1e1)); + assertEquals("300", TextUtil.doubleToString(p * 1e2)); + assertEquals("3e3", TextUtil.doubleToString(p * 1e3)); + assertEquals("3e4", TextUtil.doubleToString(p * 1e4)); + assertEquals("3e5", TextUtil.doubleToString(p * 1e5)); + assertEquals("3e6", TextUtil.doubleToString(p * 1e6)); + assertEquals("3e7", TextUtil.doubleToString(p * 1e7)); + assertEquals("3e8", TextUtil.doubleToString(p * 1e8)); + assertEquals("3e9", TextUtil.doubleToString(p * 1e9)); + assertEquals("3e10", TextUtil.doubleToString(p * 1e10)); + + assertEquals("-3e-5", TextUtil.doubleToString(-p * 1e-5)); + assertEquals("-3e-4", TextUtil.doubleToString(-p * 1e-4)); + assertEquals("-3e-3", TextUtil.doubleToString(-p * 1e-3)); + assertEquals("-0.03", TextUtil.doubleToString(-p * 1e-2)); + assertEquals("-0.3", TextUtil.doubleToString(-p * 1e-1)); + assertEquals("-3", TextUtil.doubleToString(-p)); + assertEquals("-30", TextUtil.doubleToString(-p * 1e1)); + assertEquals("-300", TextUtil.doubleToString(-p * 1e2)); + assertEquals("-3e3", TextUtil.doubleToString(-p * 1e3)); + assertEquals("-3e4", TextUtil.doubleToString(-p * 1e4)); + assertEquals("-3e5", TextUtil.doubleToString(-p * 1e5)); + assertEquals("-3e6", TextUtil.doubleToString(-p * 1e6)); + assertEquals("-3e7", TextUtil.doubleToString(-p * 1e7)); + assertEquals("-3e8", TextUtil.doubleToString(-p * 1e8)); + assertEquals("-3e9", TextUtil.doubleToString(-p * 1e9)); + assertEquals("-3e10", TextUtil.doubleToString(-p * 1e10)); + } @Test @@ -164,42 +164,42 @@ public class TextUtilTest { assertEquals("1.001", TextUtil.doubleToString(1.00096)); - + /* * Not testing with 1.00015 because it might be changed during number formatting * calculations. Its rounding is basically arbitrary anyway. */ - + assertEquals("1.0002e-5", TextUtil.doubleToString(1.0001500001e-5)); assertEquals("1.0001e-5", TextUtil.doubleToString(1.0001499999e-5)); assertEquals("1.0002e-4", TextUtil.doubleToString(1.0001500001e-4)); assertEquals("1.0001e-4", TextUtil.doubleToString(1.0001499999e-4)); assertEquals("0.0010002", TextUtil.doubleToString(1.0001500001e-3)); assertEquals("0.0010001", TextUtil.doubleToString(1.0001499999e-3)); - assertEquals("0.010002", TextUtil.doubleToString(1.0001500001e-2)); - assertEquals("0.010001", TextUtil.doubleToString(1.0001499999e-2)); - assertEquals("0.10002", TextUtil.doubleToString(1.0001500001e-1)); - assertEquals("0.10001", TextUtil.doubleToString(1.0001499999e-1)); - assertEquals("1.0002", TextUtil.doubleToString(1.0001500001)); - assertEquals("1.0001", TextUtil.doubleToString(1.0001499999)); - assertEquals("10.002", TextUtil.doubleToString(1.0001500001e1)); - assertEquals("10.001", TextUtil.doubleToString(1.0001499999e1)); - assertEquals("100.02", TextUtil.doubleToString(1.0001500001e2)); - assertEquals("100.01", TextUtil.doubleToString(1.0001499999e2)); - assertEquals("1000.2", TextUtil.doubleToString(1.0001500001e3)); - assertEquals("1000.1", TextUtil.doubleToString(1.0001499999e3)); - assertEquals("10002", TextUtil.doubleToString(1.0001500001e4)); - assertEquals("10001", TextUtil.doubleToString(1.0001499999e4)); - assertEquals("100012", TextUtil.doubleToString(1.00011500001e5)); - assertEquals("100011", TextUtil.doubleToString(1.00011499999e5)); - assertEquals("1000112", TextUtil.doubleToString(1.000111500001e6)); - assertEquals("1000111", TextUtil.doubleToString(1.000111499999e6)); - assertEquals("10001112", TextUtil.doubleToString(1.0001111500001e7)); - assertEquals("10001111", TextUtil.doubleToString(1.0001111499999e7)); - assertEquals("1.0002e8", TextUtil.doubleToString(1.0001500001e8)); - assertEquals("1.0001e8", TextUtil.doubleToString(1.0001499999e8)); - assertEquals("1.0002e9", TextUtil.doubleToString(1.0001500001e9)); - assertEquals("1.0001e9", TextUtil.doubleToString(1.0001499999e9)); + assertEquals("0.010002", TextUtil.doubleToString(1.0001500001e-2)); + assertEquals("0.010001", TextUtil.doubleToString(1.0001499999e-2)); + assertEquals("0.10002", TextUtil.doubleToString(1.0001500001e-1)); + assertEquals("0.10001", TextUtil.doubleToString(1.0001499999e-1)); + assertEquals("1.0002", TextUtil.doubleToString(1.0001500001)); + assertEquals("1.0001", TextUtil.doubleToString(1.0001499999)); + assertEquals("10.002", TextUtil.doubleToString(1.0001500001e1)); + assertEquals("10.001", TextUtil.doubleToString(1.0001499999e1)); + assertEquals("100.02", TextUtil.doubleToString(1.0001500001e2)); + assertEquals("100.01", TextUtil.doubleToString(1.0001499999e2)); + assertEquals("1000.2", TextUtil.doubleToString(1.0001500001e3)); + assertEquals("1000.1", TextUtil.doubleToString(1.0001499999e3)); + assertEquals("10002", TextUtil.doubleToString(1.0001500001e4)); + assertEquals("10001", TextUtil.doubleToString(1.0001499999e4)); + assertEquals("100012", TextUtil.doubleToString(1.00011500001e5)); + assertEquals("100011", TextUtil.doubleToString(1.00011499999e5)); + assertEquals("1000112", TextUtil.doubleToString(1.000111500001e6)); + assertEquals("1000111", TextUtil.doubleToString(1.000111499999e6)); + assertEquals("10001112", TextUtil.doubleToString(1.0001111500001e7)); + assertEquals("10001111", TextUtil.doubleToString(1.0001111499999e7)); + assertEquals("1.0002e8", TextUtil.doubleToString(1.0001500001e8)); + assertEquals("1.0001e8", TextUtil.doubleToString(1.0001499999e8)); + assertEquals("1.0002e9", TextUtil.doubleToString(1.0001500001e9)); + assertEquals("1.0001e9", TextUtil.doubleToString(1.0001499999e9)); assertEquals("1.0002e10", TextUtil.doubleToString(1.0001500001e10)); assertEquals("1.0001e10", TextUtil.doubleToString(1.0001499999e10)); @@ -210,30 +210,30 @@ public class TextUtilTest { assertEquals("-1.0001e-4", TextUtil.doubleToString(-1.0001499999e-4)); assertEquals("-0.0010002", TextUtil.doubleToString(-1.0001500001e-3)); assertEquals("-0.0010001", TextUtil.doubleToString(-1.0001499999e-3)); - assertEquals("-0.010002", TextUtil.doubleToString(-1.0001500001e-2)); - assertEquals("-0.010001", TextUtil.doubleToString(-1.0001499999e-2)); - assertEquals("-0.10002", TextUtil.doubleToString(-1.0001500001e-1)); - assertEquals("-0.10001", TextUtil.doubleToString(-1.0001499999e-1)); - assertEquals("-1.0002", TextUtil.doubleToString(-1.0001500001)); - assertEquals("-1.0001", TextUtil.doubleToString(-1.0001499999)); - assertEquals("-10.002", TextUtil.doubleToString(-1.0001500001e1)); - assertEquals("-10.001", TextUtil.doubleToString(-1.0001499999e1)); - assertEquals("-100.02", TextUtil.doubleToString(-1.0001500001e2)); - assertEquals("-100.01", TextUtil.doubleToString(-1.0001499999e2)); - assertEquals("-1000.2", TextUtil.doubleToString(-1.0001500001e3)); - assertEquals("-1000.1", TextUtil.doubleToString(-1.0001499999e3)); - assertEquals("-10002", TextUtil.doubleToString(-1.0001500001e4)); - assertEquals("-10001", TextUtil.doubleToString(-1.0001499999e4)); - assertEquals("-100012", TextUtil.doubleToString(-1.00011500001e5)); - assertEquals("-100011", TextUtil.doubleToString(-1.00011499999e5)); - assertEquals("-1000112", TextUtil.doubleToString(-1.000111500001e6)); - assertEquals("-1000111", TextUtil.doubleToString(-1.000111499999e6)); - assertEquals("-10001112", TextUtil.doubleToString(-1.0001111500001e7)); - assertEquals("-10001111", TextUtil.doubleToString(-1.0001111499999e7)); - assertEquals("-1.0002e8", TextUtil.doubleToString(-1.0001500001e8)); - assertEquals("-1.0001e8", TextUtil.doubleToString(-1.0001499999e8)); - assertEquals("-1.0002e9", TextUtil.doubleToString(-1.0001500001e9)); - assertEquals("-1.0001e9", TextUtil.doubleToString(-1.0001499999e9)); + assertEquals("-0.010002", TextUtil.doubleToString(-1.0001500001e-2)); + assertEquals("-0.010001", TextUtil.doubleToString(-1.0001499999e-2)); + assertEquals("-0.10002", TextUtil.doubleToString(-1.0001500001e-1)); + assertEquals("-0.10001", TextUtil.doubleToString(-1.0001499999e-1)); + assertEquals("-1.0002", TextUtil.doubleToString(-1.0001500001)); + assertEquals("-1.0001", TextUtil.doubleToString(-1.0001499999)); + assertEquals("-10.002", TextUtil.doubleToString(-1.0001500001e1)); + assertEquals("-10.001", TextUtil.doubleToString(-1.0001499999e1)); + assertEquals("-100.02", TextUtil.doubleToString(-1.0001500001e2)); + assertEquals("-100.01", TextUtil.doubleToString(-1.0001499999e2)); + assertEquals("-1000.2", TextUtil.doubleToString(-1.0001500001e3)); + assertEquals("-1000.1", TextUtil.doubleToString(-1.0001499999e3)); + assertEquals("-10002", TextUtil.doubleToString(-1.0001500001e4)); + assertEquals("-10001", TextUtil.doubleToString(-1.0001499999e4)); + assertEquals("-100012", TextUtil.doubleToString(-1.00011500001e5)); + assertEquals("-100011", TextUtil.doubleToString(-1.00011499999e5)); + assertEquals("-1000112", TextUtil.doubleToString(-1.000111500001e6)); + assertEquals("-1000111", TextUtil.doubleToString(-1.000111499999e6)); + assertEquals("-10001112", TextUtil.doubleToString(-1.0001111500001e7)); + assertEquals("-10001111", TextUtil.doubleToString(-1.0001111499999e7)); + assertEquals("-1.0002e8", TextUtil.doubleToString(-1.0001500001e8)); + assertEquals("-1.0001e8", TextUtil.doubleToString(-1.0001499999e8)); + assertEquals("-1.0002e9", TextUtil.doubleToString(-1.0001500001e9)); + assertEquals("-1.0001e9", TextUtil.doubleToString(-1.0001499999e9)); assertEquals("-1.0002e10", TextUtil.doubleToString(-1.0001500001e10)); assertEquals("-1.0001e10", TextUtil.doubleToString(-1.0001499999e10)); @@ -242,10 +242,10 @@ public class TextUtilTest { @Test public void randomTest() { - for (int i=0; i<10000; i++) { + for (int i = 0; i < 10000; i++) { double orig = Math.random(); double result; - double expected = Math.rint(orig*100000) / 100000.0; + double expected = Math.rint(orig * 100000) / 100000.0; if (orig < 0.1) continue; diff --git a/web/html/techdoc.pdf b/web/html/techdoc.pdf index 2377e85c..ffb4aada 100644 Binary files a/web/html/techdoc.pdf and b/web/html/techdoc.pdf differ