Also use pink noise for multi-level wind

This commit is contained in:
SiboVG 2024-09-21 05:12:33 +01:00
parent 19f8e3c8f3
commit ea210bac8d
20 changed files with 556 additions and 406 deletions

View File

@ -16,7 +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.models.wind.MultiLevelPinkNoiseWindModel;
import info.openrocket.core.preferences.DocumentPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -339,23 +339,25 @@ public class OpenRocketSaver extends RocketSaver {
writeElement("launchroddirection", cond.getLaunchRodDirection() * 360.0 / (2.0 * Math.PI));
// 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());
writeElement("windaverage", cond.getAverageWindModel().getAverage());
writeElement("windturbulence", cond.getAverageWindModel().getTurbulenceIntensity());
writeElement("winddirection", cond.getAverageWindModel().getDirection());
writeln("<wind model=\"pinknoise\">");
writeln("<wind model=\"average\">");
indent++;
writeElement("windaverage", cond.getPinkNoiseWindModel().getAverage());
writeElement("windturbulence", cond.getPinkNoiseWindModel().getTurbulenceIntensity());
writeElement("winddirection", cond.getPinkNoiseWindModel().getDirection());
writeElement("speed", cond.getAverageWindModel().getAverage());
writeElement("direction", cond.getAverageWindModel().getDirection());
writeElement("standarddeviation", cond.getAverageWindModel().getStandardDeviation());
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 + "\"/>");
for (MultiLevelPinkNoiseWindModel.LevelWindModel level : cond.getMultiLevelWindModel().getLevels()) {
writeln("<windlevel altitude=\"" + level.getAltitude() + "\" speed=\"" + level.getSpeed() +
"\" direction=\"" + level.getDirection() + "\" standarddeviation=\"" + level.getStandardDeviation() +
"\"/>");
}
indent--;
writeln("</wind>");

View File

@ -83,21 +83,21 @@ class SimulationConditionsHandler extends AbstractElementHandler {
if (Double.isNaN(d)) {
warnings.add("Illegal average windspeed defined, ignoring.");
} else {
options.getPinkNoiseWindModel().setAverage(d);
options.getAverageWindModel().setAverage(d);
}
}
case "windturbulence" -> {
if (Double.isNaN(d)) {
warnings.add("Illegal wind turbulence intensity defined, ignoring.");
} else {
options.getPinkNoiseWindModel().setTurbulenceIntensity(d);
options.getAverageWindModel().setTurbulenceIntensity(d);
}
}
case "winddirection" -> {
if (Double.isNaN(d)) {
warnings.add("Illegal wind direction defined, ignoring.");
} else {
options.getPinkNoiseWindModel().setDirection(d);
options.getAverageWindModel().setDirection(d);
}
}

View File

