Also use pink noise for multi-level wind
This commit is contained in:
parent
19f8e3c8f3
commit
ea210bac8d
@ -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>");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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());
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -21,7 +21,7 @@ public interface SimulationOptionsInterface extends ChangeSource {
|
||||
|
||||
void setLaunchRodDirection(double launchRodDirection);
|
||||
|
||||
PinkNoiseWindModel getPinkNoiseWindModel();
|
||||
PinkNoiseWindModel getAverageWindModel();
|
||||
|
||||
double getLaunchAltitude();
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user