parent
3e60597ade
commit
f645f580ec
@ -16,6 +16,7 @@ import info.openrocket.core.logging.ErrorSet;
|
||||
import info.openrocket.core.logging.SimulationAbort;
|
||||
import info.openrocket.core.logging.WarningSet;
|
||||
import info.openrocket.core.material.Material;
|
||||
import info.openrocket.core.models.wind.MultiLevelWindModel;
|
||||
import info.openrocket.core.preferences.DocumentPreferences;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -336,8 +337,30 @@ public class OpenRocketSaver extends RocketSaver {
|
||||
writeElement("launchrodlength", cond.getLaunchRodLength());
|
||||
writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
|
||||
writeElement("launchroddirection", cond.getLaunchRodDirection() * 360.0 / (2.0 * Math.PI));
|
||||
writeElement("windaverage", cond.getWindSpeedAverage());
|
||||
writeElement("windturbulence", cond.getWindTurbulenceIntensity());
|
||||
|
||||
// TODO: remove once support for OR 23.09 and prior is dropped
|
||||
writeElement("windaverage", cond.getPinkNoiseWindModel().getAverage());
|
||||
writeElement("windturbulence", cond.getPinkNoiseWindModel().getTurbulenceIntensity());
|
||||
writeElement("winddirection", cond.getPinkNoiseWindModel().getDirection());
|
||||
|
||||
writeln("<wind model=\"pinknoise\">");
|
||||
indent++;
|
||||
writeElement("windaverage", cond.getPinkNoiseWindModel().getAverage());
|
||||
writeElement("windturbulence", cond.getPinkNoiseWindModel().getTurbulenceIntensity());
|
||||
writeElement("winddirection", cond.getPinkNoiseWindModel().getDirection());
|
||||
indent--;
|
||||
writeln("</wind>");
|
||||
|
||||
if (!cond.getMultiLevelWindModel().getLevels().isEmpty()) {
|
||||
writeln("<wind model=\"multilevel\">");
|
||||
indent++;
|
||||
for (MultiLevelWindModel.WindLevel level : cond.getMultiLevelWindModel().getLevels()) {
|
||||
writeln("<windlevel altitude=\"" + level.altitude + "\" speed=\"" + level.speed + "\" direction=\"" + level.direction + "\"/>");
|
||||
}
|
||||
indent--;
|
||||
writeln("</wind>");
|
||||
}
|
||||
|
||||
writeElement("launchaltitude", cond.getLaunchAltitude());
|
||||
writeElement("launchlatitude", cond.getLaunchLatitude());
|
||||
writeElement("launchlongitude", cond.getLaunchLongitude());
|
||||
|
@ -17,6 +17,7 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
public FlightConfigurationId idToSet = FlightConfigurationId.ERROR_FCID;
|
||||
private final SimulationOptions options;
|
||||
private AtmosphereHandler atmosphereHandler;
|
||||
private WindHandler windHandler;
|
||||
|
||||
public SimulationConditionsHandler(Rocket rocket, DocumentLoadingContext context) {
|
||||
this.context = context;
|
||||
@ -32,7 +33,10 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
@Override
|
||||
public ElementHandler openElement(String element, HashMap<String, String> attributes,
|
||||
WarningSet warnings) {
|
||||
if (element.equals("atmosphere")) {
|
||||
if (element.equals("wind")) {
|
||||
windHandler = new WindHandler(attributes.get("model"), options);
|
||||
return windHandler;
|
||||
} else if (element.equals("atmosphere")) {
|
||||
atmosphereHandler = new AtmosphereHandler(attributes.get("model"), context);
|
||||
return atmosphereHandler;
|
||||
}
|
||||
@ -69,19 +73,33 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
} else {
|
||||
options.setLaunchRodDirection(d * 2.0 * Math.PI / 360);
|
||||
}
|
||||
} else if (element.equals("windaverage")) {
|
||||
}
|
||||
// TODO: remove once support for OR 23.09 and prior is dropped
|
||||
else if (element.equals("windaverage")) {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal average windspeed defined, ignoring.");
|
||||
} else {
|
||||
options.setWindSpeedAverage(d);
|
||||
options.getPinkNoiseWindModel().setAverage(d);
|
||||
}
|
||||
} else if (element.equals("windturbulence")) {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal wind turbulence intensity defined, ignoring.");
|
||||
} else {
|
||||
options.setWindTurbulenceIntensity(d);
|
||||
options.getPinkNoiseWindModel().setTurbulenceIntensity(d);
|
||||
}
|
||||
} else if (element.equals("launchaltitude")) {
|
||||
} else if (element.equals("winddirection")) {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal wind direction defined, ignoring.");
|
||||
} else {
|
||||
options.getPinkNoiseWindModel().setDirection(d);
|
||||
}
|
||||
}
|
||||
|
||||
else if (element.equals("wind")) {
|
||||
windHandler.storeSettings(options, warnings);
|
||||
}
|
||||
|
||||
else if (element.equals("launchaltitude")) {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch altitude defined, ignoring.");
|
||||
} else {
|
||||
|
@ -0,0 +1,69 @@
|
||||
package info.openrocket.core.file.openrocket.importt;
|
||||
|
||||
import info.openrocket.core.file.simplesax.AbstractElementHandler;
|
||||
import info.openrocket.core.file.simplesax.ElementHandler;
|
||||
import info.openrocket.core.file.simplesax.PlainTextHandler;
|
||||
import info.openrocket.core.logging.WarningSet;
|
||||
import info.openrocket.core.models.wind.WindModelType;
|
||||
import info.openrocket.core.simulation.SimulationOptions;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class WindHandler extends AbstractElementHandler {
|
||||
private final String model;
|
||||
private final SimulationOptions options;
|
||||
|
||||
public WindHandler(String model, SimulationOptions options) {
|
||||
this.model = model;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementHandler openElement(String element, HashMap<String, String> attributes,
|
||||
WarningSet warnings) {
|
||||
return PlainTextHandler.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeElement(String element, HashMap<String, String> attributes,
|
||||
String content, WarningSet warnings) {
|
||||
double d = Double.NaN;
|
||||
try {
|
||||
d = Double.parseDouble(content);
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
|
||||
if ("pinknoise".equals(model)) {
|
||||
if (element.equals("windaverage")) {
|
||||
if (!Double.isNaN(d)) {
|
||||
options.getPinkNoiseWindModel().setAverage(d);
|
||||
}
|
||||
} else if (element.equals("windturbulence")) {
|
||||
if (!Double.isNaN(d)) {
|
||||
options.getPinkNoiseWindModel().setTurbulenceIntensity(d);
|
||||
}
|
||||
} else if (element.equals("winddirection")) {
|
||||
if (!Double.isNaN(d)) {
|
||||
options.getPinkNoiseWindModel().setDirection(d);
|
||||
}
|
||||
}
|
||||
} else if ("multilevel".equals(model)) {
|
||||
if (element.equals("windlevel")) {
|
||||
double altitude = Double.parseDouble(attributes.get("altitude"));
|
||||
double speed = Double.parseDouble(attributes.get("speed"));
|
||||
double direction = Double.parseDouble(attributes.get("direction"));
|
||||
options.getMultiLevelWindModel().addWindLevel(altitude, speed, direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void storeSettings(SimulationOptions options, WarningSet warnings) {
|
||||
if ("pinknoise".equals(model)) {
|
||||
options.setWindModelType(WindModelType.PINK_NOISE);
|
||||
} else if ("multilevel".equals(model)) {
|
||||
options.setWindModelType(WindModelType.MULTI_LEVEL);
|
||||
} else {
|
||||
warnings.add("Unknown wind model type '" + model + "', using default.");
|
||||
}
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ public class LaunchSiteDTO {
|
||||
setTemperature(RASAeroCommonConstants.OPENROCKET_TO_RASAERO_TEMPERATURE(options.getLaunchTemperature()));
|
||||
setRodAngle(options.getLaunchRodAngle() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_ANGLE);
|
||||
setRodLength(options.getLaunchRodLength() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_ALTITUDE); // It's a length, but stored in RASAero in feet instead of inches
|
||||
setWindSpeed(options.getWindSpeedAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
setWindSpeed(options.getPinkNoiseWindModel().getAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ public class LaunchSiteDTO {
|
||||
setTemperature(RASAeroCommonConstants.OPENROCKET_TO_RASAERO_TEMPERATURE(prefs.getLaunchTemperature()));
|
||||
setRodAngle(prefs.getLaunchRodAngle() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_ANGLE);
|
||||
setRodLength(prefs.getLaunchRodLength() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_ALTITUDE); // It's a length, but stored in RASAero in feet instead of inches
|
||||
setWindSpeed(prefs.getWindSpeedAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
setWindSpeed(prefs.getPinkNoiseWindModel().getAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
}
|
||||
|
||||
public Double getAltitude() {
|
||||
|
@ -59,7 +59,7 @@ public class LaunchSiteHandler extends AbstractElementHandler {
|
||||
launchSiteSettings.setLaunchTemperature(
|
||||
RASAeroCommonConstants.RASAERO_TO_OPENROCKET_TEMPERATURE(Double.parseDouble(content)));
|
||||
} else if (RASAeroCommonConstants.LAUNCH_WIND_SPEED.equals(element)) {
|
||||
launchSiteSettings.setWindSpeedAverage(
|
||||
launchSiteSettings.getPinkNoiseWindModel().setAverage(
|
||||
Double.parseDouble(content) / RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
|
@ -0,0 +1,193 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import info.openrocket.core.util.ModID;
|
||||
|
||||
public class MultiLevelWindModel implements WindModel {
|
||||
|
||||
private List<WindLevel> levels;
|
||||
|
||||
public MultiLevelWindModel() {
|
||||
this.levels = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addWindLevel(double altitude, double speed, double direction) {
|
||||
WindLevel newLevel = new WindLevel(altitude, speed, direction);
|
||||
int index = Collections.binarySearch(levels, newLevel, Comparator.comparingDouble(l -> l.altitude));
|
||||
if (index >= 0) {
|
||||
throw new IllegalArgumentException("Wind level already exists for altitude: " + altitude);
|
||||
}
|
||||
levels.add(-index - 1, newLevel);
|
||||
}
|
||||
|
||||
public void removeWindLevel(double altitude) {
|
||||
levels.removeIf(level -> level.altitude == altitude);
|
||||
}
|
||||
|
||||
public void removeWindLevelIdx(int index) {
|
||||
levels.remove(index);
|
||||
}
|
||||
|
||||
public List<WindLevel> getLevels() {
|
||||
return new ArrayList<>(levels);
|
||||
}
|
||||
|
||||
public void resortLevels() {
|
||||
levels.sort(Comparator.comparingDouble(l -> l.altitude));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Coordinate getWindVelocity(double time, double altitude) {
|
||||
if (levels.isEmpty()) {
|
||||
return Coordinate.ZERO;
|
||||
}
|
||||
|
||||
int index = Collections.binarySearch(levels, new WindLevel(altitude, 0, 0),
|
||||
Comparator.comparingDouble(l -> l.altitude));
|
||||
|
||||
// Retrieve the wind level if it exists
|
||||
if (index >= 0) {
|
||||
return levels.get(index).toCoordinate();
|
||||
}
|
||||
|
||||
// Extrapolation (take the value of the outer bounds)
|
||||
int insertionPoint = -index - 1;
|
||||
if (insertionPoint == 0) {
|
||||
return levels.get(0).toCoordinate();
|
||||
}
|
||||
if (insertionPoint == levels.size()) {
|
||||
return levels.get(levels.size() - 1).toCoordinate();
|
||||
}
|
||||
|
||||
// Interpolation (take the value between the closest two bounds)
|
||||
WindLevel lower = levels.get(insertionPoint - 1);
|
||||
WindLevel upper = levels.get(insertionPoint);
|
||||
|
||||
double fraction = (altitude - lower.altitude) / (upper.altitude - lower.altitude);
|
||||
double speed = MathUtil.interpolate(lower.speed, upper.speed, fraction);
|
||||
double direction = MathUtil.interpolate(lower.direction, upper.direction, fraction);
|
||||
|
||||
return new Coordinate(speed * Math.sin(direction), speed * Math.cos(direction), 0);
|
||||
}
|
||||
|
||||
public double getWindDirection(double altitude) {
|
||||
if (levels.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int index = Collections.binarySearch(levels, new WindLevel(altitude, 0, 0),
|
||||
Comparator.comparingDouble(l -> l.altitude));
|
||||
|
||||
if (index >= 0) {
|
||||
return levels.get(index).direction;
|
||||
}
|
||||
|
||||
int insertionPoint = -index - 1;
|
||||
if (insertionPoint == 0) {
|
||||
return levels.get(0).direction;
|
||||
}
|
||||
if (insertionPoint == levels.size()) {
|
||||
return levels.get(levels.size() - 1).direction;
|
||||
}
|
||||
|
||||
WindLevel lower = levels.get(insertionPoint - 1);
|
||||
WindLevel upper = levels.get(insertionPoint);
|
||||
|
||||
double fraction = (altitude - lower.altitude) / (upper.altitude - lower.altitude);
|
||||
return MathUtil.interpolate(lower.direction, upper.direction, fraction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModID getModID() {
|
||||
return ModID.ZERO; // You might want to create a specific ModID for this model
|
||||
}
|
||||
|
||||
public void loadFrom(MultiLevelWindModel source) {
|
||||
this.levels.clear();
|
||||
for (WindLevel level : source.levels) {
|
||||
this.levels.add(level.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultiLevelWindModel clone() {
|
||||
try {
|
||||
MultiLevelWindModel clone = (MultiLevelWindModel) super.clone();
|
||||
clone.levels = new ArrayList<>(this.levels.size());
|
||||
clone.loadFrom(this);
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError(); // This should never happen
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
MultiLevelWindModel that = (MultiLevelWindModel) o;
|
||||
return levels.equals(that.levels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return levels.hashCode();
|
||||
}
|
||||
|
||||
public static class WindLevel implements Cloneable {
|
||||
public double altitude;
|
||||
public double speed;
|
||||
public double direction;
|
||||
|
||||
public WindLevel(double altitude, double speed, double direction) {
|
||||
this.altitude = altitude;
|
||||
this.speed = speed;
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
Coordinate toCoordinate() {
|
||||
return new Coordinate(speed * Math.sin(direction), speed * Math.cos(direction), 0);
|
||||
}
|
||||
|
||||
public void loadFrom(WindLevel source) {
|
||||
this.altitude = source.altitude;
|
||||
this.speed = source.speed;
|
||||
this.direction = source.direction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindLevel clone() {
|
||||
try {
|
||||
WindLevel clone = (WindLevel) super.clone();
|
||||
clone.loadFrom(this);
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError(); // This should never happen
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
WindLevel windLevel = (WindLevel) o;
|
||||
return Double.compare(windLevel.altitude, altitude) == 0 &&
|
||||
Double.compare(windLevel.speed, speed) == 0 &&
|
||||
Double.compare(windLevel.direction, direction) == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + Double.hashCode(altitude);
|
||||
result = 31 * result + Double.hashCode(speed);
|
||||
result = 31 * result + Double.hashCode(direction);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import info.openrocket.core.util.ModID;
|
||||
import info.openrocket.core.util.PinkNoise;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
/**
|
||||
* A wind simulator that generates wind speed as pink noise from a specified
|
||||
@ -55,6 +58,10 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
this.seed = seed ^ SEED_RANDOMIZATION;
|
||||
}
|
||||
|
||||
public PinkNoiseWindModel() {
|
||||
this(new Random().nextInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average wind speed.
|
||||
*
|
||||
@ -71,13 +78,21 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
* @param average the average wind speed to set
|
||||
*/
|
||||
public void setAverage(double average) {
|
||||
if (average == this.average) {
|
||||
return;
|
||||
}
|
||||
double intensity = getTurbulenceIntensity();
|
||||
this.average = Math.max(average, 0);
|
||||
setTurbulenceIntensity(intensity);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void setDirection(double direction) {
|
||||
if (direction == this.direction) {
|
||||
return;
|
||||
}
|
||||
this.direction = direction;
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getDirection() {
|
||||
@ -99,7 +114,12 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
* @param standardDeviation the standardDeviation to set
|
||||
*/
|
||||
public void setStandardDeviation(double standardDeviation) {
|
||||
if (standardDeviation == this.standardDeviation) {
|
||||
return;
|
||||
}
|
||||
this.standardDeviation = Math.max(standardDeviation, 0);
|
||||
setTurbulenceIntensity(standardDeviation / average);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,9 +181,47 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
randomSource = null;
|
||||
}
|
||||
|
||||
public void loadFrom(PinkNoiseWindModel source) {
|
||||
this.average = source.average;
|
||||
this.direction = source.direction;
|
||||
this.standardDeviation = source.standardDeviation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModID getModID() {
|
||||
return ModID.ZERO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PinkNoiseWindModel clone() {
|
||||
try {
|
||||
PinkNoiseWindModel clone = (PinkNoiseWindModel) super.clone();
|
||||
clone.loadFrom(this);
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError(); // This should never happen
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PinkNoiseWindModel that = (PinkNoiseWindModel) o;
|
||||
return Double.compare(that.average, average) == 0 &&
|
||||
Double.compare(that.standardDeviation, standardDeviation) == 0 &&
|
||||
Double.compare(that.direction, direction) == 0 &&
|
||||
seed == that.seed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + Double.hashCode(average);
|
||||
result = 31 * result + Double.hashCode(standardDeviation);
|
||||
result = 31 * result + Double.hashCode(direction);
|
||||
result = 31 * result + seed;
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,10 +1,40 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import info.openrocket.core.util.ChangeSource;
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.Monitorable;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
public interface WindModel extends Monitorable {
|
||||
import java.util.ArrayList;
|
||||
import java.util.EventListener;
|
||||
import java.util.EventObject;
|
||||
import java.util.List;
|
||||
|
||||
public Coordinate getWindVelocity(double time, double altitude);
|
||||
public interface WindModel extends Monitorable, Cloneable, ChangeSource {
|
||||
List<StateChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
Coordinate getWindVelocity(double time, double altitude);
|
||||
|
||||
WindModel clone();
|
||||
|
||||
@Override
|
||||
default void addChangeListener(StateChangeListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void removeChangeListener(StateChangeListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
default void fireChangeEvent() {
|
||||
EventObject event = new EventObject(this);
|
||||
// Copy the list before iterating to prevent concurrent modification exceptions.
|
||||
EventListener[] list = listeners.toArray(new EventListener[0]);
|
||||
for (EventListener l : list) {
|
||||
if (l instanceof StateChangeListener) {
|
||||
((StateChangeListener) l).stateChanged(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
public enum WindModelType {
|
||||
PINK_NOISE,
|
||||
MULTI_LEVEL
|
||||
}
|
@ -19,12 +19,14 @@ import info.openrocket.core.file.wavefrontobj.export.OBJExportOptions;
|
||||
import info.openrocket.core.material.Material;
|
||||
import info.openrocket.core.models.atmosphere.AtmosphericModel;
|
||||
import info.openrocket.core.models.atmosphere.ExtendedISAModel;
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.preset.ComponentPreset;
|
||||
import info.openrocket.core.rocketcomponent.FlightConfiguration;
|
||||
import info.openrocket.core.rocketcomponent.MassObject;
|
||||
import info.openrocket.core.rocketcomponent.Rocket;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.simulation.RK4SimulationStepper;
|
||||
import info.openrocket.core.simulation.SimulationOptionsInterface;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.util.BugException;
|
||||
import info.openrocket.core.util.BuildProperties;
|
||||
@ -35,7 +37,7 @@ import info.openrocket.core.util.LineStyle;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
public abstract class ApplicationPreferences implements ChangeSource, ORPreferences {
|
||||
public abstract class ApplicationPreferences implements ChangeSource, ORPreferences, SimulationOptionsInterface, StateChangeListener {
|
||||
private static final String SPLIT_CHARACTER = "|";
|
||||
|
||||
/*
|
||||
@ -153,6 +155,9 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
|
||||
private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
|
||||
|
||||
private PinkNoiseWindModel pinkNoiseWindModel = null;
|
||||
|
||||
|
||||
/*
|
||||
* ******************************************************************************************
|
||||
*
|
||||
@ -346,19 +351,6 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public final double getWindTurbulenceIntensity() {
|
||||
return Application.getPreferences().getChoice(ApplicationPreferences.WIND_TURBULENCE, 0.9, 0.1);
|
||||
}
|
||||
|
||||
public final void setWindTurbulenceIntensity(double wti) {
|
||||
double oldWTI = Application.getPreferences().getChoice(ApplicationPreferences.WIND_TURBULENCE, 0.9, 0.3);
|
||||
|
||||
if (MathUtil.equals(oldWTI, wti))
|
||||
return;
|
||||
this.putDouble(ApplicationPreferences.WIND_TURBULENCE, wti);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getLaunchRodLength() {
|
||||
return this.getDouble(LAUNCH_ROD_LENGTH, 1);
|
||||
}
|
||||
@ -401,45 +393,30 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
|
||||
|
||||
|
||||
public double getWindSpeedAverage() {
|
||||
return this.getDouble(WIND_AVERAGE, 2);
|
||||
protected void loadWindModelState() {
|
||||
double average = getDouble(WIND_AVERAGE, 2.0);
|
||||
double turbulenceIntensity = getDouble(WIND_TURBULENCE, 0.1);
|
||||
double direction = getDouble(WIND_DIRECTION, Math.PI / 2);
|
||||
|
||||
getPinkNoiseWindModel().setAverage(average);
|
||||
getPinkNoiseWindModel().setTurbulenceIntensity(turbulenceIntensity);
|
||||
getPinkNoiseWindModel().setDirection(direction);
|
||||
}
|
||||
|
||||
public void setWindSpeedAverage(double windAverage) {
|
||||
if (MathUtil.equals(this.getDouble(WIND_AVERAGE, 2), windAverage))
|
||||
return;
|
||||
this.putDouble(WIND_AVERAGE, MathUtil.max(windAverage, 0));
|
||||
fireChangeEvent();
|
||||
protected void storeWindModelState() {
|
||||
putDouble(WIND_AVERAGE, getPinkNoiseWindModel().getAverage());
|
||||
putDouble(WIND_TURBULENCE, getPinkNoiseWindModel().getTurbulenceIntensity());
|
||||
putDouble(WIND_DIRECTION, getPinkNoiseWindModel().getDirection());
|
||||
}
|
||||
|
||||
|
||||
public double getWindSpeedDeviation() {
|
||||
return this.getDouble(WIND_AVERAGE, 2) * this.getDouble(WIND_TURBULENCE, 0.1);
|
||||
}
|
||||
|
||||
public void setWindSpeedDeviation(double windDeviation) {
|
||||
double windAverage = this.getDouble(WIND_DIRECTION, 2);
|
||||
if (windAverage < 0.1) {
|
||||
windAverage = 0.1;
|
||||
@Override
|
||||
public PinkNoiseWindModel getPinkNoiseWindModel() {
|
||||
if (pinkNoiseWindModel == null) {
|
||||
pinkNoiseWindModel = new PinkNoiseWindModel();
|
||||
pinkNoiseWindModel.addChangeListener(this);
|
||||
loadWindModelState();
|
||||
}
|
||||
setWindTurbulenceIntensity(windDeviation / windAverage);
|
||||
}
|
||||
|
||||
public void setWindDirection(double direction) {
|
||||
direction = MathUtil.reduce2Pi(direction);
|
||||
if (this.getBoolean(LAUNCH_INTO_WIND, true)) {
|
||||
this.setLaunchRodDirection(direction);
|
||||
}
|
||||
if (MathUtil.equals(this.getDouble(WIND_DIRECTION, Math.PI / 2), direction))
|
||||
return;
|
||||
this.putDouble(WIND_DIRECTION, direction);
|
||||
fireChangeEvent();
|
||||
|
||||
}
|
||||
|
||||
public double getWindDirection() {
|
||||
return this.getDouble(WIND_DIRECTION, Math.PI / 2);
|
||||
|
||||
return pinkNoiseWindModel;
|
||||
}
|
||||
|
||||
public double getLaunchAltitude() {
|
||||
@ -1291,4 +1268,11 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChanged(EventObject e) {
|
||||
if (e.getSource() == pinkNoiseWindModel) {
|
||||
storeWindModelState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,10 +35,9 @@ public class DefaultSimulationOptionFactory {
|
||||
SimulationOptions defaults = new SimulationOptions();
|
||||
if (prefs != null) {
|
||||
|
||||
defaults.setWindSpeedAverage(prefs.getDouble(SIMCONDITION_WIND_SPEED, defaults.getWindSpeedAverage()));
|
||||
defaults.setWindSpeedDeviation(prefs.getDouble(SIMCONDITION_WIND_STDDEV, defaults.getWindSpeedDeviation()));
|
||||
defaults.setWindTurbulenceIntensity(
|
||||
prefs.getDouble(SIMCONDITION_WIND_TURB, defaults.getWindTurbulenceIntensity()));
|
||||
defaults.getPinkNoiseWindModel().setAverage(prefs.getPinkNoiseWindModel().getAverage());
|
||||
defaults.getPinkNoiseWindModel().setStandardDeviation(prefs.getPinkNoiseWindModel().getStandardDeviation());
|
||||
defaults.getPinkNoiseWindModel().setTurbulenceIntensity(prefs.getPinkNoiseWindModel().getTurbulenceIntensity());
|
||||
|
||||
defaults.setLaunchLatitude(prefs.getDouble(SIMCONDITION_SITE_LAT, defaults.getLaunchLatitude()));
|
||||
defaults.setLaunchLongitude(prefs.getDouble(SIMCONDITION_SITE_LON, defaults.getLaunchLongitude()));
|
||||
@ -59,9 +58,9 @@ public class DefaultSimulationOptionFactory {
|
||||
|
||||
public void saveDefault(SimulationOptions newDefaults) {
|
||||
|
||||
prefs.putDouble(SIMCONDITION_WIND_SPEED, newDefaults.getWindSpeedAverage());
|
||||
prefs.putDouble(SIMCONDITION_WIND_STDDEV, newDefaults.getWindSpeedDeviation());
|
||||
prefs.putDouble(SIMCONDITION_WIND_TURB, newDefaults.getWindTurbulenceIntensity());
|
||||
prefs.putDouble(SIMCONDITION_WIND_SPEED, newDefaults.getPinkNoiseWindModel().getAverage());
|
||||
prefs.putDouble(SIMCONDITION_WIND_STDDEV, newDefaults.getPinkNoiseWindModel().getStandardDeviation());
|
||||
prefs.putDouble(SIMCONDITION_WIND_TURB, newDefaults.getPinkNoiseWindModel().getTurbulenceIntensity());
|
||||
|
||||
prefs.putDouble(SIMCONDITION_SITE_LAT, newDefaults.getLaunchLatitude());
|
||||
prefs.putDouble(SIMCONDITION_SITE_LON, newDefaults.getLaunchLongitude());
|
||||
|
@ -6,6 +6,9 @@ import java.util.EventObject;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import info.openrocket.core.models.wind.MultiLevelWindModel;
|
||||
import info.openrocket.core.models.wind.WindModel;
|
||||
import info.openrocket.core.models.wind.WindModelType;
|
||||
import info.openrocket.core.preferences.ApplicationPreferences;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -54,12 +57,8 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
private double launchRodLength = preferences.getDouble(ApplicationPreferences.LAUNCH_ROD_LENGTH, 1);
|
||||
private boolean launchIntoWind = preferences.getBoolean(ApplicationPreferences.LAUNCH_INTO_WIND, true);
|
||||
private double launchRodAngle = preferences.getDouble(ApplicationPreferences.LAUNCH_ROD_ANGLE, 0);
|
||||
private double windDirection = preferences.getDouble(ApplicationPreferences.WIND_DIRECTION, Math.PI / 2);
|
||||
private double launchRodDirection = preferences.getDouble(ApplicationPreferences.LAUNCH_ROD_DIRECTION, Math.PI / 2);
|
||||
|
||||
private double windAverage = preferences.getDouble(ApplicationPreferences.WIND_AVERAGE, 2.0);
|
||||
private double windTurbulence = preferences.getDouble(ApplicationPreferences.WIND_TURBULENCE, 0.1);
|
||||
|
||||
/*
|
||||
* SimulationOptions maintains the launch site parameters as separate double values,
|
||||
* and converts them into a WorldCoordinate when converting to SimulationConditions.
|
||||
@ -81,7 +80,13 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
|
||||
private List<EventListener> listeners = new ArrayList<>();
|
||||
|
||||
private WindModelType windModelType = WindModelType.PINK_NOISE;
|
||||
private final PinkNoiseWindModel pinkNoiseWindModel;
|
||||
private final MultiLevelWindModel multiLevelWindModel;
|
||||
|
||||
public SimulationOptions() {
|
||||
pinkNoiseWindModel = new PinkNoiseWindModel(randomSeed);
|
||||
multiLevelWindModel = new MultiLevelWindModel();
|
||||
}
|
||||
|
||||
public double getLaunchRodLength() {
|
||||
@ -120,6 +125,12 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
|
||||
public double getLaunchRodDirection() {
|
||||
if (launchIntoWind) {
|
||||
double windDirection;
|
||||
if (windModelType == WindModelType.PINK_NOISE) {
|
||||
windDirection = pinkNoiseWindModel.getDirection();
|
||||
} else {
|
||||
windDirection = multiLevelWindModel.getWindDirection(launchAltitude);
|
||||
}
|
||||
this.setLaunchRodDirection(windDirection);
|
||||
}
|
||||
return launchRodDirection;
|
||||
@ -133,57 +144,33 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getWindSpeedAverage() {
|
||||
return windAverage;
|
||||
public WindModelType getWindModelType() {
|
||||
return windModelType;
|
||||
}
|
||||
|
||||
public void setWindSpeedAverage(double windAverage) {
|
||||
if (MathUtil.equals(this.windAverage, windAverage))
|
||||
return;
|
||||
this.windAverage = MathUtil.max(windAverage, 0);
|
||||
if (MathUtil.equals(this.windAverage, 0)) {
|
||||
setWindTurbulenceIntensity(0);
|
||||
public void setWindModelType(WindModelType windModelType) {
|
||||
if (this.windModelType != windModelType) {
|
||||
this.windModelType = windModelType;
|
||||
fireChangeEvent();
|
||||
}
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getWindSpeedDeviation() {
|
||||
return windAverage * windTurbulence;
|
||||
}
|
||||
|
||||
public void setWindSpeedDeviation(double windDeviation) {
|
||||
if (windAverage < 0.1) {
|
||||
windAverage = 0.1;
|
||||
public WindModel getWindModel() {
|
||||
if (windModelType == WindModelType.PINK_NOISE) {
|
||||
return pinkNoiseWindModel;
|
||||
} else if (windModelType == WindModelType.MULTI_LEVEL) {
|
||||
return multiLevelWindModel;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown wind model type: " + windModelType);
|
||||
}
|
||||
setWindTurbulenceIntensity(windDeviation / windAverage);
|
||||
}
|
||||
|
||||
public double getWindTurbulenceIntensity() {
|
||||
return windTurbulence;
|
||||
public PinkNoiseWindModel getPinkNoiseWindModel() {
|
||||
return pinkNoiseWindModel;
|
||||
}
|
||||
|
||||
public void setWindTurbulenceIntensity(double intensity) {
|
||||
// Does not check equality so that setWindSpeedDeviation can be sure of event
|
||||
// firing
|
||||
this.windTurbulence = intensity;
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void setWindDirection(double direction) {
|
||||
direction = MathUtil.reduce2Pi(direction);
|
||||
if (launchIntoWind) {
|
||||
this.setLaunchRodDirection(direction);
|
||||
}
|
||||
if (MathUtil.equals(this.windDirection, direction))
|
||||
return;
|
||||
this.windDirection = direction;
|
||||
fireChangeEvent();
|
||||
|
||||
}
|
||||
|
||||
public double getWindDirection() {
|
||||
return this.windDirection;
|
||||
|
||||
public MultiLevelWindModel getMultiLevelWindModel() {
|
||||
return multiLevelWindModel;
|
||||
}
|
||||
|
||||
public double getLaunchAltitude() {
|
||||
@ -361,6 +348,16 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
// only do it if one of the "important" (user specified) parameters has really
|
||||
// changed.
|
||||
boolean isChanged = false;
|
||||
|
||||
if (!this.pinkNoiseWindModel.equals(src.pinkNoiseWindModel)) {
|
||||
isChanged = true;
|
||||
this.pinkNoiseWindModel.loadFrom(src.pinkNoiseWindModel);
|
||||
}
|
||||
if (!this.multiLevelWindModel.equals(src.multiLevelWindModel)) {
|
||||
isChanged = true;
|
||||
this.multiLevelWindModel.loadFrom(src.multiLevelWindModel);
|
||||
}
|
||||
|
||||
if (this.launchAltitude != src.launchAltitude) {
|
||||
isChanged = true;
|
||||
this.launchAltitude = src.launchAltitude;
|
||||
@ -405,18 +402,7 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
isChanged = true;
|
||||
this.maximumAngle = src.maximumAngle;
|
||||
}
|
||||
if (this.windAverage != src.windAverage) {
|
||||
isChanged = true;
|
||||
this.windAverage = src.windAverage;
|
||||
}
|
||||
if (this.windDirection != src.windDirection) {
|
||||
isChanged = true;
|
||||
this.windDirection = src.windDirection;
|
||||
}
|
||||
if (this.windTurbulence != src.windTurbulence) {
|
||||
isChanged = true;
|
||||
this.windTurbulence = src.windTurbulence;
|
||||
}
|
||||
|
||||
if (this.timeStep != src.timeStep) {
|
||||
isChanged = true;
|
||||
this.timeStep = src.timeStep;
|
||||
@ -453,10 +439,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
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) &&
|
||||
MathUtil.equals(this.windDirection, o.windDirection));
|
||||
MathUtil.equals(this.timeStep, o.timeStep)) &&
|
||||
this.windModelType == o.windModelType &&
|
||||
this.pinkNoiseWindModel.equals(o.pinkNoiseWindModel) &&
|
||||
this.multiLevelWindModel.equals(o.multiLevelWindModel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -505,17 +491,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
conditions.setGeodeticComputation(getGeodeticComputation());
|
||||
conditions.setRandomSeed(randomSeed);
|
||||
|
||||
PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed);
|
||||
windModel.setAverage(getWindSpeedAverage());
|
||||
windModel.setStandardDeviation(getWindSpeedDeviation());
|
||||
windModel.setDirection(windDirection);
|
||||
|
||||
WindModel windModel = getWindModel().clone();
|
||||
conditions.setWindModel(windModel);
|
||||
|
||||
conditions.setAtmosphericModel(getAtmosphericModel());
|
||||
|
||||
GravityModel gravityModel = new WGSGravityModel();
|
||||
|
||||
conditions.setGravityModel(gravityModel);
|
||||
|
||||
conditions.setAerodynamicCalculator(new BarrowmanCalculator());
|
||||
@ -533,10 +512,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
.concat(String.format(" launchRodLength: %f\n", launchRodLength))
|
||||
.concat(String.format(" launchIntoWind: %b\n", launchIntoWind))
|
||||
.concat(String.format(" launchRodAngle: %f\n", launchRodAngle))
|
||||
.concat(String.format(" windDirection: %f\n", windDirection))
|
||||
.concat(String.format(" launchRodDirection: %f\n", launchRodDirection))
|
||||
.concat(String.format(" windAverage: %f\n", windAverage))
|
||||
.concat(String.format(" windTurbulence: %f\n", windTurbulence))
|
||||
.concat(String.format(" windModelType: %s\n", windModelType))
|
||||
.concat(String.format(" pinkNoiseWindModel: %s\n", pinkNoiseWindModel))
|
||||
.concat(String.format(" multiLevelWindModel: %s\n", multiLevelWindModel))
|
||||
.concat(String.format(" launchAltitude: %f\n", launchAltitude))
|
||||
.concat(String.format(" launchLatitude: %f\n", launchLatitude))
|
||||
.concat(String.format(" launchLongitude: %f\n", launchLongitude))
|
||||
|
@ -1,5 +1,6 @@
|
||||
package info.openrocket.core.simulation;
|
||||
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.util.ChangeSource;
|
||||
import info.openrocket.core.util.GeodeticComputationStrategy;
|
||||
|
||||
@ -20,31 +21,7 @@ public interface SimulationOptionsInterface extends ChangeSource {
|
||||
|
||||
void setLaunchRodDirection(double launchRodDirection);
|
||||
|
||||
double getWindSpeedAverage();
|
||||
|
||||
void setWindSpeedAverage(double windAverage);
|
||||
|
||||
double getWindSpeedDeviation();
|
||||
|
||||
void setWindSpeedDeviation(double windDeviation);
|
||||
|
||||
/**
|
||||
* Return the wind turbulence intensity (standard deviation / average).
|
||||
*
|
||||
* @return the turbulence intensity
|
||||
*/
|
||||
double getWindTurbulenceIntensity();
|
||||
|
||||
/**
|
||||
* Set the wind standard deviation to match the given turbulence intensity.
|
||||
*
|
||||
* @param intensity the turbulence intensity
|
||||
*/
|
||||
void setWindTurbulenceIntensity(double intensity);
|
||||
|
||||
void setWindDirection(double direction);
|
||||
|
||||
double getWindDirection();
|
||||
PinkNoiseWindModel getPinkNoiseWindModel();
|
||||
|
||||
double getLaunchAltitude();
|
||||
|
||||
|
@ -42,7 +42,6 @@ public class AbstractChangeSource implements ChangeSource {
|
||||
((StateChangeListener) l).stateChanged(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -331,7 +331,6 @@ public class MathUtil {
|
||||
* or if t is outsize the domain.
|
||||
*/
|
||||
public static double interpolate(List<Double> domain, List<Double> range, double t) {
|
||||
|
||||
if (domain == null || range == null || domain.size() != range.size()) {
|
||||
return Double.NaN;
|
||||
}
|
||||
@ -368,7 +367,16 @@ public class MathUtil {
|
||||
}
|
||||
|
||||
return range.get(left) + (t - domain.get(left)) * deltay / deltax;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Use interpolation to determine the value of the function at point t.
|
||||
* @param a the lower bound
|
||||
* @param b the upper bound
|
||||
* @param fraction the fraction between a and b
|
||||
* @return the interpolated value
|
||||
*/
|
||||
public static double interpolate(double a, double b, double fraction) {
|
||||
return a + (b - a) * fraction;
|
||||
}
|
||||
}
|
||||
|
@ -503,6 +503,10 @@ simedtdlg.lbl.ttip.Pressure = The atmospheric pressure at the launch site.
|
||||
simedtdlg.lbl.Launchsite = Launch site
|
||||
simedtdlg.lbl.Latitude = Latitude:
|
||||
simedtdlg.lbl.ttip.Latitude = <html>The launch site latitude affects the gravitational pull of Earth.<br>Positive values are on the Northern hemisphere, negative values on the Southern hemisphere.
|
||||
simedtdlg.col.Altitude = Altitude
|
||||
simedtdlg.col.Speed = Speed
|
||||
simedtdlg.col.Direction = Direction
|
||||
simedtdlg.col.Unit = Unit
|
||||
|
||||
simedtdlg.lbl.Longitude = Longitude:
|
||||
simedtdlg.lbl.ttip.Longitude = <html>Required for weather prediction and elevation models.
|
||||
@ -552,6 +556,20 @@ simedtdlg.IntensityDesc.Medium = Medium
|
||||
simedtdlg.IntensityDesc.High = High
|
||||
simedtdlg.IntensityDesc.Veryhigh = Very high
|
||||
simedtdlg.IntensityDesc.Extreme = Extreme
|
||||
simedtdlg.lbl.WindModelSelection = Wind model to use:
|
||||
simedtdlg.radio.PinkNoiseWind = Pink noise
|
||||
simedtdlg.radio.PinkNoiseWind.ttip = Model the wind as pink noise from the average wind speed and standard deviation.
|
||||
simedtdlg.radio.MultiLevelWind = Multi-level
|
||||
simedtdlg.radio.MultiLevelWind.ttip = Model the wind using speed and direction entries at various altitude levels.
|
||||
simedtdlg.but.addWindLevel = Add level
|
||||
simedtdlg.but.removeWindLevel = Remove level
|
||||
simedtdlg.but.visualizeWindLevels = Visualize levels
|
||||
|
||||
! WindLevelVisualizationDialog
|
||||
WindLevelVisualizationDialog.title.WindLevelVisualization = Wind Level Visualization
|
||||
WindLevelVisualizationDialog.lbl.WindSpeed = Wind speed
|
||||
WindLevelVisualizationDialog.lbl.Altitude = Altitude
|
||||
WindLevelVisualizationDialog.checkbox.ShowDirections = Show wind direction vectors
|
||||
|
||||
! SimulationConfigDialog
|
||||
SimulationConfigDialog.tab.Settings = Settings
|
||||
|
@ -0,0 +1,155 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.ModID;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class MultiLevelWindModelTest {
|
||||
private final static double EPSILON = MathUtil.EPSILON;
|
||||
|
||||
private MultiLevelWindModel model;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
model = new MultiLevelWindModel();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Add and remove wind levels")
|
||||
void testAddAndRemoveWindLevels() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4);
|
||||
model.addWindLevel(200, 10, Math.PI / 2);
|
||||
assertEquals(2, model.getLevels().size());
|
||||
|
||||
model.removeWindLevel(100);
|
||||
assertEquals(1, model.getLevels().size());
|
||||
assertEquals(200, model.getLevels().get(0).altitude, EPSILON);
|
||||
|
||||
model.removeWindLevel(0);
|
||||
assertEquals(1, model.getLevels().size());
|
||||
assertEquals(200, model.getLevels().get(0).altitude, EPSILON);
|
||||
|
||||
model.removeWindLevel(200);
|
||||
assertTrue(model.getLevels().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Adding duplicate altitude throws IllegalArgumentException")
|
||||
void testAddDuplicateAltitude() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4);
|
||||
assertThrows(IllegalArgumentException.class, () -> model.addWindLevel(100, 10, Math.PI / 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get wind velocity")
|
||||
void testGetWindVelocity() {
|
||||
model.addWindLevel(0, 5, 0);
|
||||
model.addWindLevel(1000, 10, Math.PI / 2);
|
||||
|
||||
verifyWind(500, 7.5, Math.PI / 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Interpolation between levels")
|
||||
void testInterpolationBetweenLevels() {
|
||||
model.addWindLevel(0, 5, 0);
|
||||
model.addWindLevel(1000, 10, Math.PI);
|
||||
|
||||
verifyWind(200, 6, Math.PI / 5);
|
||||
verifyWind(500, 7.5, Math.PI / 2);
|
||||
verifyWind(900, 9.5, 9 * Math.PI / 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Extrapolation outside levels")
|
||||
void testExtrapolationOutsideLevels() {
|
||||
model.addWindLevel(100, 5, 0);
|
||||
model.addWindLevel(200, 10, Math.PI / 2);
|
||||
|
||||
verifyWind(0, 5, 0);
|
||||
verifyWind(300, 10, Math.PI / 2);
|
||||
verifyWind(1000, 10, Math.PI / 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Resort levels")
|
||||
void testResortLevels() {
|
||||
model.addWindLevel(200, 10, Math.PI / 2);
|
||||
model.addWindLevel(100, 5, Math.PI / 4);
|
||||
model.addWindLevel(300, 15, 3 * Math.PI / 4);
|
||||
|
||||
model.resortLevels();
|
||||
|
||||
List<MultiLevelWindModel.WindLevel> levels = model.getLevels();
|
||||
assertEquals(3, levels.size());
|
||||
assertEquals(100, levels.get(0).altitude, EPSILON);
|
||||
assertEquals(200, levels.get(1).altitude, EPSILON);
|
||||
assertEquals(300, levels.get(2).altitude, EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clone model")
|
||||
void testClone() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4);
|
||||
model.addWindLevel(200, 10, Math.PI / 2);
|
||||
|
||||
MultiLevelWindModel clonedModel = model.clone();
|
||||
assertNotSame(model, clonedModel);
|
||||
assertEquals(model, clonedModel);
|
||||
|
||||
clonedModel.addWindLevel(300, 15, 3 * Math.PI / 4);
|
||||
assertNotEquals(model, clonedModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Load from another model")
|
||||
void testLoadFrom() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4);
|
||||
model.addWindLevel(200, 10, Math.PI / 2);
|
||||
|
||||
MultiLevelWindModel newModel = new MultiLevelWindModel();
|
||||
newModel.loadFrom(model);
|
||||
|
||||
assertEquals(model, newModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get ModID")
|
||||
void testGetModID() {
|
||||
assertEquals(ModID.ZERO, model.getModID());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Change listeners")
|
||||
void testChangeListeners() {
|
||||
final boolean[] listenerCalled = {false};
|
||||
StateChangeListener listener = event -> listenerCalled[0] = true;
|
||||
|
||||
model.addChangeListener(listener);
|
||||
model.fireChangeEvent();
|
||||
assertTrue(listenerCalled[0]);
|
||||
|
||||
listenerCalled[0] = false;
|
||||
model.removeChangeListener(listener);
|
||||
model.fireChangeEvent();
|
||||
assertFalse(listenerCalled[0]);
|
||||
}
|
||||
|
||||
private void verifyWind(double altitude, double expectedSpeed, double expectedDirection) {
|
||||
Coordinate velocity = model.getWindVelocity(0, altitude);
|
||||
assertEquals(expectedSpeed, velocity.length(), EPSILON, "Wind speed at altitude " + altitude);
|
||||
assertEquals(expectedSpeed * Math.sin(expectedDirection), velocity.x, EPSILON, "Wind velocity X component at altitude " + altitude);
|
||||
assertEquals(expectedSpeed * Math.cos(expectedDirection), velocity.y, EPSILON, "Wind velocity Y component at altitude " + altitude);
|
||||
assertEquals(0, velocity.z, EPSILON, "Wind velocity Z component at altitude " + altitude);
|
||||
assertEquals(expectedDirection, Math.atan2(velocity.x, velocity.y), EPSILON, "Wind direction at altitude " + altitude);
|
||||
}
|
||||
}
|
@ -10,15 +10,20 @@ import info.openrocket.core.formatting.RocketDescriptor;
|
||||
import info.openrocket.core.formatting.RocketDescriptorImpl;
|
||||
import info.openrocket.core.l10n.DebugTranslator;
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.models.wind.MultiLevelWindModel;
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.plugin.PluginModule;
|
||||
import info.openrocket.core.preferences.ApplicationPreferences;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.startup.MockPreferences;
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ -60,16 +65,99 @@ public class SimulationConditionsTest {
|
||||
assertEquals(Math.PI / 2, options.getLaunchRodDirection(), EPSILON);
|
||||
assertEquals(0.0, options.getLaunchRodAngle(), EPSILON);
|
||||
assertTrue(options.getLaunchIntoWind());
|
||||
assertEquals(Math.PI / 2, options.getWindDirection(), EPSILON);
|
||||
assertEquals(0.1, options.getWindTurbulenceIntensity(), EPSILON);
|
||||
assertEquals(2.0, options.getWindSpeedAverage(), EPSILON);
|
||||
assertEquals(0.2, options.getWindSpeedDeviation(), EPSILON);
|
||||
assertEquals(Math.PI / 2, options.getPinkNoiseWindModel().getDirection(), EPSILON);
|
||||
assertEquals(0.1, options.getPinkNoiseWindModel().getTurbulenceIntensity(), EPSILON);
|
||||
assertEquals(2.0, options.getPinkNoiseWindModel().getAverage(), EPSILON);
|
||||
assertEquals(0.2, options.getPinkNoiseWindModel().getStandardDeviation(), EPSILON);
|
||||
|
||||
assertEquals(0.05, options.getTimeStep(), EPSILON);
|
||||
assertEquals(3 * Math.PI / 180, options.getMaximumStepAngle(), EPSILON);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Compare PinkNoiseWindModel and MultiLevelWindModel in SimulationConditions")
|
||||
public void testWindModelComparison() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
|
||||
// Test PinkNoiseWindModel
|
||||
PinkNoiseWindModel pinkNoiseModel = new PinkNoiseWindModel();
|
||||
pinkNoiseModel.setAverage(5.0);
|
||||
pinkNoiseModel.setStandardDeviation(1.0);
|
||||
pinkNoiseModel.setDirection(Math.PI / 4); // 45 degrees
|
||||
|
||||
conditions.setWindModel(pinkNoiseModel);
|
||||
|
||||
Coordinate pinkNoiseVelocity = conditions.getWindModel().getWindVelocity(0, 100);
|
||||
assertNotNull(pinkNoiseVelocity);
|
||||
assertTrue(pinkNoiseVelocity.length() > 0);
|
||||
|
||||
// Test MultiLevelWindModel
|
||||
MultiLevelWindModel multiLevelModel = new MultiLevelWindModel();
|
||||
multiLevelModel.addWindLevel(0, 5.0, Math.PI / 4);
|
||||
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2);
|
||||
|
||||
conditions.setWindModel(multiLevelModel);
|
||||
|
||||
Coordinate multiLevelVelocity = conditions.getWindModel().getWindVelocity(0, 100);
|
||||
assertNotNull(multiLevelVelocity);
|
||||
assertTrue(multiLevelVelocity.length() > 0);
|
||||
|
||||
// Compare behaviors
|
||||
assertNotEquals(pinkNoiseVelocity, multiLevelVelocity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test wind velocity consistency for MultiLevelWindModel")
|
||||
public void testMultiLevelWindModelConsistency() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
MultiLevelWindModel multiLevelModel = new MultiLevelWindModel();
|
||||
multiLevelModel.addWindLevel(0, 5.0, Math.PI / 4);
|
||||
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2);
|
||||
|
||||
conditions.setWindModel(multiLevelModel);
|
||||
|
||||
Coordinate velocity1 = conditions.getWindModel().getWindVelocity(0, 500);
|
||||
Coordinate velocity2 = conditions.getWindModel().getWindVelocity(0, 500);
|
||||
|
||||
assertEquals(velocity1, velocity2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test wind velocity variation for PinkNoiseWindModel")
|
||||
public void testPinkNoiseWindModelVariation() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
PinkNoiseWindModel pinkNoiseModel = new PinkNoiseWindModel();
|
||||
pinkNoiseModel.setAverage(5.0);
|
||||
pinkNoiseModel.setStandardDeviation(1.0);
|
||||
pinkNoiseModel.setDirection(Math.PI / 4);
|
||||
|
||||
conditions.setWindModel(pinkNoiseModel);
|
||||
|
||||
Coordinate velocity1 = conditions.getWindModel().getWindVelocity(0, 100);
|
||||
Coordinate velocity2 = conditions.getWindModel().getWindVelocity(1, 100);
|
||||
|
||||
assertNotEquals(velocity1, velocity2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test altitude dependence of MultiLevelWindModel")
|
||||
public void testMultiLevelWindModelAltitudeDependence() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
MultiLevelWindModel multiLevelModel = new MultiLevelWindModel();
|
||||
multiLevelModel.addWindLevel(0, 5.0, 0);
|
||||
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2);
|
||||
|
||||
conditions.setWindModel(multiLevelModel);
|
||||
|
||||
Coordinate velocityLow = conditions.getWindModel().getWindVelocity(0, 0);
|
||||
Coordinate velocityHigh = conditions.getWindModel().getWindVelocity(0, 1000);
|
||||
Coordinate velocityMid = conditions.getWindModel().getWindVelocity(0, 500);
|
||||
|
||||
assertNotEquals(velocityLow, velocityHigh);
|
||||
assertTrue(velocityMid.length() > velocityLow.length() && velocityMid.length() < velocityHigh.length());
|
||||
}
|
||||
|
||||
|
||||
private static class PreferencesModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
|
@ -34,8 +34,7 @@ public class LaunchPreferencesPanel extends PreferencesPanel {
|
||||
add(warning, "spanx, growx 0, gapbottom para, wrap");
|
||||
|
||||
// Simulation conditions
|
||||
SimulationConditionsPanel.addSimulationConditionsPanel(this, preferences);
|
||||
|
||||
SimulationConditionsPanel.addSimulationConditionsPanel(this, preferences, false);
|
||||
}
|
||||
|
||||
private static void initColors() {
|
||||
|
@ -1,30 +1,58 @@
|
||||
package info.openrocket.swing.gui.simulation;
|
||||
|
||||
import java.awt.CardLayout;
|
||||
import java.awt.Component;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.awt.Dialog;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.util.Comparator;
|
||||
import java.util.EventObject;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.ButtonGroup;
|
||||
import javax.swing.DefaultCellEditor;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JComboBox;
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.JOptionPane;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.JRadioButton;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JSeparator;
|
||||
import javax.swing.JSpinner;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.JTextField;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.RowSorter;
|
||||
import javax.swing.SortOrder;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
import javax.swing.table.TableColumn;
|
||||
import javax.swing.table.TableColumnModel;
|
||||
import javax.swing.table.TableRowSorter;
|
||||
|
||||
import info.openrocket.core.document.Simulation;
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.models.atmosphere.ExtendedISAModel;
|
||||
import info.openrocket.core.models.wind.MultiLevelWindModel;
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.models.wind.WindModelType;
|
||||
import info.openrocket.core.simulation.DefaultSimulationOptionFactory;
|
||||
import info.openrocket.core.simulation.SimulationOptions;
|
||||
import info.openrocket.core.simulation.SimulationOptionsInterface;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
import info.openrocket.core.unit.UnitGroup;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
@ -38,54 +66,28 @@ import info.openrocket.swing.gui.components.UnitSelector;
|
||||
public class SimulationConditionsPanel extends JPanel {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private WindLevelVisualizationDialog visualizationDialog;
|
||||
|
||||
SimulationConditionsPanel(final Simulation simulation) {
|
||||
super(new MigLayout("fill"));
|
||||
|
||||
final SimulationOptions conditions = simulation.getOptions();
|
||||
SimulationOptions simulationOptions = simulation.getOptions();
|
||||
|
||||
// Simulation conditions settings
|
||||
addSimulationConditionsPanel(this, conditions);
|
||||
|
||||
|
||||
JButton restoreDefaults = new JButton(trans.get("simedtdlg.but.resettodefault"));
|
||||
restoreDefaults.addActionListener(new ActionListener() {
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
|
||||
DefaultSimulationOptionFactory f = Application.getInjector().getInstance(DefaultSimulationOptionFactory.class);
|
||||
SimulationOptions defaults = f.getDefault();
|
||||
conditions.copyConditionsFrom(defaults);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
this.add(restoreDefaults, "span, split 3, skip, gapbottom para, gapright para, right");
|
||||
|
||||
JButton saveDefaults = new JButton(trans.get("simedtdlg.but.savedefault"));
|
||||
saveDefaults.addActionListener(new ActionListener() {
|
||||
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
|
||||
DefaultSimulationOptionFactory f = Application.getInjector().getInstance(DefaultSimulationOptionFactory.class);
|
||||
f.saveDefault(conditions);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this.add(saveDefaults, "gapbottom para, gapright para, right");
|
||||
addSimulationConditionsPanel(this, simulationOptions);
|
||||
|
||||
// Add buttons for restoring and saving defaults
|
||||
addDefaultButtons(simulationOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the simulation conditions panel to the parent panel.
|
||||
* @param parent The parent panel.
|
||||
* @param target The object containing the simulation conditions setters/getters.
|
||||
* @param addAllWindModels if false, only the pink noise wind model will be added.
|
||||
*/
|
||||
public static void addSimulationConditionsPanel(JPanel parent, SimulationOptionsInterface target) {
|
||||
public static void addSimulationConditionsPanel(JPanel parent, SimulationOptionsInterface target,
|
||||
boolean addAllWindModels) {
|
||||
JPanel sub;
|
||||
DoubleModel pressureModel;
|
||||
DoubleModel m;
|
||||
@ -96,135 +98,17 @@ public class SimulationConditionsPanel extends JPanel {
|
||||
UnitSelector unit;
|
||||
|
||||
//// Wind settings: Average wind speed, turbulence intensity, std. deviation, and direction
|
||||
sub = new JPanel(new MigLayout("fill, gap rel unrel",
|
||||
"[grow][75lp!][30lp!][75lp!]", ""));
|
||||
sub = new JPanel(new MigLayout("fill, gap rel unrel", "[grow]", ""));
|
||||
//// Wind
|
||||
sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.lbl.Wind")));
|
||||
parent.add(sub, "growx, split 2, aligny 0, flowy, gapright para");
|
||||
|
||||
|
||||
// Wind average
|
||||
//// Average windspeed:
|
||||
JLabel label = new JLabel(trans.get("simedtdlg.lbl.Averwindspeed"));
|
||||
//// The average windspeed relative to the ground.
|
||||
tip = trans.get("simedtdlg.lbl.ttip.Averwindspeed");
|
||||
label.setToolTipText(tip);
|
||||
sub.add(label);
|
||||
|
||||
DoubleModel windSpeedAverage = new DoubleModel(target, "WindSpeedAverage", UnitGroup.UNITS_WINDSPEED, 0);
|
||||
|
||||
spin = new JSpinner(windSpeedAverage.getSpinnerModel());
|
||||
spin.setEditor(new SpinnerEditor(spin));
|
||||
spin.setToolTipText(tip);
|
||||
sub.add(spin, "growx");
|
||||
|
||||
unit = new UnitSelector(windSpeedAverage);
|
||||
unit.setToolTipText(tip);
|
||||
sub.add(unit, "growx");
|
||||
slider = new BasicSlider(windSpeedAverage.getSliderModel(0, 10.0));
|
||||
slider.setToolTipText(tip);
|
||||
sub.add(slider, "w 75lp, wrap");
|
||||
|
||||
|
||||
// Wind std. deviation
|
||||
//// Standard deviation:
|
||||
label = new JLabel(trans.get("simedtdlg.lbl.Stddeviation"));
|
||||
//// <html>The standard deviation of the windspeed.<br>
|
||||
//// The windspeed is within twice the standard deviation from the average for 95% of the time.
|
||||
tip = trans.get("simedtdlg.lbl.ttip.Stddeviation");
|
||||
label.setToolTipText(tip);
|
||||
sub.add(label);
|
||||
|
||||
DoubleModel windSpeedDeviation = new DoubleModel(target, "WindSpeedDeviation", UnitGroup.UNITS_WINDSPEED, 0);
|
||||
DoubleModel m2 = new DoubleModel(target, "WindSpeedAverage", 0.25, UnitGroup.UNITS_COEFFICIENT, 0);
|
||||
|
||||
spin = new JSpinner(windSpeedDeviation.getSpinnerModel());
|
||||
spin.setEditor(new SpinnerEditor(spin));
|
||||
spin.setToolTipText(tip);
|
||||
addEasterEgg(spin, parent);
|
||||
sub.add(spin, "growx");
|
||||
|
||||
unit = new UnitSelector(windSpeedDeviation);
|
||||
unit.setToolTipText(tip);
|
||||
sub.add(unit, "growx");
|
||||
slider = new BasicSlider(windSpeedDeviation.getSliderModel(new DoubleModel(0), m2));
|
||||
slider.setToolTipText(tip);
|
||||
sub.add(slider, "w 75lp, wrap");
|
||||
|
||||
windSpeedAverage.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
windSpeedDeviation.stateChanged(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Wind turbulence intensity
|
||||
//// Turbulence intensity:
|
||||
label = new JLabel(trans.get("simedtdlg.lbl.Turbulenceintensity"));
|
||||
//// <html>The turbulence intensity is the standard deviation divided by the average windspeed.<br>
|
||||
//// Typical values range from
|
||||
//// to
|
||||
tip = trans.get("simedtdlg.lbl.ttip.Turbulenceintensity1") +
|
||||
trans.get("simedtdlg.lbl.ttip.Turbulenceintensity2") + " " +
|
||||
UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.05) +
|
||||
" " + trans.get("simedtdlg.lbl.ttip.Turbulenceintensity3") + " " +
|
||||
UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.20) + ".";
|
||||
label.setToolTipText(tip);
|
||||
sub.add(label);
|
||||
|
||||
DoubleModel windTurbulenceIntensity = new DoubleModel(target, "WindTurbulenceIntensity", UnitGroup.UNITS_RELATIVE, 0);
|
||||
|
||||
spin = new JSpinner(windTurbulenceIntensity.getSpinnerModel());
|
||||
spin.setEditor(new SpinnerEditor(spin));
|
||||
spin.setToolTipText(tip);
|
||||
sub.add(spin, "growx");
|
||||
|
||||
unit = new UnitSelector(windTurbulenceIntensity);
|
||||
unit.setToolTipText(tip);
|
||||
sub.add(unit, "growx");
|
||||
|
||||
final JLabel intensityLabel = new JLabel(
|
||||
getIntensityDescription(target.getWindTurbulenceIntensity()));
|
||||
intensityLabel.setToolTipText(tip);
|
||||
sub.add(intensityLabel, "w 75lp, wrap");
|
||||
windTurbulenceIntensity.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
intensityLabel.setText(
|
||||
getIntensityDescription(target.getWindTurbulenceIntensity()));
|
||||
windSpeedDeviation.stateChanged(e);
|
||||
}
|
||||
});
|
||||
windSpeedDeviation.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
windTurbulenceIntensity.stateChanged(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Wind Direction:
|
||||
label = new JLabel(trans.get("simedtdlg.lbl.Winddirection"));
|
||||
//// Direction of the wind. 0 is north
|
||||
tip = trans.get("simedtdlg.lbl.ttip.Winddirection");
|
||||
label.setToolTipText(tip);
|
||||
sub.add(label);
|
||||
|
||||
m = new DoubleModel(target, "WindDirection", 1.0, UnitGroup.UNITS_ANGLE,
|
||||
0, 2*Math.PI);
|
||||
|
||||
spin = new JSpinner(m.getSpinnerModel());
|
||||
spin.setEditor(new SpinnerEditor(spin));
|
||||
spin.setToolTipText(tip);
|
||||
sub.add(spin, "growx");
|
||||
|
||||
unit = new UnitSelector(m);
|
||||
unit.setToolTipText(tip);
|
||||
sub.add(unit, "growx");
|
||||
slider = new BasicSlider(m.getSliderModel(0, 2*Math.PI));
|
||||
slider.setToolTipText(tip);
|
||||
sub.add(slider, "w 75lp, wrap");
|
||||
|
||||
// Add wind model selection and configuration panel
|
||||
if (addAllWindModels) {
|
||||
addWindModelPanel(sub, target);
|
||||
} else {
|
||||
addPinkNoiseSettings(sub, target);
|
||||
}
|
||||
|
||||
//// Temperature and pressure
|
||||
sub = new JPanel(new MigLayout("fill, gap rel unrel",
|
||||
@ -250,7 +134,7 @@ public class SimulationConditionsPanel extends JPanel {
|
||||
sub.add(check, "spanx, wrap unrel");
|
||||
|
||||
// Temperature:
|
||||
label = new JLabel(trans.get("simedtdlg.lbl.Temperature"));
|
||||
JLabel label = new JLabel(trans.get("simedtdlg.lbl.Temperature"));
|
||||
//// The temperature at the launch site.
|
||||
tip = trans.get("simedtdlg.lbl.ttip.Temperature");
|
||||
label.setToolTipText(tip);
|
||||
@ -483,6 +367,302 @@ public class SimulationConditionsPanel extends JPanel {
|
||||
intoWind.addEnableComponent(directionSlider, false);
|
||||
}
|
||||
|
||||
public static void addSimulationConditionsPanel(JPanel parent, SimulationOptionsInterface target) {
|
||||
addSimulationConditionsPanel(parent, target, true);
|
||||
}
|
||||
|
||||
private static void addWindModelPanel(JPanel panel, SimulationOptionsInterface target) {
|
||||
ButtonGroup windModelGroup = new ButtonGroup();
|
||||
|
||||
panel.add(new JLabel(trans.get("simedtdlg.lbl.WindModelSelection")), "spanx, split 3, gapright para");
|
||||
|
||||
JRadioButton pinkNoiseButton = new JRadioButton(trans.get("simedtdlg.radio.PinkNoiseWind"));
|
||||
pinkNoiseButton.setToolTipText(trans.get("simedtdlg.radio.PinkNoiseWind.ttip"));
|
||||
JRadioButton multiLevelButton = new JRadioButton(trans.get("simedtdlg.radio.MultiLevelWind"));
|
||||
multiLevelButton.setToolTipText(trans.get("simedtdlg.radio.MultiLevelWind.ttip"));
|
||||
|
||||
windModelGroup.add(pinkNoiseButton);
|
||||
windModelGroup.add(multiLevelButton);
|
||||
|
||||
panel.add(pinkNoiseButton);
|
||||
panel.add(multiLevelButton, "wrap");
|
||||
|
||||
panel.add(new JSeparator(JSeparator.HORIZONTAL), "spanx, growx, wrap");
|
||||
|
||||
JPanel windSettingsPanel = new JPanel(new CardLayout());
|
||||
|
||||
JPanel pinkNoisePanel = new JPanel(new MigLayout("fill, ins 0, gap rel unrel", "[grow][75lp!][30lp!][75lp!]", ""));
|
||||
JPanel multiLevelPanel = new JPanel(new MigLayout("fill, ins 0, gap rel unrel", "[grow]", ""));
|
||||
|
||||
addPinkNoiseSettings(pinkNoisePanel, target);
|
||||
addMultiLevelSettings(multiLevelPanel, target);
|
||||
|
||||
windSettingsPanel.add(pinkNoisePanel, "PinkNoise");
|
||||
windSettingsPanel.add(multiLevelPanel, "MultiLevel");
|
||||
|
||||
panel.add(windSettingsPanel, "grow, wrap");
|
||||
|
||||
pinkNoiseButton.addActionListener(e -> {
|
||||
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "PinkNoise");
|
||||
if (target instanceof SimulationOptions) {
|
||||
((SimulationOptions) target).setWindModelType(WindModelType.PINK_NOISE);
|
||||
}
|
||||
});
|
||||
|
||||
multiLevelButton.addActionListener(e -> {
|
||||
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "MultiLevel");
|
||||
if (target instanceof SimulationOptions) {
|
||||
((SimulationOptions) target).setWindModelType(WindModelType.MULTI_LEVEL);
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial selection based on current wind model
|
||||
if (target instanceof SimulationOptions) {
|
||||
SimulationOptions options = (SimulationOptions) target;
|
||||
if (options.getWindModelType() == WindModelType.PINK_NOISE) {
|
||||
pinkNoiseButton.setSelected(true);
|
||||
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "PinkNoise");
|
||||
} else {
|
||||
multiLevelButton.setSelected(true);
|
||||
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "MultiLevel");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPinkNoiseSettings(JPanel panel, SimulationOptionsInterface target) {
|
||||
PinkNoiseWindModel model = target.getPinkNoiseWindModel();
|
||||
|
||||
// Wind average
|
||||
final DoubleModel windSpeedAverage = addDoubleModel(panel, "Averwindspeed", trans.get("simedtdlg.lbl.ttip.Averwindspeed"), model, "Average",
|
||||
UnitGroup.UNITS_WINDSPEED, 0, 10.0);
|
||||
|
||||
// Wind standard deviation
|
||||
final DoubleModel windSpeedDeviation = addDoubleModel(panel, "Stddeviation", trans.get("simedtdlg.lbl.ttip.Stddeviation"),
|
||||
model, "StandardDeviation", UnitGroup.UNITS_WINDSPEED, 0,
|
||||
new DoubleModel(model, "Average", 0.25, UnitGroup.UNITS_COEFFICIENT, 0));
|
||||
|
||||
windSpeedAverage.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
windSpeedDeviation.stateChanged(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Turbulence intensity
|
||||
String tip = trans.get("simedtdlg.lbl.ttip.Turbulenceintensity1") +
|
||||
trans.get("simedtdlg.lbl.ttip.Turbulenceintensity2") + " " +
|
||||
UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.05) +
|
||||
" " + trans.get("simedtdlg.lbl.ttip.Turbulenceintensity3") + " " +
|
||||
UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.20) + ".";
|
||||
final DoubleModel windTurbulenceIntensity = addDoubleModel(panel, "Turbulenceintensity", tip, model,
|
||||
"TurbulenceIntensity", UnitGroup.UNITS_RELATIVE, 0, 1.0, true);
|
||||
|
||||
final JLabel intensityLabel = new JLabel(
|
||||
getIntensityDescription(target.getPinkNoiseWindModel().getTurbulenceIntensity()));
|
||||
intensityLabel.setToolTipText(tip);
|
||||
panel.add(intensityLabel, "w 75lp, skip 1, wrap");
|
||||
windTurbulenceIntensity.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
intensityLabel.setText(
|
||||
getIntensityDescription(target.getPinkNoiseWindModel().getTurbulenceIntensity()));
|
||||
windSpeedDeviation.stateChanged(e);
|
||||
}
|
||||
});
|
||||
windSpeedDeviation.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
windTurbulenceIntensity.stateChanged(e);
|
||||
}
|
||||
});
|
||||
|
||||
// Wind direction
|
||||
addDoubleModel(panel, "Winddirection", trans.get("simedtdlg.lbl.ttip.Winddirection"), model, "Direction",
|
||||
UnitGroup.UNITS_ANGLE, 0, 2 * Math.PI);
|
||||
}
|
||||
|
||||
private static void addMultiLevelSettings(JPanel panel, SimulationOptionsInterface target) {
|
||||
if (!(target instanceof SimulationOptions options)) {
|
||||
return;
|
||||
}
|
||||
MultiLevelWindModel model = options.getMultiLevelWindModel();
|
||||
|
||||
// Create the levels table
|
||||
WindLevelTableModel tableModel = new WindLevelTableModel(model);
|
||||
JTable windLevelTable = new JTable(tableModel);
|
||||
windLevelTable.setRowSelectionAllowed(false);
|
||||
windLevelTable.setColumnSelectionAllowed(false);
|
||||
windLevelTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
|
||||
// Set up value columns
|
||||
SelectAllCellEditor selectAllEditor = new SelectAllCellEditor();
|
||||
ValueCellRenderer valueCellRenderer = new ValueCellRenderer();
|
||||
for (int i = 0; i < windLevelTable.getColumnCount(); i += 2) {
|
||||
windLevelTable.getColumnModel().getColumn(i).setCellRenderer(valueCellRenderer);
|
||||
windLevelTable.getColumnModel().getColumn(i).setCellEditor(selectAllEditor);
|
||||
}
|
||||
|
||||
// Set up unit selector columns
|
||||
for (int i = 1; i < windLevelTable.getColumnCount(); i += 2) {
|
||||
windLevelTable.getColumnModel().getColumn(i).setCellRenderer(new UnitSelectorRenderer());
|
||||
windLevelTable.getColumnModel().getColumn(i).setCellEditor(new UnitSelectorEditor());
|
||||
}
|
||||
|
||||
// Adjust column widths
|
||||
adjustColumnWidths(windLevelTable);
|
||||
|
||||
windLevelTable.setRowHeight(windLevelTable.getRowHeight() + 10);
|
||||
|
||||
// Set up sorting
|
||||
TableRowSorter<WindLevelTableModel> sorter = new TableRowSorter<>(tableModel);
|
||||
windLevelTable.setRowSorter(sorter);
|
||||
sorter.setSortable(0, true);
|
||||
sorter.setSortable(1, false);
|
||||
sorter.setSortable(2, false);
|
||||
sorter.setComparator(0, Comparator.comparingDouble(a -> (Double) a));
|
||||
sorter.setSortKeys(List.of(new RowSorter.SortKey(0, SortOrder.ASCENDING)));
|
||||
|
||||
JScrollPane scrollPane = new JScrollPane(windLevelTable);
|
||||
scrollPane.setPreferredSize(new Dimension(350, 150));
|
||||
panel.add(scrollPane, "grow");
|
||||
|
||||
//// Buttons
|
||||
JPanel buttonPanel = new JPanel(new MigLayout("fill, ins 0, gap rel unrel", "[grow]", ""));
|
||||
|
||||
// Add wind level
|
||||
JButton addButton = new JButton(trans.get("simedtdlg.but.addWindLevel"));
|
||||
addButton.addActionListener(e -> {
|
||||
tableModel.addWindLevel();
|
||||
sorter.sort();
|
||||
});
|
||||
buttonPanel.add(addButton, "growx, wrap");
|
||||
|
||||
// Remove wind level
|
||||
JButton removeButton = new JButton(trans.get("simedtdlg.but.removeWindLevel"));
|
||||
removeButton.addActionListener(e -> {
|
||||
int selectedRow = windLevelTable.getSelectedRow();
|
||||
tableModel.removeWindLevel(selectedRow);
|
||||
sorter.sort();
|
||||
});
|
||||
buttonPanel.add(removeButton, "growx, wrap");
|
||||
|
||||
// Visualization button
|
||||
JButton visualizeButton = new JButton(trans.get("simedtdlg.but.visualizeWindLevels"));
|
||||
visualizeButton.addActionListener(e -> {
|
||||
Window owner = SwingUtilities.getWindowAncestor(panel);
|
||||
if (owner instanceof Dialog) {
|
||||
WindLevelVisualizationDialog visualizationDialog = new WindLevelVisualizationDialog(
|
||||
(Dialog) owner,
|
||||
model,
|
||||
tableModel.getCurrentUnits()[0],
|
||||
tableModel.getCurrentUnits()[1]
|
||||
);
|
||||
tableModel.setVisualizationDialog(visualizationDialog);
|
||||
visualizationDialog.setVisible(true);
|
||||
}
|
||||
});
|
||||
buttonPanel.add(visualizeButton, "growx, wrap");
|
||||
|
||||
panel.add(buttonPanel, "grow, wrap");
|
||||
|
||||
// Add listener to update visualization when table data changes
|
||||
tableModel.addTableModelListener(e -> {
|
||||
sorter.sort();
|
||||
if (tableModel.getVisualizationDialog() != null) {
|
||||
tableModel.getVisualizationDialog().repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static DoubleModel addDoubleModel(JPanel panel, String labelKey, String tooltipText, Object source, String sourceKey,
|
||||
UnitGroup unit, double min, Object max, boolean easterEgg) {
|
||||
JLabel label = new JLabel(trans.get("simedtdlg.lbl." + labelKey));
|
||||
panel.add(label);
|
||||
|
||||
DoubleModel model;
|
||||
if (max instanceof Double) {
|
||||
model = new DoubleModel(source, sourceKey, unit, min, (Double) max);
|
||||
} else if (max instanceof DoubleModel) {
|
||||
model = new DoubleModel(source, sourceKey, unit, min, (DoubleModel) max);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid max value");
|
||||
}
|
||||
|
||||
JSpinner spin = new JSpinner(model.getSpinnerModel());
|
||||
spin.setEditor(new SpinnerEditor(spin));
|
||||
if (tooltipText != null) {
|
||||
spin.setToolTipText(tooltipText);
|
||||
}
|
||||
panel.add(spin, "growx");
|
||||
|
||||
if (easterEgg) {
|
||||
addEasterEgg(spin, panel);
|
||||
}
|
||||
|
||||
UnitSelector unitSelector = new UnitSelector(model);
|
||||
panel.add(unitSelector, "growx");
|
||||
|
||||
BasicSlider slider = new BasicSlider(model.getSliderModel());
|
||||
panel.add(slider, "w 75lp, wrap");
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private static DoubleModel addDoubleModel(JPanel panel, String labelKey, String tooltipText, Object source, String sourceKey,
|
||||
UnitGroup unit, double min, Object max) {
|
||||
return addDoubleModel(panel, labelKey, tooltipText, source, sourceKey, unit, min, max, false);
|
||||
}
|
||||
|
||||
private static void adjustColumnWidths(JTable table) {
|
||||
TableColumnModel columnModel = table.getColumnModel();
|
||||
for (int column = 0; column < table.getColumnCount(); column++) {
|
||||
TableColumn tableColumn = columnModel.getColumn(column);
|
||||
int preferredWidth = getPreferredColumnWidth(table, column);
|
||||
tableColumn.setPreferredWidth(preferredWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getPreferredColumnWidth(JTable table, int column) {
|
||||
TableColumn tableColumn = table.getColumnModel().getColumn(column);
|
||||
|
||||
// Get width of column header
|
||||
TableCellRenderer headerRenderer = tableColumn.getHeaderRenderer();
|
||||
if (headerRenderer == null) {
|
||||
headerRenderer = table.getTableHeader().getDefaultRenderer();
|
||||
}
|
||||
Object headerValue = tableColumn.getHeaderValue();
|
||||
Component headerComp = headerRenderer.getTableCellRendererComponent(table, headerValue, false, false, 0, column);
|
||||
int headerWidth = headerComp.getPreferredSize().width;
|
||||
|
||||
// Get maximum width of column data
|
||||
int maxWidth = headerWidth;
|
||||
for (int row = 0; row < table.getRowCount(); row++) {
|
||||
TableCellRenderer cellRenderer = table.getCellRenderer(row, column);
|
||||
Component comp = table.prepareRenderer(cellRenderer, row, column);
|
||||
maxWidth = Math.max(maxWidth, comp.getPreferredSize().width);
|
||||
}
|
||||
|
||||
// Add some padding
|
||||
return maxWidth + 10;
|
||||
}
|
||||
|
||||
private void addDefaultButtons(SimulationOptions options) {
|
||||
JButton restoreDefaults = new JButton(trans.get("simedtdlg.but.resettodefault"));
|
||||
restoreDefaults.addActionListener(e -> {
|
||||
DefaultSimulationOptionFactory f = Application.getInjector().getInstance(DefaultSimulationOptionFactory.class);
|
||||
SimulationOptions defaults = f.getDefault();
|
||||
options.copyConditionsFrom(defaults);
|
||||
});
|
||||
this.add(restoreDefaults, "span, split 3, skip, gapbottom para, gapright para, right");
|
||||
|
||||
JButton saveDefaults = new JButton(trans.get("simedtdlg.but.savedefault"));
|
||||
saveDefaults.addActionListener(e -> {
|
||||
DefaultSimulationOptionFactory f = Application.getInjector().getInstance(DefaultSimulationOptionFactory.class);
|
||||
f.saveDefault(options);
|
||||
});
|
||||
this.add(saveDefaults, "gapbottom para, gapright para, right");
|
||||
}
|
||||
|
||||
private static String getIntensityDescription(double i) {
|
||||
if (i < 0.001)
|
||||
//// None
|
||||
@ -524,4 +704,241 @@ public class SimulationConditionsPanel extends JPanel {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
// Dispose of the visualization dialog if it exists
|
||||
if (visualizationDialog != null) {
|
||||
visualizationDialog.dispose();
|
||||
visualizationDialog = null;
|
||||
}
|
||||
|
||||
// Remove all components from the panel
|
||||
removeAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addNotify() {
|
||||
super.addNotify();
|
||||
|
||||
// Now that the panel is added to a container, we can safely get the parent window
|
||||
Window parent = SwingUtilities.getWindowAncestor(this);
|
||||
if (parent != null) {
|
||||
parent.addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static class WindLevelTableModel extends AbstractTableModel {
|
||||
private final MultiLevelWindModel model;
|
||||
private static final String[] columnNames = {
|
||||
trans.get("simedtdlg.col.Altitude"),
|
||||
trans.get("simedtdlg.col.Unit"),
|
||||
trans.get("simedtdlg.col.Speed"),
|
||||
trans.get("simedtdlg.col.Unit"),
|
||||
trans.get("simedtdlg.col.Direction"),
|
||||
trans.get("simedtdlg.col.Unit"),
|
||||
};
|
||||
private static final UnitGroup[] unitGroups = {
|
||||
UnitGroup.UNITS_DISTANCE, UnitGroup.UNITS_VELOCITY, UnitGroup.UNITS_ANGLE};
|
||||
private final Unit[] currentUnits = {
|
||||
UnitGroup.UNITS_DISTANCE.getDefaultUnit(),
|
||||
UnitGroup.UNITS_VELOCITY.getDefaultUnit(),
|
||||
UnitGroup.UNITS_ANGLE.getDefaultUnit()
|
||||
};
|
||||
private WindLevelVisualizationDialog visualizationDialog;
|
||||
|
||||
public WindLevelTableModel(MultiLevelWindModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public void setVisualizationDialog(WindLevelVisualizationDialog visualizationDialog) {
|
||||
this.visualizationDialog = visualizationDialog;
|
||||
}
|
||||
|
||||
public WindLevelVisualizationDialog getVisualizationDialog() {
|
||||
return visualizationDialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return model.getLevels().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return columnNames.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
return columnNames[column];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex) {
|
||||
return (columnIndex % 2 == 0) ? Double.class : Unit.class;
|
||||
}
|
||||
|
||||
public Object getSIValueAt(int rowIndex, int columnIndex) {
|
||||
MultiLevelWindModel.WindLevel level = model.getLevels().get(rowIndex);
|
||||
return switch (columnIndex) {
|
||||
case 0 -> level.altitude;
|
||||
case 2 -> level.speed;
|
||||
case 4 -> level.direction;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
if (columnIndex % 2 == 0) {
|
||||
Object rawValue = getSIValueAt(rowIndex, columnIndex);
|
||||
if (rawValue == null) {
|
||||
return null;
|
||||
}
|
||||
return currentUnits[columnIndex / 2].toUnit((double) rawValue);
|
||||
} else {
|
||||
return currentUnits[columnIndex / 2];
|
||||
}
|
||||
}
|
||||
|
||||
public Unit[] getCurrentUnits() {
|
||||
return currentUnits;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
MultiLevelWindModel.WindLevel level = model.getLevels().get(rowIndex);
|
||||
if (columnIndex % 2 == 0) {
|
||||
// Value column
|
||||
double value = Double.parseDouble((String) aValue);
|
||||
switch (columnIndex) {
|
||||
case 0:
|
||||
level.altitude = currentUnits[0].fromUnit(value);
|
||||
break;
|
||||
case 2:
|
||||
// Handle negative speed
|
||||
if (value < 0) {
|
||||
level.speed = currentUnits[1].fromUnit(Math.abs(value));
|
||||
// Adjust direction by 180 degrees
|
||||
level.direction = (level.direction + Math.PI) % (2 * Math.PI);
|
||||
} else {
|
||||
level.speed = currentUnits[1].fromUnit(value);
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
level.direction = currentUnits[2].fromUnit(value);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Unit column
|
||||
Unit unit = (Unit) aValue;
|
||||
currentUnits[columnIndex / 2] = unit;
|
||||
if (visualizationDialog != null) {
|
||||
visualizationDialog.updateUnits(currentUnits[0], currentUnits[1]);
|
||||
}
|
||||
}
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void addWindLevel() {
|
||||
List<MultiLevelWindModel.WindLevel> levels = model.getLevels();
|
||||
double newAltitude = levels.isEmpty() ? 0 : levels.get(levels.size() - 1).altitude + 100;
|
||||
double newSpeed = levels.isEmpty() ? 5 : levels.get(levels.size() - 1).speed;
|
||||
double newDirection = levels.isEmpty() ? Math.PI / 2 : levels.get(levels.size() - 1).direction;
|
||||
|
||||
model.addWindLevel(newAltitude, newSpeed, newDirection);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
public void removeWindLevel(int index) {
|
||||
if (index >= 0 && index < model.getLevels().size()) {
|
||||
model.removeWindLevelIdx(index);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public UnitGroup getUnitGroup(int columnIndex) {
|
||||
return unitGroups[columnIndex / 2];
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitSelectorRenderer extends DefaultTableCellRenderer {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
Unit unit = (Unit) value;
|
||||
return super.getTableCellRendererComponent(table, unit.getUnit(), isSelected, hasFocus, row, column);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitSelectorEditor extends DefaultCellEditor {
|
||||
private final JComboBox<Unit> comboBox;
|
||||
|
||||
public UnitSelectorEditor() {
|
||||
super(new JComboBox<>());
|
||||
comboBox = (JComboBox<Unit>) getComponent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
|
||||
WindLevelTableModel model = (WindLevelTableModel) table.getModel();
|
||||
UnitGroup unitGroup = model.getUnitGroup(column);
|
||||
comboBox.removeAllItems();
|
||||
for (Unit unit : unitGroup.getUnits()) {
|
||||
comboBox.addItem(unit);
|
||||
}
|
||||
comboBox.setSelectedItem(value);
|
||||
return comboBox;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ValueCellRenderer extends DefaultTableCellRenderer {
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if (value instanceof Double) {
|
||||
WindLevelTableModel model = (WindLevelTableModel) table.getModel();
|
||||
Unit unit = model.getCurrentUnits()[column / 2];
|
||||
double SIValue = unit.fromUnit((Double) value);
|
||||
value = unit.toString(SIValue);
|
||||
}
|
||||
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SelectAllCellEditor extends DefaultCellEditor {
|
||||
public SelectAllCellEditor() {
|
||||
super(new JTextField());
|
||||
setClickCountToStart(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
|
||||
JTextField textField = (JTextField) super.getTableCellEditorComponent(table, value, isSelected, row, column);
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
textField.selectAll();
|
||||
}
|
||||
});
|
||||
return textField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(EventObject e) {
|
||||
if (e instanceof MouseEvent) {
|
||||
return ((MouseEvent) e).getClickCount() >= getClickCountToStart();
|
||||
}
|
||||
return super.isCellEditable(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,244 @@
|
||||
package info.openrocket.swing.gui.simulation;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.models.wind.MultiLevelWindModel;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dialog;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.WindowListener;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class WindLevelVisualizationDialog extends JDialog {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private final WindLevelVisualization visualization;
|
||||
private final JCheckBox showDirectionsCheckBox;
|
||||
|
||||
public WindLevelVisualizationDialog(Dialog owner, MultiLevelWindModel model, Unit altitudeUnit, Unit speedUnit) {
|
||||
super(owner, trans.get("WindLevelVisualizationDialog.title.WindLevelVisualization"), false);
|
||||
|
||||
visualization = new WindLevelVisualization(model, altitudeUnit, speedUnit);
|
||||
visualization.setPreferredSize(new Dimension(400, 500));
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.add(visualization, BorderLayout.CENTER);
|
||||
|
||||
// Use BorderLayout for the control panel
|
||||
JPanel controlPanel = new JPanel(new BorderLayout());
|
||||
controlPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); // Add some padding
|
||||
|
||||
// Checkbox on the left
|
||||
showDirectionsCheckBox = new JCheckBox(trans.get("WindLevelVisualizationDialog.checkbox.ShowDirections"));
|
||||
showDirectionsCheckBox.setSelected(true);
|
||||
showDirectionsCheckBox.addActionListener(e -> {
|
||||
visualization.setShowDirections(showDirectionsCheckBox.isSelected());
|
||||
visualization.repaint();
|
||||
});
|
||||
controlPanel.add(showDirectionsCheckBox, BorderLayout.WEST);
|
||||
|
||||
// Close button on the right
|
||||
JButton closeButton = new JButton(trans.get("button.close"));
|
||||
closeButton.addActionListener(e -> dispose());
|
||||
JPanel closeButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
|
||||
closeButtonPanel.add(closeButton);
|
||||
controlPanel.add(closeButtonPanel, BorderLayout.EAST);
|
||||
|
||||
contentPane.add(controlPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(contentPane);
|
||||
pack();
|
||||
setLocationRelativeTo(owner);
|
||||
|
||||
setDefaultCloseOperation(HIDE_ON_CLOSE);
|
||||
setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
public void updateUnits(Unit altitudeUnit, Unit speedUnit) {
|
||||
visualization.updateUnits(altitudeUnit, speedUnit);
|
||||
}
|
||||
|
||||
private static class WindLevelVisualization extends JPanel {
|
||||
private final MultiLevelWindModel model;
|
||||
private static final int MARGIN = 50;
|
||||
private static final int ARROW_SIZE = 10;
|
||||
private static final int TICK_LENGTH = 5;
|
||||
|
||||
private Unit altitudeUnit;
|
||||
private Unit speedUnit;
|
||||
private boolean showDirections = true;
|
||||
|
||||
public WindLevelVisualization(MultiLevelWindModel model, Unit altitudeUnit, Unit speedUnit) {
|
||||
this.model = model;
|
||||
this.altitudeUnit = altitudeUnit;
|
||||
this.speedUnit = speedUnit;
|
||||
}
|
||||
|
||||
public void updateUnits(Unit altitudeUnit, Unit speedUnit) {
|
||||
this.altitudeUnit = altitudeUnit;
|
||||
this.speedUnit = speedUnit;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setShowDirections(boolean showDirections) {
|
||||
this.showDirections = showDirections;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
// Enable antialiasing for smoother lines
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// Draw background
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
|
||||
List<MultiLevelWindModel.WindLevel> levels = model.getLevels();
|
||||
if (levels.isEmpty()) return;
|
||||
|
||||
// Sort levels before drawing
|
||||
levels.sort(Comparator.comparingDouble(level -> level.altitude));
|
||||
|
||||
double maxAltitude = levels.stream().mapToDouble(l -> l.altitude).max().orElse(1000);
|
||||
double maxSpeed = levels.stream().mapToDouble(l -> l.speed).max().orElse(10);
|
||||
|
||||
// Extend axis ranges by 10% for drawing
|
||||
double extendedMaxAltitude = maxAltitude * 1.1;
|
||||
double extendedMaxSpeed = maxSpeed * 1.1;
|
||||
|
||||
// Draw axes
|
||||
drawAxes(g2d, width, height, maxSpeed, maxAltitude, extendedMaxSpeed, extendedMaxAltitude);
|
||||
|
||||
// Draw wind levels
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
MultiLevelWindModel.WindLevel level = levels.get(i);
|
||||
|
||||
int x = MARGIN + (int) (level.speed / extendedMaxSpeed * (width - 2 * MARGIN));
|
||||
int y = height - MARGIN - (int) (level.altitude / extendedMaxAltitude * (height - 2 * MARGIN));
|
||||
|
||||
// Draw point
|
||||
g2d.setColor(Color.BLUE);
|
||||
g2d.fillOval(x - 3, y - 3, 6, 6);
|
||||
|
||||
// Draw wind direction arrow
|
||||
if (showDirections) {
|
||||
drawWindArrow(g2d, x, y, level.direction);
|
||||
}
|
||||
|
||||
// Draw connecting line if not the first point
|
||||
if (i > 0) {
|
||||
MultiLevelWindModel.WindLevel prevLevel = levels.get(i - 1);
|
||||
int prevX = MARGIN + (int) (prevLevel.speed / extendedMaxSpeed * (width - 2 * MARGIN));
|
||||
int prevY = height - MARGIN - (int) (prevLevel.altitude / extendedMaxAltitude * (height - 2 * MARGIN));
|
||||
g2d.setColor(Color.GRAY);
|
||||
g2d.drawLine(prevX, prevY, x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAxes(Graphics2D g2d, int width, int height, double maxSpeed, double maxAltitude,
|
||||
double extendedMaxSpeed, double extendedMaxAltitude) {
|
||||
g2d.setColor(Color.BLACK);
|
||||
|
||||
// Draw X-axis
|
||||
g2d.drawLine(MARGIN, height - MARGIN, width - MARGIN, height - MARGIN);
|
||||
drawFilledArrowHead(g2d, width - MARGIN + (ARROW_SIZE-2), height - MARGIN, ARROW_SIZE, 0);
|
||||
|
||||
// Draw Y-axis
|
||||
g2d.drawLine(MARGIN, height - MARGIN, MARGIN, MARGIN);
|
||||
drawFilledArrowHead(g2d, MARGIN, MARGIN - (ARROW_SIZE-2), ARROW_SIZE, -Math.PI / 2);
|
||||
|
||||
// Draw max value ticks and labels
|
||||
g2d.setFont(g2d.getFont().deriveFont(10f));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
|
||||
// X-axis max value
|
||||
int xTickX = MARGIN + (int) ((maxSpeed / extendedMaxSpeed) * (width - 2 * MARGIN));
|
||||
g2d.drawLine(xTickX, height - MARGIN, xTickX, height - MARGIN + TICK_LENGTH);
|
||||
String xMaxLabel = speedUnit.toString(maxSpeed);
|
||||
g2d.drawString(xMaxLabel, xTickX - fm.stringWidth(xMaxLabel) / 2, height - MARGIN + TICK_LENGTH + fm.getHeight());
|
||||
|
||||
// Y-axis max value
|
||||
int yTickY = height - MARGIN - (int) ((maxAltitude / extendedMaxAltitude) * (height - 2 * MARGIN));
|
||||
g2d.drawLine(MARGIN - TICK_LENGTH, yTickY, MARGIN, yTickY);
|
||||
String yMaxLabel = altitudeUnit.toString(maxAltitude);
|
||||
g2d.drawString(yMaxLabel, MARGIN - TICK_LENGTH - fm.stringWidth(yMaxLabel) - 2, yTickY + fm.getAscent() / 2);
|
||||
|
||||
// Draw axis labels
|
||||
g2d.setFont(g2d.getFont().deriveFont(12f));
|
||||
fm = g2d.getFontMetrics();
|
||||
|
||||
// X-axis label
|
||||
String xLabel = trans.get("WindLevelVisualizationDialog.lbl.WindSpeed") + " (" + speedUnit.getUnit() + ")";
|
||||
g2d.drawString(xLabel, width / 2 - fm.stringWidth(xLabel) / 2, height - 10);
|
||||
|
||||
// Y-axis label
|
||||
String yLabel = trans.get("WindLevelVisualizationDialog.lbl.Altitude") + " (" + altitudeUnit.getUnit() + ")";
|
||||
AffineTransform originalTransform = g2d.getTransform();
|
||||
g2d.rotate(-Math.PI / 2);
|
||||
g2d.drawString(yLabel, -height / 2 - fm.stringWidth(yLabel) / 2, MARGIN / 2);
|
||||
g2d.setTransform(originalTransform);
|
||||
}
|
||||
|
||||
private void drawFilledArrowHead(Graphics2D g, int x, int y, int arrowSize, double angle) {
|
||||
int[] xPoints = new int[3];
|
||||
int[] yPoints = new int[3];
|
||||
|
||||
xPoints[0] = x;
|
||||
yPoints[0] = y;
|
||||
xPoints[1] = x - (int) (arrowSize * Math.cos(angle - Math.PI / 6));
|
||||
yPoints[1] = y - (int) (arrowSize * Math.sin(angle - Math.PI / 6));
|
||||
xPoints[2] = x - (int) (arrowSize * Math.cos(angle + Math.PI / 6));
|
||||
yPoints[2] = y - (int) (arrowSize * Math.sin(angle + Math.PI / 6));
|
||||
|
||||
g.fillPolygon(xPoints, yPoints, 3);
|
||||
}
|
||||
|
||||
private void drawWindArrow(Graphics2D g, int x, int y, double direction) {
|
||||
int directionVectorLength = 15;
|
||||
int dx = (int) (directionVectorLength * Math.sin(direction));
|
||||
int dy = (int) (directionVectorLength * Math.cos(direction));
|
||||
int arrowSize = 10;
|
||||
|
||||
g.setColor(Color.RED);
|
||||
|
||||
// Draw the main line
|
||||
g.drawLine(x, y, x + dx, y - dy);
|
||||
|
||||
int dx_arrow = (int) ((arrowSize-1) * Math.sin(direction));
|
||||
int dy_arrow = (int) ((arrowSize-1) * Math.cos(direction));
|
||||
|
||||
// Draw filled arrow head
|
||||
drawFilledArrowHead(g, x + dx + dx_arrow, y - dy - dy_arrow, arrowSize, direction - Math.PI/2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
for (WindowListener listener : getWindowListeners()) {
|
||||
removeWindowListener(listener);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -40,7 +40,6 @@ import info.openrocket.core.rocketcomponent.RailButton;
|
||||
import info.openrocket.core.rocketcomponent.RecoveryDevice;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.rocketcomponent.TubeFinSet;
|
||||
import info.openrocket.core.simulation.SimulationOptionsInterface;
|
||||
import info.openrocket.core.util.ORColor;
|
||||
import info.openrocket.core.arch.SystemInfo;
|
||||
import info.openrocket.core.document.Simulation;
|
||||
@ -59,7 +58,7 @@ import info.openrocket.core.util.BuildProperties;
|
||||
import info.openrocket.swing.communication.AssetHandler.UpdatePlatform;
|
||||
|
||||
|
||||
public class SwingPreferences extends ApplicationPreferences implements SimulationOptionsInterface {
|
||||
public class SwingPreferences extends ApplicationPreferences {
|
||||
private static final Logger log = LoggerFactory.getLogger(SwingPreferences.class);
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user