@ -33,18 +33,22 @@ public class WindHandler extends AbstractElementHandler {
} catch (NumberFormatException ignore) {
}
if ("pinknoise".equals(model)) {
if (element.equals("windaverage")) {
if (!Double.isNaN(d)) {
options.getPinkNoiseWindModel().setAverage(d);
if ("average".equals(model)) {
switch (element) {
case "speed" -> {
if (!Double.isNaN(d)) {
options.getAverageWindModel().setAverage(d);
}
}
} else if (element.equals("windturbulence")) {
if (!Double.isNaN(d)) {
options.getPinkNoiseWindModel().setTurbulenceIntensity(d);
case "direction" -> {
if (!Double.isNaN(d)) {
options.getAverageWindModel().setDirection(d);
}
}
} else if (element.equals("winddirection")) {
if (!Double.isNaN(d)) {
options.getPinkNoiseWindModel().setDirection(d);
case "standarddeviation" -> {
if (!Double.isNaN(d)) {
options.getAverageWindModel().setStandardDeviation(d);
}
}
}
} else if ("multilevel".equals(model)) {
@ -52,14 +56,15 @@ public class WindHandler extends AbstractElementHandler {
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);
double standardDeviation = Double.parseDouble(attributes.get("standarddeviation"));
options.getMultiLevelWindModel().addWindLevel(altitude, speed, direction, standardDeviation);
}
}
}
public void storeSettings(SimulationOptions options, WarningSet warnings) {
if ("pinknoise".equals(model)) {
options.setWindModelType(WindModelType.PINK_NOISE);
if ("average".equals(model)) {
options.setWindModelType(WindModelType.AVERAGE);
} else if ("multilevel".equals(model)) {
options.setWindModelType(WindModelType.MULTI_LEVEL);
} else {

View File

@ -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.getPinkNoiseWindModel().getAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
setWindSpeed(options.getAverageWindModel().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.getPinkNoiseWindModel().getAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
setWindSpeed(prefs.getAverageWindModel().getAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
}
public Double getAltitude() {

View File

@ -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.getPinkNoiseWindModel().setAverage(
launchSiteSettings.getAverageWindModel().setAverage(
Double.parseDouble(content) / RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
}
} catch (NumberFormatException e) {

View File

@ -0,0 +1,244 @@
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.ModID;
public class MultiLevelPinkNoiseWindModel implements WindModel {
private List<LevelWindModel> levels;
public MultiLevelPinkNoiseWindModel() {
this.levels = new ArrayList<>();
}
public void addWindLevel(double altitude, double speed, double direction, double standardDeviation) {
PinkNoiseWindModel pinkNoiseModel = new PinkNoiseWindModel();
pinkNoiseModel.setAverage(speed);
pinkNoiseModel.setStandardDeviation(standardDeviation);
pinkNoiseModel.setDirection(direction);
LevelWindModel newLevel = new LevelWindModel(altitude, pinkNoiseModel);
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 void clearLevels() {
levels.clear();
}
public List<LevelWindModel> getLevels() {
return new ArrayList<>(levels);
}
public void sortLevels() {
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 LevelWindModel(altitude, null),
Comparator.comparingDouble(l -> l.altitude));
// Retrieve the wind level if it exists
if (index >= 0) {
return levels.get(index).model.getWindVelocity(time, altitude);
}
// Extrapolation (take the value of the outer bounds)
int insertionPoint = -index - 1;
if (insertionPoint == 0) {
return levels.get(0).model.getWindVelocity(time, altitude);
}
if (insertionPoint == levels.size()) {
return levels.get(levels.size() - 1).model.getWindVelocity(time, altitude);
}
// Interpolation (take the value between the closest two bounds)
LevelWindModel lowerLevel = levels.get(insertionPoint - 1);
LevelWindModel upperLevel = levels.get(insertionPoint);
double fraction = (altitude - lowerLevel.altitude) / (upperLevel.altitude - lowerLevel.altitude);
Coordinate lowerVelocity = lowerLevel.model.getWindVelocity(time, altitude);
Coordinate upperVelocity = upperLevel.model.getWindVelocity(time, altitude);
return lowerVelocity.interpolate(upperVelocity, fraction);
}
private static double getInterpolatedDirection(LevelWindModel lowerLevel, LevelWindModel upperLevel, double fractionBetweenLevels) {
double lowerDirection = lowerLevel.model.getDirection();
double upperDirection = upperLevel.model.getDirection();
double directionDifference = upperDirection - lowerDirection;
// Ensure we take the shortest path around the circle
if (directionDifference > Math.PI) {
directionDifference -= 2 * Math.PI;
} else if (directionDifference < -Math.PI) {
directionDifference += 2 * Math.PI;
}
double interpolatedDirection = lowerDirection + fractionBetweenLevels * directionDifference;
return interpolatedDirection;
}
private double getPinkNoiseValue(double time, LevelWindModel lower, LevelWindModel upper, double fraction) {
double lowerNoise = lower.model.getWindVelocity(time, lower.altitude).length() - lower.model.getAverage();
double upperNoise = upper.model.getWindVelocity(time, upper.altitude).length() - upper.model.getAverage();
return lowerNoise + fraction * (upperNoise - lowerNoise);
}
public double getWindDirection(double altitude) {
if (levels.isEmpty()) {
return 0;
}
int index = Collections.binarySearch(levels, new LevelWindModel(altitude, null),
Comparator.comparingDouble(l -> l.altitude));
if (index >= 0) {
return levels.get(index).model.getDirection();
}
int insertionPoint = -index - 1;
if (insertionPoint == 0) {
return levels.get(0).model.getDirection();
}
if (insertionPoint == levels.size()) {
return levels.get(levels.size() - 1).model.getDirection();
}
// Interpolation (take the value between the closest two bounds)
LevelWindModel lowerLevel = levels.get(insertionPoint - 1);
LevelWindModel upperLevel = levels.get(insertionPoint);
double fractionBetweenLevels = (altitude - lowerLevel.altitude) / (upperLevel.altitude - lowerLevel.altitude);
// Interpolate direction
double interpolatedDirection = getInterpolatedDirection(lowerLevel, upperLevel, fractionBetweenLevels);
// Normalize the result to be between 0 and 2*PI
return (interpolatedDirection + 2 * Math.PI) % (2 * Math.PI);
}
@Override
public ModID getModID() {
return ModID.ZERO; // You might want to create a specific ModID for this model
}
public void loadFrom(MultiLevelPinkNoiseWindModel source) {
this.levels.clear();
for (LevelWindModel level : source.levels) {
this.levels.add(level.clone());
}
}
@Override
public MultiLevelPinkNoiseWindModel clone() {
try {
MultiLevelPinkNoiseWindModel clone = (MultiLevelPinkNoiseWindModel) 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;
MultiLevelPinkNoiseWindModel that = (MultiLevelPinkNoiseWindModel) o;
return levels.equals(that.levels);
}
@Override
public int hashCode() {
return levels.hashCode();
}
public static class LevelWindModel implements Cloneable {
protected double altitude;
protected PinkNoiseWindModel model;
LevelWindModel(double altitude, PinkNoiseWindModel model) {
this.altitude = altitude;
this.model = model;
}
public double getAltitude() {
return altitude;
}
public void setAltitude(double altitude) {
this.altitude = altitude;
}
public double getSpeed() {
return model.getAverage();
}
public void setSpeed(double speed) {
model.setAverage(speed);
}
public double getDirection() {
return model.getDirection();
}
public void setDirection(double direction) {
model.setDirection(direction);
}
public double getStandardDeviation() {
return model.getStandardDeviation();
}
public void setStandardDeviation(double standardDeviation) {
model.setStandardDeviation(standardDeviation);
}
public double getTurblenceIntensity() {
return model.getTurbulenceIntensity();
}
public void setTurbulenceIntensity(double turbulenceIntensity) {
model.setTurbulenceIntensity(turbulenceIntensity);
}
@Override
public LevelWindModel clone() {
try {
LevelWindModel clone = (LevelWindModel) super.clone();
clone.model = this.model.clone();
return clone;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // This should never happen
}
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
LevelWindModel that = (LevelWindModel) obj;
return Double.compare(that.altitude, altitude) == 0 && model.equals(that.model);
}
}
}

View File

@ -1,193 +0,0 @@
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 = Math.max(speed, 0);
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;
}
}
}

View File

@ -1,14 +1,11 @@
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
@ -37,7 +34,7 @@ public class PinkNoiseWindModel implements WindModel {
private static final double STDDEV = 2.252;
/** Time difference between random samples. */
private static final double DELTA_T = 0.05;
public static final double DELTA_T = 0.05;
private double average = 0;
private double direction = Math.PI / 2; // this is an East wind

View File

@ -1,7 +1,7 @@
package info.openrocket.core.models.wind;
public enum WindModelType {
PINK_NOISE("PinkNoise"),
AVERAGE("Average"),
MULTI_LEVEL("MultiLevel");
private final String stringValue;

View File

@ -155,7 +155,7 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
private PinkNoiseWindModel pinkNoiseWindModel = null;
private PinkNoiseWindModel averageWindModel = null;
/*
@ -398,25 +398,25 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
double turbulenceIntensity = getDouble(WIND_TURBULENCE, 0.1);
double direction = getDouble(WIND_DIRECTION, Math.PI / 2);
getPinkNoiseWindModel().setAverage(average);
getPinkNoiseWindModel().setTurbulenceIntensity(turbulenceIntensity);
getPinkNoiseWindModel().setDirection(direction);
getAverageWindModel().setAverage(average);
getAverageWindModel().setTurbulenceIntensity(turbulenceIntensity);
getAverageWindModel().setDirection(direction);
}
protected void storeWindModelState() {
putDouble(WIND_AVERAGE, getPinkNoiseWindModel().getAverage());
putDouble(WIND_TURBULENCE, getPinkNoiseWindModel().getTurbulenceIntensity());
putDouble(WIND_DIRECTION, getPinkNoiseWindModel().getDirection());
putDouble(WIND_AVERAGE, getAverageWindModel().getAverage());
putDouble(WIND_TURBULENCE, getAverageWindModel().getTurbulenceIntensity());
putDouble(WIND_DIRECTION, getAverageWindModel().getDirection());
}
@Override
public PinkNoiseWindModel getPinkNoiseWindModel() {
if (pinkNoiseWindModel == null) {
pinkNoiseWindModel = new PinkNoiseWindModel();
pinkNoiseWindModel.addChangeListener(this);
public PinkNoiseWindModel getAverageWindModel() {
if (averageWindModel == null) {
averageWindModel = new PinkNoiseWindModel();
averageWindModel.addChangeListener(this);
loadWindModelState();
}
return pinkNoiseWindModel;
return averageWindModel;
}
public double getLaunchAltitude() {
@ -1271,7 +1271,7 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
@Override
public void stateChanged(EventObject e) {
if (e.getSource() == pinkNoiseWindModel) {
if (e.getSource() == averageWindModel) {
storeWindModelState();
}
}

View File

@ -35,9 +35,9 @@ public class DefaultSimulationOptionFactory {
SimulationOptions defaults = new SimulationOptions();
if (prefs != null) {
defaults.getPinkNoiseWindModel().setAverage(prefs.getPinkNoiseWindModel().getAverage());
defaults.getPinkNoiseWindModel().setStandardDeviation(prefs.getPinkNoiseWindModel().getStandardDeviation());
defaults.getPinkNoiseWindModel().setTurbulenceIntensity(prefs.getPinkNoiseWindModel().getTurbulenceIntensity());
defaults.getAverageWindModel().setAverage(prefs.getAverageWindModel().getAverage());
defaults.getAverageWindModel().setStandardDeviation(prefs.getAverageWindModel().getStandardDeviation());
defaults.getAverageWindModel().setTurbulenceIntensity(prefs.getAverageWindModel().getTurbulenceIntensity());
defaults.setLaunchLatitude(prefs.getDouble(SIMCONDITION_SITE_LAT, defaults.getLaunchLatitude()));
defaults.setLaunchLongitude(prefs.getDouble(SIMCONDITION_SITE_LON, defaults.getLaunchLongitude()));
@ -58,9 +58,9 @@ public class DefaultSimulationOptionFactory {
public void saveDefault(SimulationOptions newDefaults) {
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_WIND_SPEED, newDefaults.getAverageWindModel().getAverage());
prefs.putDouble(SIMCONDITION_WIND_STDDEV, newDefaults.getAverageWindModel().getStandardDeviation());
prefs.putDouble(SIMCONDITION_WIND_TURB, newDefaults.getAverageWindModel().getTurbulenceIntensity());
prefs.putDouble(SIMCONDITION_SITE_LAT, newDefaults.getLaunchLatitude());
prefs.putDouble(SIMCONDITION_SITE_LON, newDefaults.getLaunchLongitude());

View File

@ -6,7 +6,7 @@ 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.MultiLevelPinkNoiseWindModel;
import info.openrocket.core.models.wind.WindModel;
import info.openrocket.core.models.wind.WindModelType;
import info.openrocket.core.preferences.ApplicationPreferences;
@ -80,13 +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;
private WindModelType windModelType = WindModelType.AVERAGE;
private final PinkNoiseWindModel averageWindModel;
private final MultiLevelPinkNoiseWindModel multiLevelPinkNoiseWindModel;
public SimulationOptions() {
pinkNoiseWindModel = new PinkNoiseWindModel(randomSeed);
multiLevelWindModel = new MultiLevelWindModel();
averageWindModel = new PinkNoiseWindModel(randomSeed);
multiLevelPinkNoiseWindModel = new MultiLevelPinkNoiseWindModel();
}
public double getLaunchRodLength() {
@ -126,10 +126,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
public double getLaunchRodDirection() {
if (launchIntoWind) {
double windDirection;
if (windModelType == WindModelType.PINK_NOISE) {
windDirection = pinkNoiseWindModel.getDirection();
if (windModelType == WindModelType.AVERAGE) {
windDirection = averageWindModel.getDirection();
} else {
windDirection = multiLevelWindModel.getWindDirection(launchAltitude);
windDirection = multiLevelPinkNoiseWindModel.getWindDirection(launchAltitude);
}
this.setLaunchRodDirection(windDirection);
}
@ -156,21 +156,21 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
}
public WindModel getWindModel() {
if (windModelType == WindModelType.PINK_NOISE) {
return pinkNoiseWindModel;
if (windModelType == WindModelType.AVERAGE) {
return averageWindModel;
} else if (windModelType == WindModelType.MULTI_LEVEL) {
return multiLevelWindModel;
return multiLevelPinkNoiseWindModel;
} else {
throw new IllegalArgumentException("Unknown wind model type: " + windModelType);
}
}
public PinkNoiseWindModel getPinkNoiseWindModel() {
return pinkNoiseWindModel;
public PinkNoiseWindModel getAverageWindModel() {
return averageWindModel;
}
public MultiLevelWindModel getMultiLevelWindModel() {
return multiLevelWindModel;
public MultiLevelPinkNoiseWindModel getMultiLevelWindModel() {
return multiLevelPinkNoiseWindModel;
}
public double getLaunchAltitude() {
@ -349,13 +349,13 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
// changed.
boolean isChanged = false;
if (!this.pinkNoiseWindModel.equals(src.pinkNoiseWindModel)) {
if (!this.averageWindModel.equals(src.averageWindModel)) {
isChanged = true;
this.pinkNoiseWindModel.loadFrom(src.pinkNoiseWindModel);
this.averageWindModel.loadFrom(src.averageWindModel);
}
if (!this.multiLevelWindModel.equals(src.multiLevelWindModel)) {
if (!this.multiLevelPinkNoiseWindModel.equals(src.multiLevelPinkNoiseWindModel)) {
isChanged = true;
this.multiLevelWindModel.loadFrom(src.multiLevelWindModel);
this.multiLevelPinkNoiseWindModel.loadFrom(src.multiLevelPinkNoiseWindModel);
}
if (this.launchAltitude != src.launchAltitude) {
@ -441,8 +441,8 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
MathUtil.equals(this.timeStep, o.timeStep)) &&
this.windModelType == o.windModelType &&
this.pinkNoiseWindModel.equals(o.pinkNoiseWindModel) &&
this.multiLevelWindModel.equals(o.multiLevelWindModel);
this.averageWindModel.equals(o.averageWindModel) &&
this.multiLevelPinkNoiseWindModel.equals(o.multiLevelPinkNoiseWindModel);
}
/**
@ -514,8 +514,8 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
.concat(String.format(" launchRodAngle: %f\n", launchRodAngle))
.concat(String.format(" launchRodDirection: %f\n", launchRodDirection))
.concat(String.format(" windModelType: %s\n", windModelType))
.concat(String.format(" pinkNoiseWindModel: %s\n", pinkNoiseWindModel))
.concat(String.format(" multiLevelWindModel: %s\n", multiLevelWindModel))
.concat(String.format(" pinkNoiseWindModel: %s\n", averageWindModel))
.concat(String.format(" multiLevelPinkNoiseWindModel: %s\n", multiLevelPinkNoiseWindModel))
.concat(String.format(" launchAltitude: %f\n", launchAltitude))
.concat(String.format(" launchLatitude: %f\n", launchLatitude))
.concat(String.format(" launchLongitude: %f\n", launchLongitude))

View File

@ -21,7 +21,7 @@ public interface SimulationOptionsInterface extends ChangeSource {
void setLaunchRodDirection(double launchRodDirection);
PinkNoiseWindModel getPinkNoiseWindModel();
PinkNoiseWindModel getAverageWindModel();
double getLaunchAltitude();

View File

@ -506,6 +506,9 @@ simedtdlg.lbl.ttip.Latitude = <html>The launch site latitude affects the gravita
simedtdlg.col.Altitude = Altitude
simedtdlg.col.Speed = Speed
simedtdlg.col.Direction = Direction
simedtdlg.col.StandardDeviation = Deviation
simedtdlg.col.Turbulence = Turbulence
simedtdlg.col.Intensity = Intensity
simedtdlg.col.Unit = Unit
simedtdlg.lbl.Longitude = Longitude:
@ -557,8 +560,8 @@ 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.AverageWind = Average
simedtdlg.radio.AverageWind.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

View File

@ -9,25 +9,29 @@ import info.openrocket.core.util.Coordinate;
import info.openrocket.core.util.ModID;
import info.openrocket.core.util.StateChangeListener;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
class MultiLevelWindModelTest {
private final static double EPSILON = MathUtil.EPSILON;
private static final double DELTA_T = PinkNoiseWindModel.DELTA_T;
private static final int SAMPLE_SIZE = 1000;
private MultiLevelWindModel model;
private MultiLevelPinkNoiseWindModel model;
@BeforeEach
void setUp() {
model = new MultiLevelWindModel();
model = new MultiLevelPinkNoiseWindModel();
}
@Test
@DisplayName("Add and remove wind levels")
void testAddAndRemoveWindLevels() {
model.addWindLevel(100, 5, Math.PI / 4);
model.addWindLevel(200, 10, Math.PI / 2);
model.addWindLevel(100, 5, Math.PI / 4, 1);
model.addWindLevel(200, 10, Math.PI / 2, 1);
assertEquals(2, model.getLevels().size());
model.removeWindLevel(100);
@ -45,51 +49,73 @@ class MultiLevelWindModelTest {
@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));
model.addWindLevel(100, 5, Math.PI / 4, 1);
assertThrows(IllegalArgumentException.class, () -> model.addWindLevel(100, 10, Math.PI / 2, 1));
}
@Test
@DisplayName("Get wind velocity")
void testGetWindVelocity() {
model.addWindLevel(0, 5, 0);
model.addWindLevel(1000, 10, Math.PI / 2);
model.addWindLevel(0, 5, 0, 1);
model.addWindLevel(1000, 10, Math.PI / 2, 2);
verifyWind(500, 7.5, Math.PI / 4);
verifyWind(0, 5, 0, 1);
verifyWind(1000, 10, Math.PI / 2, 2);
}
@Test
@DisplayName("Interpolation between levels")
void testInterpolationBetweenLevels() {
model.addWindLevel(0, 5, 0);
model.addWindLevel(1000, 10, Math.PI);
// Test speed interpolation
model.addWindLevel(0, 5, 0, 0.1);
model.addWindLevel(1000, 10, 0, 0.3);
verifyWind(200, 6, Math.PI / 5);
verifyWind(500, 7.5, Math.PI / 2);
verifyWind(900, 9.5, 9 * Math.PI / 10);
verifyWind(200, 6, 0, 0.14);
verifyWind(500, 7.5, 0, 0.2);
verifyWind(900, 9.5, 0, 0.28);
model.clearLevels();
// Test direction interpolation when speed vectors are parallel
model.addWindLevel(0, 5, 0, 0);
model.addWindLevel(1000, 5, Math.PI, 0);
verifyWind(200, 3, 0, EPSILON);
verifyWind(501, 0, Math.PI, 0.01);
verifyWind(900, 4, Math.PI, EPSILON);
model.clearLevels();
// Test direction interpolation when speed vectors are not parallel
model.addWindLevel(0, 5, 0, 0);
model.addWindLevel(1000, 5, Math.PI / 2, 0);
verifyWind(200, 4.1231056256, 0.2449786631, EPSILON);
verifyWind(500, 3.5355339059, Math.PI / 4, EPSILON);
verifyWind(800, 4.1231056256, 1.3258176637, EPSILON);
}
@Test
@DisplayName("Extrapolation outside levels")
void testExtrapolationOutsideLevels() {
model.addWindLevel(100, 5, 0);
model.addWindLevel(200, 10, Math.PI / 2);
model.addWindLevel(100, 5, 0, 1.4);
model.addWindLevel(200, 10, Math.PI / 2, 2.2);
verifyWind(0, 5, 0);
verifyWind(300, 10, Math.PI / 2);
verifyWind(1000, 10, Math.PI / 2);
verifyWind(0, 5, 0, 1.4);
verifyWind(300, 10, Math.PI / 2, 2.2);
verifyWind(1000, 10, Math.PI / 2, 2.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);
void testSortLevels() {
model.addWindLevel(200, 10, Math.PI / 2, 1);
model.addWindLevel(100, 5, Math.PI / 4, 1);
model.addWindLevel(300, 15, 3 * Math.PI / 4, 1);
model.resortLevels();
model.sortLevels();
List<MultiLevelWindModel.WindLevel> levels = model.getLevels();
List<MultiLevelPinkNoiseWindModel.LevelWindModel> levels = model.getLevels();
assertEquals(3, levels.size());
assertEquals(100, levels.get(0).altitude, EPSILON);
assertEquals(200, levels.get(1).altitude, EPSILON);
@ -99,24 +125,24 @@ class MultiLevelWindModelTest {
@Test
@DisplayName("Clone model")
void testClone() {
model.addWindLevel(100, 5, Math.PI / 4);
model.addWindLevel(200, 10, Math.PI / 2);
model.addWindLevel(100, 5, Math.PI / 4, 1);
model.addWindLevel(200, 10, Math.PI / 2, 2);
MultiLevelWindModel clonedModel = model.clone();
MultiLevelPinkNoiseWindModel clonedModel = model.clone();
assertNotSame(model, clonedModel);
assertEquals(model, clonedModel);
clonedModel.addWindLevel(300, 15, 3 * Math.PI / 4);
clonedModel.addWindLevel(300, 15, 3 * Math.PI / 4, 1);
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);
model.addWindLevel(100, 5, Math.PI / 4, 2);
model.addWindLevel(200, 10, Math.PI / 2, 1);
MultiLevelWindModel newModel = new MultiLevelWindModel();
MultiLevelPinkNoiseWindModel newModel = new MultiLevelPinkNoiseWindModel();
newModel.loadFrom(model);
assertEquals(model, newModel);
@ -144,12 +170,34 @@ class MultiLevelWindModelTest {
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);
private void verifyWind(double altitude, double expectedSpeed, double expectedDirection, double standardDeviation) {
double[] speeds = new double[SAMPLE_SIZE];
double[] directions = new double[SAMPLE_SIZE];
for (int i = 0; i < SAMPLE_SIZE; i++) {
Coordinate velocity = model.getWindVelocity(i * DELTA_T, altitude);
speeds[i] = velocity.length();
directions[i] = Math.atan2(velocity.x, velocity.y);
}
double avgSpeed = Arrays.stream(speeds, 0, SAMPLE_SIZE).average().orElse(0.0);
double avgDirection = averageAngle(directions);
// Check average speed and direction
assertEquals(expectedSpeed, avgSpeed, standardDeviation, "Average wind speed at altitude " + altitude);
assertEquals(expectedDirection, avgDirection, EPSILON, "Average wind direction at altitude " + altitude);
// Check that some values are above and below the expected value
//assertTrue(IntStream.range(0, SAMPLE_SIZE).anyMatch(i -> speeds[i] >= expectedSpeed));
//assertTrue(IntStream.range(0, SAMPLE_SIZE).anyMatch(i -> speeds[i] <= expectedSpeed));
}
private double averageAngle(double[] angles) {
double sumSin = 0, sumCos = 0;
for (double angle : angles) {
sumSin += Math.sin(angle);
sumCos += Math.cos(angle);
}
return Math.atan2(sumSin, sumCos);
}
}

View File

@ -69,7 +69,7 @@ public class FlightEventsTest extends BaseTestCase {
final Simulation sim = new Simulation(rocket);
sim.getOptions().setISAAtmosphere(true);
sim.getOptions().setTimeStep(0.05);
sim.getOptions ().getPinkNoiseWindModel().setAverage(0.1);
sim.getOptions ().getAverageWindModel().setAverage(0.1);
rocket.getSelectedConfiguration().setAllStages();
FlightConfigurationId fcid = rocket.getSelectedConfiguration().getFlightConfigurationID();
sim.setFlightConfigurationId(fcid);

View File

@ -10,7 +10,7 @@ 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.MultiLevelPinkNoiseWindModel;
import info.openrocket.core.models.wind.PinkNoiseWindModel;
import info.openrocket.core.plugin.PluginModule;
import info.openrocket.core.preferences.ApplicationPreferences;
@ -65,17 +65,17 @@ public class SimulationConditionsTest {
assertEquals(Math.PI / 2, options.getLaunchRodDirection(), EPSILON);
assertEquals(0.0, options.getLaunchRodAngle(), EPSILON);
assertTrue(options.getLaunchIntoWind());
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(Math.PI / 2, options.getAverageWindModel().getDirection(), EPSILON);
assertEquals(0.1, options.getAverageWindModel().getTurbulenceIntensity(), EPSILON);
assertEquals(2.0, options.getAverageWindModel().getAverage(), EPSILON);
assertEquals(0.2, options.getAverageWindModel().getStandardDeviation(), EPSILON);
assertEquals(0.05, options.getTimeStep(), EPSILON);
assertEquals(3 * Math.PI / 180, options.getMaximumStepAngle(), EPSILON);
}
@Test
@DisplayName("Compare PinkNoiseWindModel and MultiLevelWindModel in SimulationConditions")
@DisplayName("Compare PinkNoiseWindModel and MultiLevelPinkNoiseWindModel in SimulationConditions")
public void testWindModelComparison() {
SimulationConditions conditions = new SimulationConditions();
@ -91,10 +91,10 @@ public class SimulationConditionsTest {
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);
// Test MultiLevelPinkNoiseWindModel
MultiLevelPinkNoiseWindModel multiLevelModel = new MultiLevelPinkNoiseWindModel();
multiLevelModel.addWindLevel(0, 5.0, Math.PI / 4, 1);
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2, 2);
conditions.setWindModel(multiLevelModel);
@ -107,12 +107,12 @@ public class SimulationConditionsTest {
}
@Test
@DisplayName("Test wind velocity consistency for MultiLevelWindModel")
@DisplayName("Test wind velocity consistency for MultiLevelPinkNoiseWindModel")
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);
MultiLevelPinkNoiseWindModel multiLevelModel = new MultiLevelPinkNoiseWindModel();
multiLevelModel.addWindLevel(0, 5.0, Math.PI / 4, 2);
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2, 1);
conditions.setWindModel(multiLevelModel);
@ -140,12 +140,12 @@ public class SimulationConditionsTest {
}
@Test
@DisplayName("Test altitude dependence of MultiLevelWindModel")
@DisplayName("Test altitude dependence of MultiLevelPinkNoiseWindModel")
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);
MultiLevelPinkNoiseWindModel multiLevelModel = new MultiLevelPinkNoiseWindModel();
multiLevelModel.addWindLevel(0, 5.0, 0, 3);
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2, 4.2);
conditions.setWindModel(multiLevelModel);

View File

@ -70,4 +70,4 @@ The following file format versions exist:
1.10: Introduced with OpenRocket 24.XX.
Added a priority attribute to simulation warnings.
Added document preferences (<docprefs>).
Added wind model settings (<wind mode="{pinknoise or multilevel}">), and windmodeltype to simulation conditions.
Added wind model settings (<wind mode="{average or multilevel}">), and windmodeltype to simulation conditions.

View File

@ -35,6 +35,8 @@ import javax.swing.SortOrder;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.RowSorterEvent;
import javax.swing.event.RowSorterListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
@ -45,7 +47,7 @@ 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.MultiLevelPinkNoiseWindModel;
import info.openrocket.core.models.wind.PinkNoiseWindModel;
import info.openrocket.core.models.wind.WindModelType;
import info.openrocket.core.simulation.DefaultSimulationOptionFactory;
@ -84,7 +86,7 @@ public class SimulationConditionsPanel extends JPanel {
* 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.
* @param addAllWindModels if false, only the average wind model will be added.
*/
public static void addSimulationConditionsPanel(JPanel parent, SimulationOptionsInterface target,
boolean addAllWindModels) {
@ -98,7 +100,7 @@ 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]", ""));
sub = new JPanel(new MigLayout("fill, ins 20 20 0 20", "[grow]", ""));
//// Wind
sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.lbl.Wind")));
parent.add(sub, "growx, split 2, aligny 0, flowy, gapright para");
@ -107,12 +109,12 @@ public class SimulationConditionsPanel extends JPanel {
if (addAllWindModels) {
addWindModelPanel(sub, target);
} else {
addPinkNoiseSettings(sub, target);
addAverageWindSettings(sub, target);
}
//// Temperature and pressure
sub = new JPanel(new MigLayout("fill, gap rel unrel",
"[grow][85lp!][35lp!][75lp!]", ""));
sub = new JPanel(new MigLayout("gap rel unrel",
"[][85lp!][35lp!][75lp!]", ""));
//// Atmospheric conditions
sub.setBorder(BorderFactory.createTitledBorder(trans.get("simedtdlg.border.Atmoscond")));
parent.add(sub, "growx, aligny 0, gapright para");
@ -139,7 +141,7 @@ public class SimulationConditionsPanel extends JPanel {
tip = trans.get("simedtdlg.lbl.ttip.Temperature");
label.setToolTipText(tip);
isa.addEnableComponent(label, false);
sub.add(label);
sub.add(label, "gapright 50lp");
temperatureModel = new DoubleModel(target, "LaunchTemperature", UnitGroup.UNITS_TEMPERATURE, 0);
@ -374,38 +376,43 @@ public class SimulationConditionsPanel extends JPanel {
private static void addWindModelPanel(JPanel panel, SimulationOptionsInterface target) {
ButtonGroup windModelGroup = new ButtonGroup();
// Wind model to use
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"));
//// Average
JRadioButton averageButton = new JRadioButton(trans.get("simedtdlg.radio.AverageWind"));
averageButton.setToolTipText(trans.get("simedtdlg.radio.AverageWind.ttip"));
averageButton.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 20));
//// Multi-level
JRadioButton multiLevelButton = new JRadioButton(trans.get("simedtdlg.radio.MultiLevelWind"));
multiLevelButton.setToolTipText(trans.get("simedtdlg.radio.MultiLevelWind.ttip"));
windModelGroup.add(pinkNoiseButton);
windModelGroup.add(averageButton);
windModelGroup.add(multiLevelButton);
panel.add(pinkNoiseButton);
panel.add(averageButton);
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]", ""));
JPanel averagePanel = new JPanel(new MigLayout("fill, ins 0", "[grow][75lp!][30lp!][75lp!]", ""));
JPanel multiLevelPanel = new JPanel(new MigLayout("fill, ins 0"));
addPinkNoiseSettings(pinkNoisePanel, target);
addAverageWindSettings(averagePanel, target);
addMultiLevelSettings(multiLevelPanel, target);
windSettingsPanel.add(pinkNoisePanel, "PinkNoise");
windSettingsPanel.add(averagePanel, "Average");
windSettingsPanel.add(multiLevelPanel, "MultiLevel");
panel.add(windSettingsPanel, "grow, wrap");
pinkNoiseButton.addActionListener(e -> {
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "PinkNoise");
averageButton.addActionListener(e -> {
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "Average");
if (target instanceof SimulationOptions) {
((SimulationOptions) target).setWindModelType(WindModelType.PINK_NOISE);
((SimulationOptions) target).setWindModelType(WindModelType.AVERAGE);
}
});
@ -419,9 +426,9 @@ public class SimulationConditionsPanel extends JPanel {
// 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");
if (options.getWindModelType() == WindModelType.AVERAGE) {
averageButton.setSelected(true);
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "Average");
} else {
multiLevelButton.setSelected(true);
((CardLayout) windSettingsPanel.getLayout()).show(windSettingsPanel, "MultiLevel");
@ -429,8 +436,8 @@ public class SimulationConditionsPanel extends JPanel {
}
}
private static void addPinkNoiseSettings(JPanel panel, SimulationOptionsInterface target) {
PinkNoiseWindModel model = target.getPinkNoiseWindModel();
private static void addAverageWindSettings(JPanel panel, SimulationOptionsInterface target) {
PinkNoiseWindModel model = target.getAverageWindModel();
// Wind average
final DoubleModel windSpeedAverage = addDoubleModel(panel, "Averwindspeed", trans.get("simedtdlg.lbl.ttip.Averwindspeed"), model, "Average",
@ -458,14 +465,14 @@ public class SimulationConditionsPanel extends JPanel {
"TurbulenceIntensity", UnitGroup.UNITS_RELATIVE, 0, 1.0, true);
final JLabel intensityLabel = new JLabel(
getIntensityDescription(target.getPinkNoiseWindModel().getTurbulenceIntensity()));
getIntensityDescription(target.getAverageWindModel().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()));
getIntensityDescription(target.getAverageWindModel().getTurbulenceIntensity()));
windSpeedDeviation.stateChanged(e);
}
});
@ -485,7 +492,7 @@ public class SimulationConditionsPanel extends JPanel {
if (!(target instanceof SimulationOptions options)) {
return;
}
MultiLevelWindModel model = options.getMultiLevelWindModel();
MultiLevelPinkNoiseWindModel model = options.getMultiLevelWindModel();
// Create the levels table
WindLevelTableModel tableModel = new WindLevelTableModel(model);
@ -493,6 +500,7 @@ public class SimulationConditionsPanel extends JPanel {
windLevelTable.setRowSelectionAllowed(false);
windLevelTable.setColumnSelectionAllowed(false);
windLevelTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
windLevelTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); // Allow horizontal scrolling
// Set up value columns
SelectAllCellEditor selectAllEditor = new SelectAllCellEditor();
@ -517,36 +525,46 @@ public class SimulationConditionsPanel extends JPanel {
TableRowSorter<WindLevelTableModel> sorter = new TableRowSorter<>(tableModel);
windLevelTable.setRowSorter(sorter);
sorter.setSortable(0, true);
sorter.setSortable(1, false);
sorter.setSortable(2, false);
for (int i = 1; i < windLevelTable.getColumnCount(); i++) {
sorter.setSortable(i, false);
}
sorter.addRowSorterListener(new RowSorterListener() {
@Override
public void sorterChanged(RowSorterEvent e) {
model.sortLevels();
}
});
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");
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setPreferredSize(new Dimension(400, 150));
panel.add(scrollPane, "grow, wrap");
//// Buttons
JPanel buttonPanel = new JPanel(new MigLayout("fill, ins 0, gap rel unrel", "[grow]", ""));
JPanel buttonPanel = new JPanel(new MigLayout("ins 0"));
// Add wind level
// Add level
JButton addButton = new JButton(trans.get("simedtdlg.but.addWindLevel"));
addButton.addActionListener(e -> {
tableModel.addWindLevel();
sorter.sort();
});
buttonPanel.add(addButton, "growx, wrap");
buttonPanel.add(addButton);
// Remove wind level
// Remove 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");
buttonPanel.add(removeButton, "gapright unrel");
// Visualization button
// Visualization levels
JButton visualizeButton = new JButton(trans.get("simedtdlg.but.visualizeWindLevels"));
visualizeButton.addActionListener(e -> {
Window owner = SwingUtilities.getWindowAncestor(panel);
@ -561,7 +579,7 @@ public class SimulationConditionsPanel extends JPanel {
visualizationDialog.setVisible(true);
}
});
buttonPanel.add(visualizeButton, "growx, wrap");
buttonPanel.add(visualizeButton);
panel.add(buttonPanel, "grow, wrap");
@ -618,6 +636,7 @@ public class SimulationConditionsPanel extends JPanel {
for (int column = 0; column < table.getColumnCount(); column++) {
TableColumn tableColumn = columnModel.getColumn(column);
int preferredWidth = getPreferredColumnWidth(table, column);
preferredWidth = column == 0 ? preferredWidth + 20 : preferredWidth; // Add extra padding to first column (for sorting arrow)
tableColumn.setPreferredWidth(preferredWidth);
}
}
@ -653,14 +672,14 @@ public class SimulationConditionsPanel extends JPanel {
SimulationOptions defaults = f.getDefault();
options.copyConditionsFrom(defaults);
});
this.add(restoreDefaults, "span, split 3, skip, gapbottom para, gapright para, right");
this.add(restoreDefaults, "span, split 3, skip, 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");
this.add(saveDefaults, "gapright para, right");
}
private static String getIntensityDescription(double i) {
@ -733,7 +752,7 @@ public class SimulationConditionsPanel extends JPanel {
}
private static class WindLevelTableModel extends AbstractTableModel {
private final MultiLevelWindModel model;
private final MultiLevelPinkNoiseWindModel model;
private static final String[] columnNames = {
trans.get("simedtdlg.col.Altitude"),
trans.get("simedtdlg.col.Unit"),
@ -741,17 +760,25 @@ public class SimulationConditionsPanel extends JPanel {
trans.get("simedtdlg.col.Unit"),
trans.get("simedtdlg.col.Direction"),
trans.get("simedtdlg.col.Unit"),
trans.get("simedtdlg.col.StandardDeviation"),
trans.get("simedtdlg.col.Unit"),
trans.get("simedtdlg.col.Turbulence"),
trans.get("simedtdlg.col.Unit"),
trans.get("simedtdlg.col.Intensity"),
};
private static final UnitGroup[] unitGroups = {
UnitGroup.UNITS_DISTANCE, UnitGroup.UNITS_VELOCITY, UnitGroup.UNITS_ANGLE};
UnitGroup.UNITS_DISTANCE, UnitGroup.UNITS_VELOCITY, UnitGroup.UNITS_ANGLE,
UnitGroup.UNITS_VELOCITY, UnitGroup.UNITS_RELATIVE};
private final Unit[] currentUnits = {
UnitGroup.UNITS_DISTANCE.getDefaultUnit(),
UnitGroup.UNITS_VELOCITY.getDefaultUnit(),
UnitGroup.UNITS_ANGLE.getDefaultUnit()
UnitGroup.UNITS_ANGLE.getDefaultUnit(),
UnitGroup.UNITS_VELOCITY.getDefaultUnit(),
UnitGroup.UNITS_RELATIVE.getDefaultUnit(),
};
private WindLevelVisualizationDialog visualizationDialog;
public WindLevelTableModel(MultiLevelWindModel model) {
public WindLevelTableModel(MultiLevelPinkNoiseWindModel model) {
this.model = model;
}
@ -780,21 +807,31 @@ public class SimulationConditionsPanel extends JPanel {
@Override
public Class<?> getColumnClass(int columnIndex) {
// Intensity column
if (columnIndex == getColumnCount()-1) {
return String.class;
}
return (columnIndex % 2 == 0) ? Double.class : Unit.class;
}
public Object getSIValueAt(int rowIndex, int columnIndex) {
MultiLevelWindModel.WindLevel level = model.getLevels().get(rowIndex);
MultiLevelPinkNoiseWindModel.LevelWindModel level = model.getLevels().get(rowIndex);
return switch (columnIndex) {
case 0 -> level.altitude;
case 2 -> level.speed;
case 4 -> level.direction;
case 0 -> level.getAltitude();
case 2 -> level.getSpeed();
case 4 -> level.getDirection();
case 6 -> level.getStandardDeviation();
case 8 -> level.getTurblenceIntensity();
default -> null;
};
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
// Intensity column
if (columnIndex == getColumnCount()-1) {
return getIntensityDescription(model.getLevels().get(rowIndex).getTurblenceIntensity());
}
if (columnIndex % 2 == 0) {
Object rawValue = getSIValueAt(rowIndex, columnIndex);
if (rawValue == null) {
@ -812,26 +849,32 @@ public class SimulationConditionsPanel extends JPanel {
@Override
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
MultiLevelWindModel.WindLevel level = model.getLevels().get(rowIndex);
MultiLevelPinkNoiseWindModel.LevelWindModel 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);
level.setAltitude(currentUnits[0].fromUnit(value));
break;
case 2:
// Handle negative speed
if (value < 0) {
level.speed = currentUnits[1].fromUnit(Math.abs(value));
level.setSpeed(currentUnits[1].fromUnit(Math.abs(value)));
// Adjust direction by 180 degrees
level.direction = (level.direction + Math.PI) % (2 * Math.PI);
level.setDirection((level.getDirection() + Math.PI) % (2 * Math.PI));
} else {
level.speed = currentUnits[1].fromUnit(value);
level.setSpeed(currentUnits[1].fromUnit(value));
}
break;
case 4:
level.direction = currentUnits[2].fromUnit(value);
level.setDirection(currentUnits[2].fromUnit(value));
break;
case 6:
level.setStandardDeviation(currentUnits[3].fromUnit(value));
break;
case 8:
level.setTurbulenceIntensity(currentUnits[4].fromUnit(value));
break;
}
} else {
@ -847,16 +890,17 @@ public class SimulationConditionsPanel extends JPanel {
@Override
public boolean isCellEditable(int rowIndex, int columnIndex) {
return true;
return columnIndex != columnNames.length - 1; // Intensity column is not editable
}
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;
List<MultiLevelPinkNoiseWindModel.LevelWindModel> levels = model.getLevels();
double newAltitude = levels.isEmpty() ? 0 : levels.get(levels.size() - 1).getAltitude() + 100;
double newSpeed = levels.isEmpty() ? 5 : levels.get(levels.size() - 1).getSpeed();
double newDirection = levels.isEmpty() ? Math.PI / 2 : levels.get(levels.size() - 1).getDirection();
double newDeviation = levels.isEmpty() ? 0.2 : levels.get(levels.size() - 1).getStandardDeviation();
model.addWindLevel(newAltitude, newSpeed, newDirection);
model.addWindLevel(newAltitude, newSpeed, newDirection, newDeviation);
fireTableDataChanged();
}

View File

@ -1,7 +1,7 @@
package info.openrocket.swing.gui.simulation;
import info.openrocket.core.l10n.Translator;
import info.openrocket.core.models.wind.MultiLevelWindModel;
import info.openrocket.core.models.wind.MultiLevelPinkNoiseWindModel;
import info.openrocket.core.startup.Application;
import info.openrocket.core.unit.Unit;
@ -30,7 +30,7 @@ public class WindLevelVisualizationDialog extends JDialog {
private final WindLevelVisualization visualization;
private final JCheckBox showDirectionsCheckBox;
public WindLevelVisualizationDialog(Dialog owner, MultiLevelWindModel model, Unit altitudeUnit, Unit speedUnit) {
public WindLevelVisualizationDialog(Dialog owner, MultiLevelPinkNoiseWindModel model, Unit altitudeUnit, Unit speedUnit) {
super(owner, trans.get("WindLevelVisualizationDialog.title.WindLevelVisualization"), false);
visualization = new WindLevelVisualization(model, altitudeUnit, speedUnit);
@ -74,7 +74,7 @@ public class WindLevelVisualizationDialog extends JDialog {
}
private static class WindLevelVisualization extends JPanel {
private final MultiLevelWindModel model;
private final MultiLevelPinkNoiseWindModel model;
private static final int MARGIN = 50;
private static final int ARROW_SIZE = 10;
private static final int TICK_LENGTH = 5;
@ -83,7 +83,7 @@ public class WindLevelVisualizationDialog extends JDialog {
private Unit speedUnit;
private boolean showDirections = true;
public WindLevelVisualization(MultiLevelWindModel model, Unit altitudeUnit, Unit speedUnit) {
public WindLevelVisualization(MultiLevelPinkNoiseWindModel model, Unit altitudeUnit, Unit speedUnit) {
this.model = model;
this.altitudeUnit = altitudeUnit;
this.speedUnit = speedUnit;
@ -113,14 +113,14 @@ public class WindLevelVisualizationDialog extends JDialog {
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, width, height);
List<MultiLevelWindModel.WindLevel> levels = model.getLevels();
List<MultiLevelPinkNoiseWindModel.LevelWindModel> levels = model.getLevels();
if (levels.isEmpty()) return;
// Sort levels before drawing
levels.sort(Comparator.comparingDouble(level -> level.altitude));
levels.sort(Comparator.comparingDouble(MultiLevelPinkNoiseWindModel.LevelWindModel::getAltitude));
double maxAltitude = levels.stream().mapToDouble(l -> l.altitude).max().orElse(1000);
double maxSpeed = levels.stream().mapToDouble(l -> l.speed).max().orElse(10);
double maxAltitude = levels.stream().mapToDouble(MultiLevelPinkNoiseWindModel.LevelWindModel::getAltitude).max().orElse(1000);
double maxSpeed = levels.stream().mapToDouble(MultiLevelPinkNoiseWindModel.LevelWindModel::getSpeed).max().orElse(10);
// Extend axis ranges by 10% for drawing
double extendedMaxAltitude = maxAltitude * 1.1;
@ -131,10 +131,10 @@ public class WindLevelVisualizationDialog extends JDialog {
// Draw wind levels
for (int i = 0; i < levels.size(); i++) {
MultiLevelWindModel.WindLevel level = levels.get(i);
MultiLevelPinkNoiseWindModel.LevelWindModel 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));
int x = MARGIN + (int) (level.getSpeed() / extendedMaxSpeed * (width - 2 * MARGIN));
int y = height - MARGIN - (int) (level.getAltitude() / extendedMaxAltitude * (height - 2 * MARGIN));
// Draw point
g2d.setColor(Color.BLUE);
@ -142,14 +142,14 @@ public class WindLevelVisualizationDialog extends JDialog {
// Draw wind direction arrow
if (showDirections) {
drawWindArrow(g2d, x, y, level.direction);
drawWindArrow(g2d, x, y, level.getDirection());
}
// 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));
MultiLevelPinkNoiseWindModel.LevelWindModel prevLevel = levels.get(i - 1);
int prevX = MARGIN + (int) (prevLevel.getSpeed() / extendedMaxSpeed * (width - 2 * MARGIN));
int prevY = height - MARGIN - (int) (prevLevel.getAltitude() / extendedMaxAltitude * (height - 2 * MARGIN));
g2d.setColor(Color.GRAY);
g2d.drawLine(prevX, prevY, x, y);
}