Merge pull request #2559 from SiboVG/issue-2060
[#2060, #2558] Implement multi-level wind input
This commit is contained in:
commit
7a4833f99c
@ -16,6 +16,7 @@ import info.openrocket.core.logging.ErrorSet;
|
||||
import info.openrocket.core.logging.SimulationAbort;
|
||||
import info.openrocket.core.logging.WarningSet;
|
||||
import info.openrocket.core.material.Material;
|
||||
import info.openrocket.core.models.wind.MultiLevelPinkNoiseWindModel;
|
||||
import info.openrocket.core.preferences.DocumentPreferences;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -336,8 +337,34 @@ public class OpenRocketSaver extends RocketSaver {
|
||||
writeElement("launchrodlength", cond.getLaunchRodLength());
|
||||
writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0 / Math.PI);
|
||||
writeElement("launchroddirection", cond.getLaunchRodDirection() * 360.0 / (2.0 * Math.PI));
|
||||
writeElement("windaverage", cond.getWindSpeedAverage());
|
||||
writeElement("windturbulence", cond.getWindTurbulenceIntensity());
|
||||
|
||||
// TODO: remove once support for OR 23.09 and prior is dropped
|
||||
writeElement("windaverage", cond.getAverageWindModel().getAverage());
|
||||
writeElement("windturbulence", cond.getAverageWindModel().getTurbulenceIntensity());
|
||||
writeElement("winddirection", cond.getAverageWindModel().getDirection());
|
||||
|
||||
writeln("<wind model=\"average\">");
|
||||
indent++;
|
||||
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 (MultiLevelPinkNoiseWindModel.LevelWindModel level : cond.getMultiLevelWindModel().getLevels()) {
|
||||
writeln("<windlevel altitude=\"" + level.getAltitude() + "\" speed=\"" + level.getSpeed() +
|
||||
"\" direction=\"" + level.getDirection() + "\" standarddeviation=\"" + level.getStandardDeviation() +
|
||||
"\"/>");
|
||||
}
|
||||
indent--;
|
||||
writeln("</wind>");
|
||||
}
|
||||
|
||||
writeElement("windmodeltype", cond.getWindModelType().toStringValue());
|
||||
|
||||
writeElement("launchaltitude", cond.getLaunchAltitude());
|
||||
writeElement("launchlatitude", cond.getLaunchLatitude());
|
||||
writeElement("launchlongitude", cond.getLaunchLongitude());
|
||||
|
@ -7,6 +7,7 @@ import info.openrocket.core.file.DocumentLoadingContext;
|
||||
import info.openrocket.core.file.simplesax.AbstractElementHandler;
|
||||
import info.openrocket.core.file.simplesax.ElementHandler;
|
||||
import info.openrocket.core.file.simplesax.PlainTextHandler;
|
||||
import info.openrocket.core.models.wind.WindModelType;
|
||||
import info.openrocket.core.rocketcomponent.FlightConfigurationId;
|
||||
import info.openrocket.core.rocketcomponent.Rocket;
|
||||
import info.openrocket.core.simulation.SimulationOptions;
|
||||
@ -17,6 +18,7 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
public FlightConfigurationId idToSet = FlightConfigurationId.ERROR_FCID;
|
||||
private final SimulationOptions options;
|
||||
private AtmosphereHandler atmosphereHandler;
|
||||
private WindHandler windHandler;
|
||||
|
||||
public SimulationConditionsHandler(Rocket rocket, DocumentLoadingContext context) {
|
||||
this.context = context;
|
||||
@ -32,7 +34,10 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
@Override
|
||||
public ElementHandler openElement(String element, HashMap<String, String> attributes,
|
||||
WarningSet warnings) {
|
||||
if (element.equals("atmosphere")) {
|
||||
if (element.equals("wind")) {
|
||||
windHandler = new WindHandler(attributes.get("model"), options);
|
||||
return windHandler;
|
||||
} else if (element.equals("atmosphere")) {
|
||||
atmosphereHandler = new AtmosphereHandler(attributes.get("model"), context);
|
||||
return atmosphereHandler;
|
||||
}
|
||||
@ -49,57 +54,80 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
|
||||
if (element.equals("configid")) {
|
||||
this.idToSet = new FlightConfigurationId(content);
|
||||
} else if (element.equals("launchrodlength")) {
|
||||
switch (element) {
|
||||
case "configid" -> this.idToSet = new FlightConfigurationId(content);
|
||||
case "launchrodlength" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch rod length defined, ignoring.");
|
||||
} else {
|
||||
options.setLaunchRodLength(d);
|
||||
}
|
||||
} else if (element.equals("launchrodangle")) {
|
||||
}
|
||||
case "launchrodangle" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch rod angle defined, ignoring.");
|
||||
} else {
|
||||
options.setLaunchRodAngle(d * Math.PI / 180);
|
||||
}
|
||||
} else if (element.equals("launchroddirection")) {
|
||||
}
|
||||
case "launchroddirection" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch rod direction defined, ignoring.");
|
||||
} else {
|
||||
options.setLaunchRodDirection(d * 2.0 * Math.PI / 360);
|
||||
}
|
||||
} else if (element.equals("windaverage")) {
|
||||
}
|
||||
|
||||
// TODO: remove once support for OR 23.09 and prior is dropped
|
||||
case "windaverage" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal average windspeed defined, ignoring.");
|
||||
} else {
|
||||
options.setWindSpeedAverage(d);
|
||||
options.getAverageWindModel().setAverage(d);
|
||||
}
|
||||
} else if (element.equals("windturbulence")) {
|
||||
}
|
||||
case "windturbulence" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal wind turbulence intensity defined, ignoring.");
|
||||
} else {
|
||||
options.setWindTurbulenceIntensity(d);
|
||||
options.getAverageWindModel().setTurbulenceIntensity(d);
|
||||
}
|
||||
} else if (element.equals("launchaltitude")) {
|
||||
}
|
||||
case "winddirection" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal wind direction defined, ignoring.");
|
||||
} else {
|
||||
options.getAverageWindModel().setDirection(d);
|
||||
}
|
||||
}
|
||||
|
||||
case "wind" -> windHandler.storeSettings(options, warnings);
|
||||
case "windmodeltype" -> {
|
||||
options.setWindModelType(WindModelType.fromString(content));
|
||||
}
|
||||
|
||||
case "launchaltitude" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch altitude defined, ignoring.");
|
||||
} else {
|
||||
options.setLaunchAltitude(d);
|
||||
}
|
||||
} else if (element.equals("launchlatitude")) {
|
||||
}
|
||||
case "launchlatitude" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch latitude defined, ignoring.");
|
||||
} else {
|
||||
options.setLaunchLatitude(d);
|
||||
}
|
||||
} else if (element.equals("launchlongitude")) {
|
||||
}
|
||||
case "launchlongitude" -> {
|
||||
if (Double.isNaN(d)) {
|
||||
warnings.add("Illegal launch longitude.");
|
||||
} else {
|
||||
options.setLaunchLongitude(d);
|
||||
}
|
||||
} else if (element.equals("geodeticmethod")) {
|
||||
}
|
||||
case "geodeticmethod" -> {
|
||||
GeodeticComputationStrategy gcs = (GeodeticComputationStrategy) DocumentConfig.findEnum(content,
|
||||
GeodeticComputationStrategy.class);
|
||||
if (gcs != null) {
|
||||
@ -107,9 +135,9 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
} else {
|
||||
warnings.add("Unknown geodetic computation method '" + content + "'");
|
||||
}
|
||||
} else if (element.equals("atmosphere")) {
|
||||
atmosphereHandler.storeSettings(options, warnings);
|
||||
} else if (element.equals("timestep")) {
|
||||
}
|
||||
case "atmosphere" -> atmosphereHandler.storeSettings(options, warnings);
|
||||
case "timestep" -> {
|
||||
if (Double.isNaN(d) || d <= 0) {
|
||||
warnings.add("Illegal time step defined, ignoring.");
|
||||
} else {
|
||||
@ -117,4 +145,5 @@ class SimulationConditionsHandler extends AbstractElementHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package info.openrocket.core.file.openrocket.importt;
|
||||
|
||||
import info.openrocket.core.file.simplesax.AbstractElementHandler;
|
||||
import info.openrocket.core.file.simplesax.ElementHandler;
|
||||
import info.openrocket.core.file.simplesax.PlainTextHandler;
|
||||
import info.openrocket.core.logging.WarningSet;
|
||||
import info.openrocket.core.models.wind.WindModelType;
|
||||
import info.openrocket.core.simulation.SimulationOptions;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class WindHandler extends AbstractElementHandler {
|
||||
private final String model;
|
||||
private final SimulationOptions options;
|
||||
|
||||
public WindHandler(String model, SimulationOptions options) {
|
||||
this.model = model;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementHandler openElement(String element, HashMap<String, String> attributes,
|
||||
WarningSet warnings) {
|
||||
return PlainTextHandler.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeElement(String element, HashMap<String, String> attributes,
|
||||
String content, WarningSet warnings) {
|
||||
double d = Double.NaN;
|
||||
try {
|
||||
d = Double.parseDouble(content);
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
|
||||
if ("average".equals(model)) {
|
||||
switch (element) {
|
||||
case "speed" -> {
|
||||
if (!Double.isNaN(d)) {
|
||||
options.getAverageWindModel().setAverage(d);
|
||||
}
|
||||
}
|
||||
case "direction" -> {
|
||||
if (!Double.isNaN(d)) {
|
||||
options.getAverageWindModel().setDirection(d);
|
||||
}
|
||||
}
|
||||
case "standarddeviation" -> {
|
||||
if (!Double.isNaN(d)) {
|
||||
options.getAverageWindModel().setStandardDeviation(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if ("multilevel".equals(model)) {
|
||||
if (element.equals("windlevel")) {
|
||||
double altitude = Double.parseDouble(attributes.get("altitude"));
|
||||
double speed = Double.parseDouble(attributes.get("speed"));
|
||||
double direction = Double.parseDouble(attributes.get("direction"));
|
||||
double standardDeviation = Double.parseDouble(attributes.get("standarddeviation"));
|
||||
options.getMultiLevelWindModel().addWindLevel(altitude, speed, direction, standardDeviation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void storeSettings(SimulationOptions options, WarningSet warnings) {
|
||||
if ("average".equals(model)) {
|
||||
options.setWindModelType(WindModelType.AVERAGE);
|
||||
} else if ("multilevel".equals(model)) {
|
||||
options.setWindModelType(WindModelType.MULTI_LEVEL);
|
||||
} else {
|
||||
warnings.add("Unknown wind model type '" + model + "', using default.");
|
||||
}
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ public class LaunchSiteDTO {
|
||||
setTemperature(RASAeroCommonConstants.OPENROCKET_TO_RASAERO_TEMPERATURE(options.getLaunchTemperature()));
|
||||
setRodAngle(options.getLaunchRodAngle() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_ANGLE);
|
||||
setRodLength(options.getLaunchRodLength() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_ALTITUDE); // It's a length, but stored in RASAero in feet instead of inches
|
||||
setWindSpeed(options.getWindSpeedAverage() * RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
setWindSpeed(options.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.getWindSpeedAverage() * 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.setWindSpeedAverage(
|
||||
launchSiteSettings.getAverageWindModel().setAverage(
|
||||
Double.parseDouble(content) / RASAeroCommonConstants.OPENROCKET_TO_RASAERO_SPEED);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
|
@ -0,0 +1,273 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EventListener;
|
||||
import java.util.EventObject;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
|
||||
import info.openrocket.core.util.ChangeSource;
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.ModID;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
public class MultiLevelPinkNoiseWindModel implements WindModel {
|
||||
private List<LevelWindModel> levels;
|
||||
|
||||
private final List<StateChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
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);
|
||||
newLevel.addChangeListener(e -> fireChangeEvent());
|
||||
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);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void removeWindLevel(double altitude) {
|
||||
levels.removeIf(level -> level.altitude == altitude);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void removeWindLevelIdx(int index) {
|
||||
levels.remove(index);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void clearLevels() {
|
||||
levels.clear();
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public double getWindDirection(double time, double altitude) {
|
||||
Coordinate velocity = getWindVelocity(time, altitude);
|
||||
double direction = Math.atan2(velocity.x, velocity.y);
|
||||
|
||||
// Normalize the result to be between 0 and 2*PI
|
||||
return (direction + 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;
|
||||
|
||||
// Compare the levels list
|
||||
if (levels.size() != that.levels.size()) return false;
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
if (!levels.get(i).equals(that.levels.get(i))) return false;
|
||||
}
|
||||
|
||||
// If we implement any additional fields in the future, we should compare them here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(levels);
|
||||
}
|
||||
|
||||
public static class LevelWindModel implements Cloneable, ChangeSource {
|
||||
protected double altitude;
|
||||
protected PinkNoiseWindModel model;
|
||||
|
||||
private final List<StateChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
LevelWindModel(double altitude, PinkNoiseWindModel model) {
|
||||
this.altitude = altitude;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public double getAltitude() {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
public void setAltitude(double altitude) {
|
||||
this.altitude = altitude;
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
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 o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
LevelWindModel that = (LevelWindModel) o;
|
||||
return Double.compare(that.altitude, altitude) == 0 &&
|
||||
model.equals(that.model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(altitude, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChangeListener(StateChangeListener listener) {
|
||||
listeners.add(listener);
|
||||
model.addChangeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeChangeListener(StateChangeListener listener) {
|
||||
listeners.remove(listener);
|
||||
model.removeChangeListener(listener);
|
||||
}
|
||||
|
||||
public void fireChangeEvent() {
|
||||
EventObject event = new EventObject(this);
|
||||
// Copy the list before iterating to prevent concurrent modification exceptions.
|
||||
EventListener[] list = listeners.toArray(new EventListener[0]);
|
||||
for (EventListener l : list) {
|
||||
if (l instanceof StateChangeListener) {
|
||||
((StateChangeListener) l).stateChanged(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChangeListener(StateChangeListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeChangeListener(StateChangeListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void fireChangeEvent() {
|
||||
EventObject event = new EventObject(this);
|
||||
// Copy the list before iterating to prevent concurrent modification exceptions.
|
||||
EventListener[] list = listeners.toArray(new EventListener[0]);
|
||||
for (EventListener l : list) {
|
||||
if (l instanceof StateChangeListener) {
|
||||
((StateChangeListener) l).stateChanged(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,11 +1,16 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EventListener;
|
||||
import java.util.EventObject;
|
||||
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
|
||||
@ -34,7 +39,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
|
||||
@ -46,6 +51,8 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
private double time1;
|
||||
private double value1, value2;
|
||||
|
||||
private final List<StateChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Construct a new wind simulation with a specific seed value.
|
||||
*
|
||||
@ -55,6 +62,10 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
this.seed = seed ^ SEED_RANDOMIZATION;
|
||||
}
|
||||
|
||||
public PinkNoiseWindModel() {
|
||||
this(new Random().nextInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the average wind speed.
|
||||
*
|
||||
@ -71,13 +82,22 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
* @param average the average wind speed to set
|
||||
*/
|
||||
public void setAverage(double average) {
|
||||
average = Math.max(average, 0);
|
||||
if (average == this.average) {
|
||||
return;
|
||||
}
|
||||
double intensity = getTurbulenceIntensity();
|
||||
this.average = Math.max(average, 0);
|
||||
this.average = average;
|
||||
setTurbulenceIntensity(intensity);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void setDirection(double direction) {
|
||||
if (direction == this.direction) {
|
||||
return;
|
||||
}
|
||||
this.direction = direction;
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getDirection() {
|
||||
@ -99,7 +119,12 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
* @param standardDeviation the standardDeviation to set
|
||||
*/
|
||||
public void setStandardDeviation(double standardDeviation) {
|
||||
if (standardDeviation == this.standardDeviation) {
|
||||
return;
|
||||
}
|
||||
this.standardDeviation = Math.max(standardDeviation, 0);
|
||||
setTurbulenceIntensity(standardDeviation / average);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,9 +186,67 @@ public class PinkNoiseWindModel implements WindModel {
|
||||
randomSource = null;
|
||||
}
|
||||
|
||||
public void loadFrom(PinkNoiseWindModel source) {
|
||||
this.average = source.average;
|
||||
this.direction = source.direction;
|
||||
this.standardDeviation = source.standardDeviation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModID getModID() {
|
||||
return ModID.ZERO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PinkNoiseWindModel clone() {
|
||||
try {
|
||||
PinkNoiseWindModel clone = (PinkNoiseWindModel) super.clone();
|
||||
clone.loadFrom(this);
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new AssertionError(); // This should never happen
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PinkNoiseWindModel that = (PinkNoiseWindModel) o;
|
||||
return Double.compare(that.average, average) == 0 &&
|
||||
Double.compare(that.standardDeviation, standardDeviation) == 0 &&
|
||||
Double.compare(that.direction, direction) == 0 &&
|
||||
seed == that.seed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = 17;
|
||||
result = 31 * result + Double.hashCode(average);
|
||||
result = 31 * result + Double.hashCode(standardDeviation);
|
||||
result = 31 * result + Double.hashCode(direction);
|
||||
result = 31 * result + seed;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addChangeListener(StateChangeListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeChangeListener(StateChangeListener listener) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
|
||||
public void fireChangeEvent() {
|
||||
EventObject event = new EventObject(this);
|
||||
// Copy the list before iterating to prevent concurrent modification exceptions.
|
||||
EventListener[] list = listeners.toArray(new EventListener[0]);
|
||||
for (EventListener l : list) {
|
||||
if (l instanceof StateChangeListener) {
|
||||
((StateChangeListener) l).stateChanged(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import info.openrocket.core.util.ChangeSource;
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.Monitorable;
|
||||
|
||||
public interface WindModel extends Monitorable {
|
||||
|
||||
public Coordinate getWindVelocity(double time, double altitude);
|
||||
public interface WindModel extends Monitorable, Cloneable, ChangeSource {
|
||||
Coordinate getWindVelocity(double time, double altitude);
|
||||
|
||||
WindModel clone();
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
public enum WindModelType {
|
||||
AVERAGE("Average"),
|
||||
MULTI_LEVEL("MultiLevel");
|
||||
|
||||
private final String stringValue;
|
||||
|
||||
WindModelType(String stringValue) {
|
||||
this.stringValue = stringValue;
|
||||
}
|
||||
|
||||
public String toStringValue() {
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
public static WindModelType fromString(String stringValue) {
|
||||
for (WindModelType type : WindModelType.values()) {
|
||||
if (type.stringValue.equalsIgnoreCase(stringValue)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("No enum constant " + WindModelType.class.getCanonicalName() + " for string value: " + stringValue);
|
||||
}
|
||||
}
|
@ -19,12 +19,14 @@ import info.openrocket.core.file.wavefrontobj.export.OBJExportOptions;
|
||||
import info.openrocket.core.material.Material;
|
||||
import info.openrocket.core.models.atmosphere.AtmosphericModel;
|
||||
import info.openrocket.core.models.atmosphere.ExtendedISAModel;
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.preset.ComponentPreset;
|
||||
import info.openrocket.core.rocketcomponent.FlightConfiguration;
|
||||
import info.openrocket.core.rocketcomponent.MassObject;
|
||||
import info.openrocket.core.rocketcomponent.Rocket;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.simulation.RK4SimulationStepper;
|
||||
import info.openrocket.core.simulation.SimulationOptionsInterface;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.util.BugException;
|
||||
import info.openrocket.core.util.BuildProperties;
|
||||
@ -35,7 +37,7 @@ import info.openrocket.core.util.LineStyle;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
public abstract class ApplicationPreferences implements ChangeSource, ORPreferences {
|
||||
public abstract class ApplicationPreferences implements ChangeSource, ORPreferences, SimulationOptionsInterface, StateChangeListener {
|
||||
private static final String SPLIT_CHARACTER = "|";
|
||||
|
||||
/*
|
||||
@ -154,6 +156,9 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
|
||||
private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
|
||||
|
||||
private PinkNoiseWindModel averageWindModel = null;
|
||||
|
||||
|
||||
/*
|
||||
* ******************************************************************************************
|
||||
*
|
||||
@ -347,19 +352,6 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public final double getWindTurbulenceIntensity() {
|
||||
return Application.getPreferences().getChoice(ApplicationPreferences.WIND_TURBULENCE, 0.9, 0.1);
|
||||
}
|
||||
|
||||
public final void setWindTurbulenceIntensity(double wti) {
|
||||
double oldWTI = Application.getPreferences().getChoice(ApplicationPreferences.WIND_TURBULENCE, 0.9, 0.3);
|
||||
|
||||
if (MathUtil.equals(oldWTI, wti))
|
||||
return;
|
||||
this.putDouble(ApplicationPreferences.WIND_TURBULENCE, wti);
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getLaunchRodLength() {
|
||||
return this.getDouble(LAUNCH_ROD_LENGTH, 1);
|
||||
}
|
||||
@ -402,45 +394,30 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
|
||||
|
||||
|
||||
public double getWindSpeedAverage() {
|
||||
return this.getDouble(WIND_AVERAGE, 2);
|
||||
protected void loadWindModelState() {
|
||||
double average = getDouble(WIND_AVERAGE, 2.0);
|
||||
double turbulenceIntensity = getDouble(WIND_TURBULENCE, 0.1);
|
||||
double direction = getDouble(WIND_DIRECTION, Math.PI / 2);
|
||||
|
||||
getAverageWindModel().setAverage(average);
|
||||
getAverageWindModel().setTurbulenceIntensity(turbulenceIntensity);
|
||||
getAverageWindModel().setDirection(direction);
|
||||
}
|
||||
|
||||
public void setWindSpeedAverage(double windAverage) {
|
||||
if (MathUtil.equals(this.getDouble(WIND_AVERAGE, 2), windAverage))
|
||||
return;
|
||||
this.putDouble(WIND_AVERAGE, MathUtil.max(windAverage, 0));
|
||||
fireChangeEvent();
|
||||
protected void storeWindModelState() {
|
||||
putDouble(WIND_AVERAGE, getAverageWindModel().getAverage());
|
||||
putDouble(WIND_TURBULENCE, getAverageWindModel().getTurbulenceIntensity());
|
||||
putDouble(WIND_DIRECTION, getAverageWindModel().getDirection());
|
||||
}
|
||||
|
||||
|
||||
public double getWindSpeedDeviation() {
|
||||
return this.getDouble(WIND_AVERAGE, 2) * this.getDouble(WIND_TURBULENCE, 0.1);
|
||||
@Override
|
||||
public PinkNoiseWindModel getAverageWindModel() {
|
||||
if (averageWindModel == null) {
|
||||
averageWindModel = new PinkNoiseWindModel();
|
||||
averageWindModel.addChangeListener(this);
|
||||
loadWindModelState();
|
||||
}
|
||||
|
||||
public void setWindSpeedDeviation(double windDeviation) {
|
||||
double windAverage = this.getDouble(WIND_DIRECTION, 2);
|
||||
if (windAverage < 0.1) {
|
||||
windAverage = 0.1;
|
||||
}
|
||||
setWindTurbulenceIntensity(windDeviation / windAverage);
|
||||
}
|
||||
|
||||
public void setWindDirection(double direction) {
|
||||
direction = MathUtil.reduce2Pi(direction);
|
||||
if (this.getBoolean(LAUNCH_INTO_WIND, true)) {
|
||||
this.setLaunchRodDirection(direction);
|
||||
}
|
||||
if (MathUtil.equals(this.getDouble(WIND_DIRECTION, Math.PI / 2), direction))
|
||||
return;
|
||||
this.putDouble(WIND_DIRECTION, direction);
|
||||
fireChangeEvent();
|
||||
|
||||
}
|
||||
|
||||
public double getWindDirection() {
|
||||
return this.getDouble(WIND_DIRECTION, Math.PI / 2);
|
||||
|
||||
return averageWindModel;
|
||||
}
|
||||
|
||||
public double getLaunchAltitude() {
|
||||
@ -1364,4 +1341,11 @@ public abstract class ApplicationPreferences implements ChangeSource, ORPreferen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stateChanged(EventObject e) {
|
||||
if (e.getSource() == averageWindModel) {
|
||||
storeWindModelState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,10 +35,9 @@ public class DefaultSimulationOptionFactory {
|
||||
SimulationOptions defaults = new SimulationOptions();
|
||||
if (prefs != null) {
|
||||
|
||||
defaults.setWindSpeedAverage(prefs.getDouble(SIMCONDITION_WIND_SPEED, defaults.getWindSpeedAverage()));
|
||||
defaults.setWindSpeedDeviation(prefs.getDouble(SIMCONDITION_WIND_STDDEV, defaults.getWindSpeedDeviation()));
|
||||
defaults.setWindTurbulenceIntensity(
|
||||
prefs.getDouble(SIMCONDITION_WIND_TURB, defaults.getWindTurbulenceIntensity()));
|
||||
defaults.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()));
|
||||
@ -59,9 +58,9 @@ public class DefaultSimulationOptionFactory {
|
||||
|
||||
public void saveDefault(SimulationOptions newDefaults) {
|
||||
|
||||
prefs.putDouble(SIMCONDITION_WIND_SPEED, newDefaults.getWindSpeedAverage());
|
||||
prefs.putDouble(SIMCONDITION_WIND_STDDEV, newDefaults.getWindSpeedDeviation());
|
||||
prefs.putDouble(SIMCONDITION_WIND_TURB, newDefaults.getWindTurbulenceIntensity());
|
||||
prefs.putDouble(SIMCONDITION_WIND_SPEED, newDefaults.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,6 +6,9 @@ import java.util.EventObject;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
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;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -54,12 +57,8 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
private double launchRodLength = preferences.getDouble(ApplicationPreferences.LAUNCH_ROD_LENGTH, 1);
|
||||
private boolean launchIntoWind = preferences.getBoolean(ApplicationPreferences.LAUNCH_INTO_WIND, true);
|
||||
private double launchRodAngle = preferences.getDouble(ApplicationPreferences.LAUNCH_ROD_ANGLE, 0);
|
||||
private double windDirection = preferences.getDouble(ApplicationPreferences.WIND_DIRECTION, Math.PI / 2);
|
||||
private double launchRodDirection = preferences.getDouble(ApplicationPreferences.LAUNCH_ROD_DIRECTION, Math.PI / 2);
|
||||
|
||||
private double windAverage = preferences.getDouble(ApplicationPreferences.WIND_AVERAGE, 2.0);
|
||||
private double windTurbulence = preferences.getDouble(ApplicationPreferences.WIND_TURBULENCE, 0.1);
|
||||
|
||||
/*
|
||||
* SimulationOptions maintains the launch site parameters as separate double values,
|
||||
* and converts them into a WorldCoordinate when converting to SimulationConditions.
|
||||
@ -81,7 +80,15 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
|
||||
private List<EventListener> listeners = new ArrayList<>();
|
||||
|
||||
private WindModelType windModelType = WindModelType.AVERAGE;
|
||||
private PinkNoiseWindModel averageWindModel;
|
||||
private MultiLevelPinkNoiseWindModel multiLevelPinkNoiseWindModel;
|
||||
|
||||
public SimulationOptions() {
|
||||
averageWindModel = new PinkNoiseWindModel(randomSeed);
|
||||
averageWindModel.addChangeListener(e -> fireChangeEvent());
|
||||
multiLevelPinkNoiseWindModel = new MultiLevelPinkNoiseWindModel();
|
||||
multiLevelPinkNoiseWindModel.addChangeListener(e -> fireChangeEvent());
|
||||
}
|
||||
|
||||
public double getLaunchRodLength() {
|
||||
@ -120,6 +127,12 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
|
||||
public double getLaunchRodDirection() {
|
||||
if (launchIntoWind) {
|
||||
double windDirection;
|
||||
if (windModelType == WindModelType.AVERAGE) {
|
||||
windDirection = averageWindModel.getDirection();
|
||||
} else {
|
||||
windDirection = multiLevelPinkNoiseWindModel.getWindDirection(0, launchAltitude);
|
||||
}
|
||||
this.setLaunchRodDirection(windDirection);
|
||||
}
|
||||
return launchRodDirection;
|
||||
@ -133,57 +146,33 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getWindSpeedAverage() {
|
||||
return windAverage;
|
||||
public WindModelType getWindModelType() {
|
||||
return windModelType;
|
||||
}
|
||||
|
||||
public void setWindSpeedAverage(double windAverage) {
|
||||
if (MathUtil.equals(this.windAverage, windAverage))
|
||||
return;
|
||||
this.windAverage = MathUtil.max(windAverage, 0);
|
||||
if (MathUtil.equals(this.windAverage, 0)) {
|
||||
setWindTurbulenceIntensity(0);
|
||||
}
|
||||
public void setWindModelType(WindModelType windModelType) {
|
||||
if (this.windModelType != windModelType) {
|
||||
this.windModelType = windModelType;
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public double getWindSpeedDeviation() {
|
||||
return windAverage * windTurbulence;
|
||||
}
|
||||
|
||||
public void setWindSpeedDeviation(double windDeviation) {
|
||||
if (windAverage < 0.1) {
|
||||
windAverage = 0.1;
|
||||
public WindModel getWindModel() {
|
||||
if (windModelType == WindModelType.AVERAGE) {
|
||||
return averageWindModel;
|
||||
} else if (windModelType == WindModelType.MULTI_LEVEL) {
|
||||
return multiLevelPinkNoiseWindModel;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown wind model type: " + windModelType);
|
||||
}
|
||||
setWindTurbulenceIntensity(windDeviation / windAverage);
|
||||
}
|
||||
|
||||
public double getWindTurbulenceIntensity() {
|
||||
return windTurbulence;
|
||||
public PinkNoiseWindModel getAverageWindModel() {
|
||||
return averageWindModel;
|
||||
}
|
||||
|
||||
public void setWindTurbulenceIntensity(double intensity) {
|
||||
// Does not check equality so that setWindSpeedDeviation can be sure of event
|
||||
// firing
|
||||
this.windTurbulence = intensity;
|
||||
fireChangeEvent();
|
||||
}
|
||||
|
||||
public void setWindDirection(double direction) {
|
||||
direction = MathUtil.reduce2Pi(direction);
|
||||
if (launchIntoWind) {
|
||||
this.setLaunchRodDirection(direction);
|
||||
}
|
||||
if (MathUtil.equals(this.windDirection, direction))
|
||||
return;
|
||||
this.windDirection = direction;
|
||||
fireChangeEvent();
|
||||
|
||||
}
|
||||
|
||||
public double getWindDirection() {
|
||||
return this.windDirection;
|
||||
|
||||
public MultiLevelPinkNoiseWindModel getMultiLevelWindModel() {
|
||||
return multiLevelPinkNoiseWindModel;
|
||||
}
|
||||
|
||||
public double getLaunchAltitude() {
|
||||
@ -349,7 +338,16 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
public SimulationOptions clone() {
|
||||
try {
|
||||
SimulationOptions copy = (SimulationOptions) super.clone();
|
||||
|
||||
// Deep clone the wind models
|
||||
copy.averageWindModel = this.averageWindModel.clone();
|
||||
copy.multiLevelPinkNoiseWindModel = this.multiLevelPinkNoiseWindModel.clone();
|
||||
|
||||
copy.windModelType = this.windModelType;
|
||||
|
||||
// Create a new list for listeners
|
||||
copy.listeners = new ArrayList<>();
|
||||
|
||||
return copy;
|
||||
} catch (CloneNotSupportedException e) {
|
||||
throw new BugException(e);
|
||||
@ -361,6 +359,20 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
// only do it if one of the "important" (user specified) parameters has really
|
||||
// changed.
|
||||
boolean isChanged = false;
|
||||
|
||||
if (this.windModelType != src.windModelType) {
|
||||
isChanged = true;
|
||||
this.windModelType = src.windModelType;
|
||||
}
|
||||
if (!this.averageWindModel.equals(src.averageWindModel)) {
|
||||
isChanged = true;
|
||||
this.averageWindModel.loadFrom(src.averageWindModel);
|
||||
}
|
||||
if (!this.multiLevelPinkNoiseWindModel.equals(src.multiLevelPinkNoiseWindModel)) {
|
||||
isChanged = true;
|
||||
this.multiLevelPinkNoiseWindModel.loadFrom(src.multiLevelPinkNoiseWindModel);
|
||||
}
|
||||
|
||||
if (this.launchAltitude != src.launchAltitude) {
|
||||
isChanged = true;
|
||||
this.launchAltitude = src.launchAltitude;
|
||||
@ -405,18 +417,7 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
isChanged = true;
|
||||
this.maximumAngle = src.maximumAngle;
|
||||
}
|
||||
if (this.windAverage != src.windAverage) {
|
||||
isChanged = true;
|
||||
this.windAverage = src.windAverage;
|
||||
}
|
||||
if (this.windDirection != src.windDirection) {
|
||||
isChanged = true;
|
||||
this.windDirection = src.windDirection;
|
||||
}
|
||||
if (this.windTurbulence != src.windTurbulence) {
|
||||
isChanged = true;
|
||||
this.windTurbulence = src.windTurbulence;
|
||||
}
|
||||
|
||||
if (this.timeStep != src.timeStep) {
|
||||
isChanged = true;
|
||||
this.timeStep = src.timeStep;
|
||||
@ -453,10 +454,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
|
||||
MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
|
||||
MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
|
||||
MathUtil.equals(this.timeStep, o.timeStep) &&
|
||||
MathUtil.equals(this.windAverage, o.windAverage) &&
|
||||
MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
|
||||
MathUtil.equals(this.windDirection, o.windDirection));
|
||||
MathUtil.equals(this.timeStep, o.timeStep)) &&
|
||||
this.windModelType == o.windModelType &&
|
||||
this.averageWindModel.equals(o.averageWindModel) &&
|
||||
this.multiLevelPinkNoiseWindModel.equals(o.multiLevelPinkNoiseWindModel);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -505,17 +506,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
conditions.setGeodeticComputation(getGeodeticComputation());
|
||||
conditions.setRandomSeed(randomSeed);
|
||||
|
||||
PinkNoiseWindModel windModel = new PinkNoiseWindModel(randomSeed);
|
||||
windModel.setAverage(getWindSpeedAverage());
|
||||
windModel.setStandardDeviation(getWindSpeedDeviation());
|
||||
windModel.setDirection(windDirection);
|
||||
|
||||
WindModel windModel = getWindModel().clone();
|
||||
conditions.setWindModel(windModel);
|
||||
|
||||
conditions.setAtmosphericModel(getAtmosphericModel());
|
||||
|
||||
GravityModel gravityModel = new WGSGravityModel();
|
||||
|
||||
conditions.setGravityModel(gravityModel);
|
||||
|
||||
conditions.setAerodynamicCalculator(new BarrowmanCalculator());
|
||||
@ -533,10 +527,10 @@ public class SimulationOptions implements ChangeSource, Cloneable, SimulationOpt
|
||||
.concat(String.format(" launchRodLength: %f\n", launchRodLength))
|
||||
.concat(String.format(" launchIntoWind: %b\n", launchIntoWind))
|
||||
.concat(String.format(" launchRodAngle: %f\n", launchRodAngle))
|
||||
.concat(String.format(" windDirection: %f\n", windDirection))
|
||||
.concat(String.format(" launchRodDirection: %f\n", launchRodDirection))
|
||||
.concat(String.format(" windAverage: %f\n", windAverage))
|
||||
.concat(String.format(" windTurbulence: %f\n", windTurbulence))
|
||||
.concat(String.format(" windModelType: %s\n", windModelType))
|
||||
.concat(String.format(" pinkNoiseWindModel: %s\n", 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))
|
||||
|
@ -1,5 +1,6 @@
|
||||
package info.openrocket.core.simulation;
|
||||
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.util.ChangeSource;
|
||||
import info.openrocket.core.util.GeodeticComputationStrategy;
|
||||
|
||||
@ -20,31 +21,7 @@ public interface SimulationOptionsInterface extends ChangeSource {
|
||||
|
||||
void setLaunchRodDirection(double launchRodDirection);
|
||||
|
||||
double getWindSpeedAverage();
|
||||
|
||||
void setWindSpeedAverage(double windAverage);
|
||||
|
||||
double getWindSpeedDeviation();
|
||||
|
||||
void setWindSpeedDeviation(double windDeviation);
|
||||
|
||||
/**
|
||||
* Return the wind turbulence intensity (standard deviation / average).
|
||||
*
|
||||
* @return the turbulence intensity
|
||||
*/
|
||||
double getWindTurbulenceIntensity();
|
||||
|
||||
/**
|
||||
* Set the wind standard deviation to match the given turbulence intensity.
|
||||
*
|
||||
* @param intensity the turbulence intensity
|
||||
*/
|
||||
void setWindTurbulenceIntensity(double intensity);
|
||||
|
||||
void setWindDirection(double direction);
|
||||
|
||||
double getWindDirection();
|
||||
PinkNoiseWindModel getAverageWindModel();
|
||||
|
||||
double getLaunchAltitude();
|
||||
|
||||
|
@ -42,7 +42,6 @@ public class AbstractChangeSource implements ChangeSource {
|
||||
((StateChangeListener) l).stateChanged(event);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -312,6 +312,20 @@ public final class Coordinate implements Cloneable, Serializable {
|
||||
return new Coordinate(x1, y1, z1, w1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate between two coordinates. The fraction is the weight of the other coordinate.
|
||||
* @param other Coordinate to interpolate to.
|
||||
* @param fraction Interpolation fraction (0 = this, 1 = other).
|
||||
* @return Interpolated coordinate.
|
||||
*/
|
||||
public Coordinate interpolate(Coordinate other, double fraction) {
|
||||
double x1 = this.x + (other.x - this.x) * fraction;
|
||||
double y1 = this.y + (other.y - this.y) * fraction;
|
||||
double z1 = this.z + (other.z - this.z) * fraction;
|
||||
double w1 = this.weight + (other.weight - this.weight) * fraction;
|
||||
return new Coordinate(x1, y1, z1, w1);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests whether the coordinates are the equal.
|
||||
|
@ -331,7 +331,6 @@ public class MathUtil {
|
||||
* or if t is outsize the domain.
|
||||
*/
|
||||
public static double interpolate(List<Double> domain, List<Double> range, double t) {
|
||||
|
||||
if (domain == null || range == null || domain.size() != range.size()) {
|
||||
return Double.NaN;
|
||||
}
|
||||
@ -368,7 +367,16 @@ public class MathUtil {
|
||||
}
|
||||
|
||||
return range.get(left) + (t - domain.get(left)) * deltay / deltax;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Use interpolation to determine the value of the function at point t.
|
||||
* @param a the lower bound
|
||||
* @param b the upper bound
|
||||
* @param fraction the fraction between a and b
|
||||
* @return the interpolated value
|
||||
*/
|
||||
public static double interpolate(double a, double b, double fraction) {
|
||||
return a + (b - a) * fraction;
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +242,7 @@ public class Quaternion {
|
||||
// return = (a,b,c,d) * (this)^-1 = (a,b,c,d) * (w,-x,-y,-z)
|
||||
|
||||
// Assert that the w-value is zero
|
||||
assert (Math.abs(a * w + b * x + c * y + d * z) < coord.max() * MathUtil.EPSILON)
|
||||
assert (Math.abs(a * w + b * x + c * y + d * z) <= coord.max() * MathUtil.EPSILON)
|
||||
: ("Should be zero: " + (a * w + b * x + c * y + d * z) + " in " + this + " c=" + coord);
|
||||
|
||||
return new Coordinate(
|
||||
|
@ -506,6 +506,15 @@ simedtdlg.lbl.ttip.Pressure = The atmospheric pressure at the launch site.
|
||||
simedtdlg.lbl.Launchsite = Launch site
|
||||
simedtdlg.lbl.Latitude = Latitude:
|
||||
simedtdlg.lbl.ttip.Latitude = <html>The launch site latitude affects the gravitational pull of Earth.<br>Positive values are on the Northern hemisphere, negative values on the Southern hemisphere.
|
||||
simedtdlg.col.Altitude = Altitude
|
||||
simedtdlg.col.Speed = Speed
|
||||
simedtdlg.col.Direction = Direction
|
||||
simedtdlg.col.StandardDeviation = Deviation
|
||||
simedtdlg.col.Turbulence = Turbulence
|
||||
simedtdlg.col.Intensity = Intensity
|
||||
simedtdlg.col.Unit = Unit
|
||||
simedtdlg.col.Delete = Delete
|
||||
simedtdlg.popupmenu.Delete = Delete level
|
||||
|
||||
simedtdlg.lbl.Longitude = Longitude:
|
||||
simedtdlg.lbl.ttip.Longitude = <html>Required for weather prediction and elevation models.
|
||||
@ -555,6 +564,20 @@ simedtdlg.IntensityDesc.Medium = Medium
|
||||
simedtdlg.IntensityDesc.High = High
|
||||
simedtdlg.IntensityDesc.Veryhigh = Very high
|
||||
simedtdlg.IntensityDesc.Extreme = Extreme
|
||||
simedtdlg.lbl.WindModelSelection = Wind model to use:
|
||||
simedtdlg.radio.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
|
||||
simedtdlg.but.deleteWindLevel = Delete level
|
||||
simedtdlg.but.visualizeWindLevels = Visualize levels
|
||||
|
||||
! WindLevelVisualizationDialog
|
||||
WindLevelVisualizationDialog.title.WindLevelVisualization = Wind Level Visualization
|
||||
WindLevelVisualizationDialog.lbl.WindSpeed = Wind speed
|
||||
WindLevelVisualizationDialog.lbl.Altitude = Altitude
|
||||
WindLevelVisualizationDialog.checkbox.ShowDirections = Show wind direction vectors
|
||||
|
||||
! SimulationConfigDialog
|
||||
SimulationConfigDialog.tab.Settings = Settings
|
||||
|
@ -0,0 +1,203 @@
|
||||
package info.openrocket.core.models.wind;
|
||||
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.ModID;
|
||||
import info.openrocket.core.util.StateChangeListener;
|
||||
|
||||
import java.util.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 MultiLevelPinkNoiseWindModel model;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
model = new MultiLevelPinkNoiseWindModel();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Add and remove wind levels")
|
||||
void testAddAndRemoveWindLevels() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4, 1);
|
||||
model.addWindLevel(200, 10, Math.PI / 2, 1);
|
||||
assertEquals(2, model.getLevels().size());
|
||||
|
||||
model.removeWindLevel(100);
|
||||
assertEquals(1, model.getLevels().size());
|
||||
assertEquals(200, model.getLevels().get(0).altitude, EPSILON);
|
||||
|
||||
model.removeWindLevel(0);
|
||||
assertEquals(1, model.getLevels().size());
|
||||
assertEquals(200, model.getLevels().get(0).altitude, EPSILON);
|
||||
|
||||
model.removeWindLevel(200);
|
||||
assertTrue(model.getLevels().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Adding duplicate altitude throws IllegalArgumentException")
|
||||
void testAddDuplicateAltitude() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4, 1);
|
||||
assertThrows(IllegalArgumentException.class, () -> model.addWindLevel(100, 10, Math.PI / 2, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get wind velocity")
|
||||
void testGetWindVelocity() {
|
||||
model.addWindLevel(0, 5, 0, 1);
|
||||
model.addWindLevel(1000, 10, Math.PI / 2, 2);
|
||||
|
||||
verifyWind(0, 5, 0, 1);
|
||||
verifyWind(1000, 10, Math.PI / 2, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Interpolation between levels")
|
||||
void testInterpolationBetweenLevels() {
|
||||
// Test speed interpolation
|
||||
model.addWindLevel(0, 5, 0, 0.1);
|
||||
model.addWindLevel(1000, 10, 0, 0.3);
|
||||
|
||||
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, 1.4);
|
||||
model.addWindLevel(200, 10, Math.PI / 2, 2.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 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.sortLevels();
|
||||
|
||||
List<MultiLevelPinkNoiseWindModel.LevelWindModel> levels = model.getLevels();
|
||||
assertEquals(3, levels.size());
|
||||
assertEquals(100, levels.get(0).altitude, EPSILON);
|
||||
assertEquals(200, levels.get(1).altitude, EPSILON);
|
||||
assertEquals(300, levels.get(2).altitude, EPSILON);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Clone model")
|
||||
void testClone() {
|
||||
model.addWindLevel(100, 5, Math.PI / 4, 1);
|
||||
model.addWindLevel(200, 10, Math.PI / 2, 2);
|
||||
|
||||
MultiLevelPinkNoiseWindModel clonedModel = model.clone();
|
||||
assertNotSame(model, clonedModel);
|
||||
assertEquals(model, clonedModel);
|
||||
|
||||
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, 2);
|
||||
model.addWindLevel(200, 10, Math.PI / 2, 1);
|
||||
|
||||
MultiLevelPinkNoiseWindModel newModel = new MultiLevelPinkNoiseWindModel();
|
||||
newModel.loadFrom(model);
|
||||
|
||||
assertEquals(model, newModel);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Get ModID")
|
||||
void testGetModID() {
|
||||
assertEquals(ModID.ZERO, model.getModID());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Change listeners")
|
||||
void testChangeListeners() {
|
||||
final boolean[] listenerCalled = {false};
|
||||
StateChangeListener listener = event -> listenerCalled[0] = true;
|
||||
|
||||
model.addChangeListener(listener);
|
||||
model.fireChangeEvent();
|
||||
assertTrue(listenerCalled[0]);
|
||||
|
||||
listenerCalled[0] = false;
|
||||
model.removeChangeListener(listener);
|
||||
model.fireChangeEvent();
|
||||
assertFalse(listenerCalled[0]);
|
||||
}
|
||||
|
||||
private void verifyWind(double altitude, double expectedSpeed, double expectedDirection, 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);
|
||||
}
|
||||
}
|
@ -174,11 +174,13 @@ public class DisableStageTest extends BaseTestCase {
|
||||
simRemoved.setFlightConfigurationId(fcid);
|
||||
simRemoved.getOptions().setISAAtmosphere(true);
|
||||
simRemoved.getOptions().setTimeStep(0.05);
|
||||
simRemoved.getOptions().getAverageWindModel().setStandardDeviation(0.0);
|
||||
|
||||
Simulation simDisabled = new Simulation(rocketDisabled);
|
||||
simDisabled.setFlightConfigurationId(fcid);
|
||||
simDisabled.getOptions().setISAAtmosphere(true);
|
||||
simDisabled.getOptions().setTimeStep(0.05);
|
||||
simDisabled.getOptions().getAverageWindModel().setStandardDeviation(0.0);
|
||||
|
||||
compareSims(simRemoved, simDisabled, DELTA);
|
||||
|
||||
|
@ -69,6 +69,7 @@ public class FlightEventsTest extends BaseTestCase {
|
||||
final Simulation sim = new Simulation(rocket);
|
||||
sim.getOptions().setISAAtmosphere(true);
|
||||
sim.getOptions().setTimeStep(0.05);
|
||||
sim.getOptions ().getAverageWindModel().setAverage(0.1);
|
||||
rocket.getSelectedConfiguration().setAllStages();
|
||||
FlightConfigurationId fcid = rocket.getSelectedConfiguration().getFlightConfigurationID();
|
||||
sim.setFlightConfigurationId(fcid);
|
||||
|
@ -10,15 +10,20 @@ import info.openrocket.core.formatting.RocketDescriptor;
|
||||
import info.openrocket.core.formatting.RocketDescriptorImpl;
|
||||
import info.openrocket.core.l10n.DebugTranslator;
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.models.wind.MultiLevelPinkNoiseWindModel;
|
||||
import info.openrocket.core.models.wind.PinkNoiseWindModel;
|
||||
import info.openrocket.core.plugin.PluginModule;
|
||||
import info.openrocket.core.preferences.ApplicationPreferences;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.startup.MockPreferences;
|
||||
import info.openrocket.core.util.Coordinate;
|
||||
import info.openrocket.core.util.MathUtil;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@ -60,16 +65,99 @@ public class SimulationConditionsTest {
|
||||
assertEquals(Math.PI / 2, options.getLaunchRodDirection(), EPSILON);
|
||||
assertEquals(0.0, options.getLaunchRodAngle(), EPSILON);
|
||||
assertTrue(options.getLaunchIntoWind());
|
||||
assertEquals(Math.PI / 2, options.getWindDirection(), EPSILON);
|
||||
assertEquals(0.1, options.getWindTurbulenceIntensity(), EPSILON);
|
||||
assertEquals(2.0, options.getWindSpeedAverage(), EPSILON);
|
||||
assertEquals(0.2, options.getWindSpeedDeviation(), EPSILON);
|
||||
assertEquals(Math.PI / 2, options.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 MultiLevelPinkNoiseWindModel in SimulationConditions")
|
||||
public void testWindModelComparison() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
|
||||
// Test PinkNoiseWindModel
|
||||
PinkNoiseWindModel pinkNoiseModel = new PinkNoiseWindModel();
|
||||
pinkNoiseModel.setAverage(5.0);
|
||||
pinkNoiseModel.setStandardDeviation(1.0);
|
||||
pinkNoiseModel.setDirection(Math.PI / 4); // 45 degrees
|
||||
|
||||
conditions.setWindModel(pinkNoiseModel);
|
||||
|
||||
Coordinate pinkNoiseVelocity = conditions.getWindModel().getWindVelocity(0, 100);
|
||||
assertNotNull(pinkNoiseVelocity);
|
||||
assertTrue(pinkNoiseVelocity.length() > 0);
|
||||
|
||||
// Test 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);
|
||||
|
||||
Coordinate multiLevelVelocity = conditions.getWindModel().getWindVelocity(0, 100);
|
||||
assertNotNull(multiLevelVelocity);
|
||||
assertTrue(multiLevelVelocity.length() > 0);
|
||||
|
||||
// Compare behaviors
|
||||
assertNotEquals(pinkNoiseVelocity, multiLevelVelocity);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test wind velocity consistency for MultiLevelPinkNoiseWindModel")
|
||||
public void testMultiLevelWindModelConsistency() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
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);
|
||||
|
||||
Coordinate velocity1 = conditions.getWindModel().getWindVelocity(0, 500);
|
||||
Coordinate velocity2 = conditions.getWindModel().getWindVelocity(0, 500);
|
||||
|
||||
assertEquals(velocity1, velocity2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test wind velocity variation for PinkNoiseWindModel")
|
||||
public void testPinkNoiseWindModelVariation() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
PinkNoiseWindModel pinkNoiseModel = new PinkNoiseWindModel();
|
||||
pinkNoiseModel.setAverage(5.0);
|
||||
pinkNoiseModel.setStandardDeviation(1.0);
|
||||
pinkNoiseModel.setDirection(Math.PI / 4);
|
||||
|
||||
conditions.setWindModel(pinkNoiseModel);
|
||||
|
||||
Coordinate velocity1 = conditions.getWindModel().getWindVelocity(0, 100);
|
||||
Coordinate velocity2 = conditions.getWindModel().getWindVelocity(1, 100);
|
||||
|
||||
assertNotEquals(velocity1, velocity2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test altitude dependence of MultiLevelPinkNoiseWindModel")
|
||||
public void testMultiLevelWindModelAltitudeDependence() {
|
||||
SimulationConditions conditions = new SimulationConditions();
|
||||
MultiLevelPinkNoiseWindModel multiLevelModel = new MultiLevelPinkNoiseWindModel();
|
||||
multiLevelModel.addWindLevel(0, 5.0, 0, 0);
|
||||
multiLevelModel.addWindLevel(1000, 10.0, Math.PI / 2, 0);
|
||||
|
||||
conditions.setWindModel(multiLevelModel);
|
||||
|
||||
Coordinate velocityLow = conditions.getWindModel().getWindVelocity(0, 0);
|
||||
Coordinate velocityHigh = conditions.getWindModel().getWindVelocity(0, 1000);
|
||||
Coordinate velocityMid = conditions.getWindModel().getWindVelocity(0, 500);
|
||||
|
||||
assertNotEquals(velocityLow, velocityHigh);
|
||||
assertTrue(velocityMid.length() > velocityLow.length() && velocityMid.length() < velocityHigh.length());
|
||||
}
|
||||
|
||||
|
||||
private static class PreferencesModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
|
@ -9,54 +9,144 @@ public class CoordinateTest {
|
||||
private static final double EPS = 0.0000000001;
|
||||
|
||||
@Test
|
||||
public void coordinateTest() {
|
||||
public void testConstructors() {
|
||||
Coordinate c1 = new Coordinate();
|
||||
assertCoordinateEquals(new Coordinate(0, 0, 0, 0), c1);
|
||||
|
||||
Coordinate c2 = new Coordinate(1);
|
||||
assertCoordinateEquals(new Coordinate(1, 0, 0, 0), c2);
|
||||
|
||||
Coordinate c3 = new Coordinate(1, 2);
|
||||
assertCoordinateEquals(new Coordinate(1, 2, 0, 0), c3);
|
||||
|
||||
Coordinate c4 = new Coordinate(1, 2, 3);
|
||||
assertCoordinateEquals(new Coordinate(1, 2, 3, 0), c4);
|
||||
|
||||
Coordinate c5 = new Coordinate(1, 2, 3, 4);
|
||||
assertCoordinateEquals(new Coordinate(1, 2, 3, 4), c5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetters() {
|
||||
Coordinate x = new Coordinate(1, 1, 1, 1);
|
||||
Coordinate y = new Coordinate(1, 2, 3, 4);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(2, 1, 1, 1), x.setX(2));
|
||||
assertCoordinateEquals(new Coordinate(1, 2, 1, 1), x.setY(2));
|
||||
assertCoordinateEquals(new Coordinate(1, 1, 2, 1), x.setZ(2));
|
||||
assertCoordinateEquals(new Coordinate(1, 1, 1, 2), x.setWeight(2));
|
||||
assertCoordinateEquals(new Coordinate(2, 3, 4, 1), x.setXYZ(y).add(1, 1, 1));
|
||||
|
||||
assertFalse(x.isNaN());
|
||||
assertTrue(x.setX(Double.NaN).isNaN());
|
||||
Coordinate y = new Coordinate(1, 2, 3, 4);
|
||||
assertCoordinateEquals(new Coordinate(1, 2, 3, 1), x.setXYZ(y));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsWeighted() {
|
||||
assertTrue(new Coordinate(1, 1, 1, 1).isWeighted());
|
||||
assertFalse(new Coordinate(1, 1, 1, 0).isWeighted());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsNaN() {
|
||||
assertFalse(new Coordinate(1, 1, 1, 1).isNaN());
|
||||
assertTrue(new Coordinate(Double.NaN, 1, 1, 1).isNaN());
|
||||
assertTrue(new Coordinate(1, Double.NaN, 1, 1).isNaN());
|
||||
assertTrue(new Coordinate(1, 1, Double.NaN, 1).isNaN());
|
||||
assertTrue(new Coordinate(1, 1, 1, Double.NaN).isNaN());
|
||||
assertTrue(Coordinate.NaN.isNaN());
|
||||
}
|
||||
|
||||
assertTrue(x.isWeighted());
|
||||
assertFalse(x.setWeight(0).isWeighted());
|
||||
@Test
|
||||
public void testAdd() {
|
||||
Coordinate x = new Coordinate(1, 1, 1, 1);
|
||||
Coordinate y = new Coordinate(1, 2, 3, 4);
|
||||
|
||||
assertCoordinateEquals(x, x.add(Coordinate.NUL));
|
||||
assertCoordinateEquals(new Coordinate(2, 3, 4, 5), x.add(y));
|
||||
assertCoordinateEquals(new Coordinate(2, 3, 4, 1), x.add(1, 2, 3));
|
||||
assertCoordinateEquals(new Coordinate(2, 3, 4, 5), x.add(1, 2, 3, 4));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSub() {
|
||||
Coordinate x = new Coordinate(1, 1, 1, 1);
|
||||
Coordinate y = new Coordinate(1, 2, 3, 4);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(0, -1, -2, 1), x.sub(y));
|
||||
assertCoordinateEquals(new Coordinate(0, -1, -2, 1), x.sub(1, 2, 3));
|
||||
|
||||
assertCoordinateEquals(new Coordinate(2, 4, 6, 8), y.multiply(2));
|
||||
|
||||
assertEquals(1 + 2 + 3, y.dot(x), EPS);
|
||||
assertEquals(1 + 2 + 3, x.dot(y), EPS);
|
||||
assertEquals(1 + 2 + 3, Coordinate.dot(x, y), EPS);
|
||||
assertEquals(x.dot(x), x.length2(), EPS);
|
||||
assertEquals(y.dot(y), y.length2(), EPS);
|
||||
assertEquals(3.7416573867739413, y.length(), EPS);
|
||||
assertEquals(1, y.normalize().length(), EPS);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(1.75, 1.75, 1.75, 4),
|
||||
new Coordinate(1, 1, 1, 1).average(new Coordinate(2, 2, 2, 3)));
|
||||
assertCoordinateEquals(new Coordinate(1, 1, 1, 1),
|
||||
new Coordinate(1, 1, 1, 1).average(new Coordinate(2, 2, 2, 0)));
|
||||
assertCoordinateEquals(new Coordinate(1.5, 1.5, 1.5, 0),
|
||||
new Coordinate(1, 1, 1, 0).average(new Coordinate(2, 2, 2, 0)));
|
||||
|
||||
}
|
||||
|
||||
private void assertCoordinateEquals(Coordinate a, Coordinate b) {
|
||||
assertEquals(a, b);
|
||||
assertEquals(a.weight, b.weight, EPS);
|
||||
@Test
|
||||
public void testMultiply() {
|
||||
Coordinate x = new Coordinate(1, 2, 3, 4);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(2, 4, 6, 8), x.multiply(2));
|
||||
assertCoordinateEquals(new Coordinate(1, 4, 9, 16), x.multiply(x));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDot() {
|
||||
Coordinate x = new Coordinate(1, 1, 1, 1);
|
||||
Coordinate y = new Coordinate(1, 2, 3, 4);
|
||||
|
||||
assertEquals(6, x.dot(y), EPS);
|
||||
assertEquals(6, y.dot(x), EPS);
|
||||
assertEquals(6, Coordinate.dot(x, y), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLength() {
|
||||
Coordinate x = new Coordinate(3, 4, 0, 1);
|
||||
|
||||
assertEquals(5, x.length(), EPS);
|
||||
assertEquals(25, x.length2(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMax() {
|
||||
assertEquals(3, new Coordinate(1, -2, 3, 4).max(), EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNormalize() {
|
||||
Coordinate x = new Coordinate(3, 4, 0, 2);
|
||||
Coordinate normalized = x.normalize();
|
||||
|
||||
assertEquals(1, normalized.length(), EPS);
|
||||
assertEquals(2, normalized.weight, EPS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCross() {
|
||||
Coordinate x = new Coordinate(1, 0, 0);
|
||||
Coordinate y = new Coordinate(0, 1, 0);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(0, 0, 1), x.cross(y));
|
||||
assertCoordinateEquals(new Coordinate(0, 0, 1), Coordinate.cross(x, y));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAverage() {
|
||||
Coordinate x = new Coordinate(1, 2, 4, 1);
|
||||
Coordinate y = new Coordinate(3, 5, 9, 1);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(2, 3.5, 6.5, 2), x.average(y));
|
||||
|
||||
y = new Coordinate(3, 5, 9, 3);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(2.5, 4.25, 7.75, 4), x.average(y));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInterpolate() {
|
||||
Coordinate x = new Coordinate(0, 0, 0, 0);
|
||||
Coordinate y = new Coordinate(10, 10, 10, 10);
|
||||
|
||||
assertCoordinateEquals(new Coordinate(5, 5, 5, 5), x.interpolate(y, 0.5));
|
||||
}
|
||||
|
||||
private void assertCoordinateEquals(Coordinate expected, Coordinate actual) {
|
||||
assertEquals(expected.x, actual.x, EPS);
|
||||
assertEquals(expected.y, actual.y, EPS);
|
||||
assertEquals(expected.z, actual.z, EPS);
|
||||
assertEquals(expected.weight, actual.weight, EPS);
|
||||
}
|
||||
}
|
||||
|
@ -70,3 +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="{average or multilevel}">), and windmodeltype to simulation conditions.
|
||||
|
@ -34,8 +34,7 @@ public class LaunchPreferencesPanel extends PreferencesPanel {
|
||||
add(warning, "spanx, growx 0, gapbottom para, wrap");
|
||||
|
||||
// Simulation conditions
|
||||
SimulationConditionsPanel.addSimulationConditionsPanel(this, preferences);
|
||||
|
||||
SimulationConditionsPanel.addSimulationConditionsPanel(this, preferences, false);
|
||||
}
|
||||
|
||||
private static void initColors() {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,244 @@
|
||||
package info.openrocket.swing.gui.simulation;
|
||||
|
||||
import info.openrocket.core.l10n.Translator;
|
||||
import info.openrocket.core.models.wind.MultiLevelPinkNoiseWindModel;
|
||||
import info.openrocket.core.startup.Application;
|
||||
import info.openrocket.core.unit.Unit;
|
||||
|
||||
import javax.swing.BorderFactory;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JCheckBox;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JPanel;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.awt.Dialog;
|
||||
import java.awt.Dimension;
|
||||
import java.awt.FlowLayout;
|
||||
import java.awt.FontMetrics;
|
||||
import java.awt.Graphics;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.event.WindowListener;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
public class WindLevelVisualizationDialog extends JDialog {
|
||||
private static final Translator trans = Application.getTranslator();
|
||||
|
||||
private final WindLevelVisualization visualization;
|
||||
private final JCheckBox showDirectionsCheckBox;
|
||||
|
||||
public WindLevelVisualizationDialog(Dialog owner, MultiLevelPinkNoiseWindModel model, Unit altitudeUnit, Unit speedUnit) {
|
||||
super(owner, trans.get("WindLevelVisualizationDialog.title.WindLevelVisualization"), false);
|
||||
|
||||
visualization = new WindLevelVisualization(model, altitudeUnit, speedUnit);
|
||||
visualization.setPreferredSize(new Dimension(400, 500));
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.add(visualization, BorderLayout.CENTER);
|
||||
|
||||
// Use BorderLayout for the control panel
|
||||
JPanel controlPanel = new JPanel(new BorderLayout());
|
||||
controlPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); // Add some padding
|
||||
|
||||
// Checkbox on the left
|
||||
showDirectionsCheckBox = new JCheckBox(trans.get("WindLevelVisualizationDialog.checkbox.ShowDirections"));
|
||||
showDirectionsCheckBox.setSelected(true);
|
||||
showDirectionsCheckBox.addActionListener(e -> {
|
||||
visualization.setShowDirections(showDirectionsCheckBox.isSelected());
|
||||
visualization.repaint();
|
||||
});
|
||||
controlPanel.add(showDirectionsCheckBox, BorderLayout.WEST);
|
||||
|
||||
// Close button on the right
|
||||
JButton closeButton = new JButton(trans.get("button.close"));
|
||||
closeButton.addActionListener(e -> dispose());
|
||||
JPanel closeButtonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
|
||||
closeButtonPanel.add(closeButton);
|
||||
controlPanel.add(closeButtonPanel, BorderLayout.EAST);
|
||||
|
||||
contentPane.add(controlPanel, BorderLayout.SOUTH);
|
||||
|
||||
setContentPane(contentPane);
|
||||
pack();
|
||||
setLocationRelativeTo(owner);
|
||||
|
||||
setDefaultCloseOperation(HIDE_ON_CLOSE);
|
||||
setAlwaysOnTop(true);
|
||||
}
|
||||
|
||||
public void updateUnits(Unit altitudeUnit, Unit speedUnit) {
|
||||
visualization.updateUnits(altitudeUnit, speedUnit);
|
||||
}
|
||||
|
||||
private static class WindLevelVisualization extends JPanel {
|
||||
private final MultiLevelPinkNoiseWindModel model;
|
||||
private static final int MARGIN = 50;
|
||||
private static final int ARROW_SIZE = 10;
|
||||
private static final int TICK_LENGTH = 5;
|
||||
|
||||
private Unit altitudeUnit;
|
||||
private Unit speedUnit;
|
||||
private boolean showDirections = true;
|
||||
|
||||
public WindLevelVisualization(MultiLevelPinkNoiseWindModel model, Unit altitudeUnit, Unit speedUnit) {
|
||||
this.model = model;
|
||||
this.altitudeUnit = altitudeUnit;
|
||||
this.speedUnit = speedUnit;
|
||||
}
|
||||
|
||||
public void updateUnits(Unit altitudeUnit, Unit speedUnit) {
|
||||
this.altitudeUnit = altitudeUnit;
|
||||
this.speedUnit = speedUnit;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public void setShowDirections(boolean showDirections) {
|
||||
this.showDirections = showDirections;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
Graphics2D g2d = (Graphics2D) g;
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
|
||||
// Enable antialiasing for smoother lines
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// Draw background
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
|
||||
List<MultiLevelPinkNoiseWindModel.LevelWindModel> levels = model.getLevels();
|
||||
if (levels.isEmpty()) return;
|
||||
|
||||
// Sort levels before drawing
|
||||
levels.sort(Comparator.comparingDouble(MultiLevelPinkNoiseWindModel.LevelWindModel::getAltitude));
|
||||
|
||||
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;
|
||||
double extendedMaxSpeed = maxSpeed * 1.1;
|
||||
|
||||
// Draw axes
|
||||
drawAxes(g2d, width, height, maxSpeed, maxAltitude, extendedMaxSpeed, extendedMaxAltitude);
|
||||
|
||||
// Draw wind levels
|
||||
for (int i = 0; i < levels.size(); i++) {
|
||||
MultiLevelPinkNoiseWindModel.LevelWindModel level = levels.get(i);
|
||||
|
||||
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);
|
||||
g2d.fillOval(x - 3, y - 3, 6, 6);
|
||||
|
||||
// Draw wind direction arrow
|
||||
if (showDirections) {
|
||||
drawWindArrow(g2d, x, y, level.getDirection());
|
||||
}
|
||||
|
||||
// Draw connecting line if not the first point
|
||||
if (i > 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawAxes(Graphics2D g2d, int width, int height, double maxSpeed, double maxAltitude,
|
||||
double extendedMaxSpeed, double extendedMaxAltitude) {
|
||||
g2d.setColor(Color.BLACK);
|
||||
|
||||
// Draw X-axis
|
||||
g2d.drawLine(MARGIN, height - MARGIN, width - MARGIN, height - MARGIN);
|
||||
drawFilledArrowHead(g2d, width - MARGIN + (ARROW_SIZE-2), height - MARGIN, ARROW_SIZE, 0);
|
||||
|
||||
// Draw Y-axis
|
||||
g2d.drawLine(MARGIN, height - MARGIN, MARGIN, MARGIN);
|
||||
drawFilledArrowHead(g2d, MARGIN, MARGIN - (ARROW_SIZE-2), ARROW_SIZE, -Math.PI / 2);
|
||||
|
||||
// Draw max value ticks and labels
|
||||
g2d.setFont(g2d.getFont().deriveFont(10f));
|
||||
FontMetrics fm = g2d.getFontMetrics();
|
||||
|
||||
// X-axis max value
|
||||
int xTickX = MARGIN + (int) ((maxSpeed / extendedMaxSpeed) * (width - 2 * MARGIN));
|
||||
g2d.drawLine(xTickX, height - MARGIN, xTickX, height - MARGIN + TICK_LENGTH);
|
||||
String xMaxLabel = speedUnit.toString(maxSpeed);
|
||||
g2d.drawString(xMaxLabel, xTickX - fm.stringWidth(xMaxLabel) / 2, height - MARGIN + TICK_LENGTH + fm.getHeight());
|
||||
|
||||
// Y-axis max value
|
||||
int yTickY = height - MARGIN - (int) ((maxAltitude / extendedMaxAltitude) * (height - 2 * MARGIN));
|
||||
g2d.drawLine(MARGIN - TICK_LENGTH, yTickY, MARGIN, yTickY);
|
||||
String yMaxLabel = altitudeUnit.toString(maxAltitude);
|
||||
g2d.drawString(yMaxLabel, MARGIN - TICK_LENGTH - fm.stringWidth(yMaxLabel) - 2, yTickY + fm.getAscent() / 2);
|
||||
|
||||
// Draw axis labels
|
||||
g2d.setFont(g2d.getFont().deriveFont(12f));
|
||||
fm = g2d.getFontMetrics();
|
||||
|
||||
// X-axis label
|
||||
String xLabel = trans.get("WindLevelVisualizationDialog.lbl.WindSpeed") + " (" + speedUnit.getUnit() + ")";
|
||||
g2d.drawString(xLabel, width / 2 - fm.stringWidth(xLabel) / 2, height - 10);
|
||||
|
||||
// Y-axis label
|
||||
String yLabel = trans.get("WindLevelVisualizationDialog.lbl.Altitude") + " (" + altitudeUnit.getUnit() + ")";
|
||||
AffineTransform originalTransform = g2d.getTransform();
|
||||
g2d.rotate(-Math.PI / 2);
|
||||
g2d.drawString(yLabel, -height / 2 - fm.stringWidth(yLabel) / 2, MARGIN / 2);
|
||||
g2d.setTransform(originalTransform);
|
||||
}
|
||||
|
||||
private void drawFilledArrowHead(Graphics2D g, int x, int y, int arrowSize, double angle) {
|
||||
int[] xPoints = new int[3];
|
||||
int[] yPoints = new int[3];
|
||||
|
||||
xPoints[0] = x;
|
||||
yPoints[0] = y;
|
||||
xPoints[1] = x - (int) (arrowSize * Math.cos(angle - Math.PI / 6));
|
||||
yPoints[1] = y - (int) (arrowSize * Math.sin(angle - Math.PI / 6));
|
||||
xPoints[2] = x - (int) (arrowSize * Math.cos(angle + Math.PI / 6));
|
||||
yPoints[2] = y - (int) (arrowSize * Math.sin(angle + Math.PI / 6));
|
||||
|
||||
g.fillPolygon(xPoints, yPoints, 3);
|
||||
}
|
||||
|
||||
private void drawWindArrow(Graphics2D g, int x, int y, double direction) {
|
||||
int directionVectorLength = 15;
|
||||
int dx = (int) (directionVectorLength * Math.sin(direction));
|
||||
int dy = (int) (directionVectorLength * Math.cos(direction));
|
||||
int arrowSize = 10;
|
||||
|
||||
g.setColor(Color.RED);
|
||||
|
||||
// Draw the main line
|
||||
g.drawLine(x, y, x + dx, y - dy);
|
||||
|
||||
int dx_arrow = (int) ((arrowSize-1) * Math.sin(direction));
|
||||
int dy_arrow = (int) ((arrowSize-1) * Math.cos(direction));
|
||||
|
||||
// Draw filled arrow head
|
||||
drawFilledArrowHead(g, x + dx + dx_arrow, y - dy - dy_arrow, arrowSize, direction - Math.PI/2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
for (WindowListener listener : getWindowListeners()) {
|
||||
removeWindowListener(listener);
|
||||
}
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -40,7 +40,6 @@ import info.openrocket.core.rocketcomponent.RailButton;
|
||||
import info.openrocket.core.rocketcomponent.RecoveryDevice;
|
||||
import info.openrocket.core.rocketcomponent.RocketComponent;
|
||||
import info.openrocket.core.rocketcomponent.TubeFinSet;
|
||||
import info.openrocket.core.simulation.SimulationOptionsInterface;
|
||||
import info.openrocket.core.util.ORColor;
|
||||
import info.openrocket.core.arch.SystemInfo;
|
||||
import info.openrocket.core.document.Simulation;
|
||||
@ -59,7 +58,7 @@ import info.openrocket.core.util.BuildProperties;
|
||||
import info.openrocket.swing.communication.AssetHandler.UpdatePlatform;
|
||||
|
||||
|
||||
public class SwingPreferences extends ApplicationPreferences implements SimulationOptionsInterface {
|
||||
public class SwingPreferences extends ApplicationPreferences {
|
||||
private static final Logger log = LoggerFactory.getLogger(SwingPreferences.class);
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user