From ea210bac8d4882561a9f89341a1c7e55bd504d16 Mon Sep 17 00:00:00 2001 From: SiboVG Date: Sat, 21 Sep 2024 05:12:33 +0100 Subject: [PATCH] Also use pink noise for multi-level wind --- .../core/file/openrocket/OpenRocketSaver.java | 22 +- .../importt/SimulationConditionsHandler.java | 6 +- .../file/openrocket/importt/WindHandler.java | 31 ++- .../file/rasaero/export/LaunchSiteDTO.java | 4 +- .../rasaero/importt/LaunchSiteHandler.java | 2 +- .../wind/MultiLevelPinkNoiseWindModel.java | 244 ++++++++++++++++++ .../core/models/wind/MultiLevelWindModel.java | 193 -------------- .../core/models/wind/PinkNoiseWindModel.java | 5 +- .../core/models/wind/WindModelType.java | 2 +- .../preferences/ApplicationPreferences.java | 26 +- .../DefaultSimulationOptionFactory.java | 12 +- .../core/simulation/SimulationOptions.java | 48 ++-- .../SimulationOptionsInterface.java | 2 +- .../main/resources/l10n/messages.properties | 7 +- .../models/wind/MultiLevelWindModelTest.java | 126 ++++++--- .../core/simulation/FlightEventsTest.java | 2 +- .../simulation/SimulationConditionsTest.java | 36 +-- fileformat.txt | 2 +- .../simulation/SimulationConditionsPanel.java | 162 +++++++----- .../WindLevelVisualizationDialog.java | 30 +-- 20 files changed, 556 insertions(+), 406 deletions(-) create mode 100644 core/src/main/java/info/openrocket/core/models/wind/MultiLevelPinkNoiseWindModel.java delete mode 100644 core/src/main/java/info/openrocket/core/models/wind/MultiLevelWindModel.java diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java b/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java index 18f59a833..d8f8d2285 100644 --- a/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java +++ b/core/src/main/java/info/openrocket/core/file/openrocket/OpenRocketSaver.java @@ -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(""); + writeln(""); 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(""); if (!cond.getMultiLevelWindModel().getLevels().isEmpty()) { writeln(""); indent++; - for (MultiLevelWindModel.WindLevel level : cond.getMultiLevelWindModel().getLevels()) { - writeln(""); + for (MultiLevelPinkNoiseWindModel.LevelWindModel level : cond.getMultiLevelWindModel().getLevels()) { + writeln(""); } indent--; writeln(""); diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/importt/SimulationConditionsHandler.java b/core/src/main/java/info/openrocket/core/file/openrocket/importt/SimulationConditionsHandler.java index 136faa15a..cd140e5c2 100644 --- a/core/src/main/java/info/openrocket/core/file/openrocket/importt/SimulationConditionsHandler.java +++ b/core/src/main/java/info/openrocket/core/file/openrocket/importt/SimulationConditionsHandler.java @@ -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); } } diff --git a/core/src/main/java/info/openrocket/core/file/openrocket/importt/WindHandler.java b/core/src/main/java/info/openrocket/core/file/openrocket/importt/WindHandler.java index 9c36b48ec..2afa162ae 100644 --- a/core/src/main/java/info/openrocket/core/file/openrocket/importt/WindHandler.java +++ b/core/src/main/java/info/openrocket/core/file/openrocket/importt/WindHandler.java @@ -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 { diff --git a/core/src/main/java/info/openrocket/core/file/rasaero/export/LaunchSiteDTO.java b/core/src/main/java/info/openrocket/core/file/rasaero/export/LaunchSiteDTO.java index ef7995317..92f0e8518 100644 --- a/core/src/main/java/info/openrocket/core/file/rasaero/export/LaunchSiteDTO.java +++ b/core/src/main/java/info/openrocket/core/file/rasaero/export/LaunchSiteDTO.java @@ -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() { diff --git a/core/src/main/java/info/openrocket/core/file/rasaero/importt/LaunchSiteHandler.java b/core/src/main/java/info/openrocket/core/file/rasaero/importt/LaunchSiteHandler.java index 551c04366..07bf1a167 100644 --- a/core/src/main/java/info/openrocket/core/file/rasaero/importt/LaunchSiteHandler.java +++ b/core/src/main/java/info/openrocket/core/file/rasaero/importt/LaunchSiteHandler.java @@ -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) { diff --git a/core/src/main/java/info/openrocket/core/models/wind/MultiLevelPinkNoiseWindModel.java b/core/src/main/java/info/openrocket/core/models/wind/MultiLevelPinkNoiseWindModel.java new file mode 100644 index 000000000..646a664a6 --- /dev/null +++ b/core/src/main/java/info/openrocket/core/models/wind/MultiLevelPinkNoiseWindModel.java @@ -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 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 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); + } + } +} \ No newline at end of file diff --git a/core/src/main/java/info/openrocket/core/models/wind/MultiLevelWindModel.java b/core/src/main/java/info/openrocket/core/models/wind/MultiLevelWindModel.java deleted file mode 100644 index 4523574f7..000000000 --- a/core/src/main/java/info/openrocket/core/models/wind/MultiLevelWindModel.java +++ /dev/null @@ -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 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 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; - } - } -} \ No newline at end of file diff --git a/core/src/main/java/info/openrocket/core/models/wind/PinkNoiseWindModel.java b/core/src/main/java/info/openrocket/core/models/wind/PinkNoiseWindModel.java index 6d8ecffcf..a2415cb07 100644 --- a/core/src/main/java/info/openrocket/core/models/wind/PinkNoiseWindModel.java +++ b/core/src/main/java/info/openrocket/core/models/wind/PinkNoiseWindModel.java @@ -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 diff --git a/core/src/main/java/info/openrocket/core/models/wind/WindModelType.java b/core/src/main/java/info/openrocket/core/models/wind/WindModelType.java index c8004ae5b..0e156b403 100644 --- a/core/src/main/java/info/openrocket/core/models/wind/WindModelType.java +++ b/core/src/main/java/info/openrocket/core/models/wind/WindModelType.java @@ -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; diff --git a/core/src/main/java/info/openrocket/core/preferences/ApplicationPreferences.java b/core/src/main/java/info/openrocket/core/preferences/ApplicationPreferences.java index cb5da8159..d8b7a62cf 100644 --- a/core/src/main/java/info/openrocket/core/preferences/ApplicationPreferences.java +++ b/core/src/main/java/info/openrocket/core/preferences/ApplicationPreferences.java @@ -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(); } } diff --git a/core/src/main/java/info/openrocket/core/simulation/DefaultSimulationOptionFactory.java b/core/src/main/java/info/openrocket/core/simulation/DefaultSimulationOptionFactory.java index 2fc981ba1..5b6ebf58b 100644 --- a/core/src/main/java/info/openrocket/core/simulation/DefaultSimulationOptionFactory.java +++ b/core/src/main/java/info/openrocket/core/simulation/DefaultSimulationOptionFactory.java @@ -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()); diff --git a/core/src/main/java/info/openrocket/core/simulation/SimulationOptions.java b/core/src/main/java/info/openrocket/core/simulation/SimulationOptions.java index 7aa443cfc..730bec966 100644 --- a/core/src/main/java/info/openrocket/core/simulation/SimulationOptions.java +++ b/core/src/main/java/info/openrocket/core/simulation/SimulationOptions.java @@ -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 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)) diff --git a/core/src/main/java/info/openrocket/core/simulation/SimulationOptionsInterface.java b/core/src/main/java/info/openrocket/core/simulation/SimulationOptionsInterface.java index 9843098b5..464ac8f85 100644 --- a/core/src/main/java/info/openrocket/core/simulation/SimulationOptionsInterface.java +++ b/core/src/main/java/info/openrocket/core/simulation/SimulationOptionsInterface.java @@ -21,7 +21,7 @@ public interface SimulationOptionsInterface extends ChangeSource { void setLaunchRodDirection(double launchRodDirection); - PinkNoiseWindModel getPinkNoiseWindModel(); + PinkNoiseWindModel getAverageWindModel(); double getLaunchAltitude(); diff --git a/core/src/main/resources/l10n/messages.properties b/core/src/main/resources/l10n/messages.properties index 0ae8101a1..867e8c821 100644 --- a/core/src/main/resources/l10n/messages.properties +++ b/core/src/main/resources/l10n/messages.properties @@ -506,6 +506,9 @@ simedtdlg.lbl.ttip.Latitude = 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 diff --git a/core/src/test/java/info/openrocket/core/models/wind/MultiLevelWindModelTest.java b/core/src/test/java/info/openrocket/core/models/wind/MultiLevelWindModelTest.java index af4e95b76..0b462cd53 100644 --- a/core/src/test/java/info/openrocket/core/models/wind/MultiLevelWindModelTest.java +++ b/core/src/test/java/info/openrocket/core/models/wind/MultiLevelWindModelTest.java @@ -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 levels = model.getLevels(); + List 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); } } \ No newline at end of file diff --git a/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java b/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java index 45af0f29f..fa6a893f3 100644 --- a/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java +++ b/core/src/test/java/info/openrocket/core/simulation/FlightEventsTest.java @@ -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); diff --git a/core/src/test/java/info/openrocket/core/simulation/SimulationConditionsTest.java b/core/src/test/java/info/openrocket/core/simulation/SimulationConditionsTest.java index 932dd38e3..720eb6e53 100644 --- a/core/src/test/java/info/openrocket/core/simulation/SimulationConditionsTest.java +++ b/core/src/test/java/info/openrocket/core/simulation/SimulationConditionsTest.java @@ -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); diff --git a/fileformat.txt b/fileformat.txt index dd973cf4a..144751d0b 100644 --- a/fileformat.txt +++ b/fileformat.txt @@ -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 (). - Added wind model settings (), and windmodeltype to simulation conditions. + Added wind model settings (), and windmodeltype to simulation conditions. diff --git a/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationConditionsPanel.java b/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationConditionsPanel.java index bfdbc6351..ab6bdb59a 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationConditionsPanel.java +++ b/swing/src/main/java/info/openrocket/swing/gui/simulation/SimulationConditionsPanel.java @@ -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 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 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 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(); } diff --git a/swing/src/main/java/info/openrocket/swing/gui/simulation/WindLevelVisualizationDialog.java b/swing/src/main/java/info/openrocket/swing/gui/simulation/WindLevelVisualizationDialog.java index ca5f6c1a9..055f73e8f 100644 --- a/swing/src/main/java/info/openrocket/swing/gui/simulation/WindLevelVisualizationDialog.java +++ b/swing/src/main/java/info/openrocket/swing/gui/simulation/WindLevelVisualizationDialog.java @@ -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 levels = model.getLevels(); + List 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); }