package net.sf.openrocket.startup; import java.util.ArrayList; import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import net.sf.openrocket.database.Databases; import net.sf.openrocket.material.Material; import net.sf.openrocket.models.atmosphere.AtmosphericModel; import net.sf.openrocket.models.atmosphere.ExtendedISAModel; import net.sf.openrocket.preset.ComponentPreset; import net.sf.openrocket.rocketcomponent.BodyComponent; import net.sf.openrocket.rocketcomponent.FinSet; import net.sf.openrocket.rocketcomponent.InternalComponent; import net.sf.openrocket.rocketcomponent.LaunchLug; import net.sf.openrocket.rocketcomponent.MassObject; import net.sf.openrocket.rocketcomponent.RecoveryDevice; import net.sf.openrocket.rocketcomponent.RocketComponent; import net.sf.openrocket.rocketcomponent.TubeFinSet; import net.sf.openrocket.simulation.RK4SimulationStepper; import net.sf.openrocket.util.BugException; import net.sf.openrocket.util.BuildProperties; import net.sf.openrocket.util.ChangeSource; import net.sf.openrocket.util.Color; import net.sf.openrocket.util.GeodeticComputationStrategy; import net.sf.openrocket.util.LineStyle; import net.sf.openrocket.util.MathUtil; import net.sf.openrocket.util.StateChangeListener; import net.sf.openrocket.util.UniqueID; public abstract class Preferences implements ChangeSource { /* * Well known string keys to preferences. * There are other strings out there in the source as well. */ public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition"; public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves"; public static final String DEFAULT_MACH_NUMBER = "DefaultMachNumber"; // Preferences related to data export public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator"; public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment"; public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment"; public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments"; public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter"; public static final String USER_LOCAL = "locale"; public static final String PLOT_SHOW_POINTS = "ShowPlotPoints"; private static final String CHECK_UPDATES = "CheckUpdates"; public static final String LAST_UPDATE = "LastUpdateVersion"; public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch"; public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar"; // Node names public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors"; private static final String AUTO_OPEN_LAST_DESIGN = "AUTO_OPEN_LAST_DESIGN"; private static final String SHOW_ROCKSIM_FORMAT_WARNING = "SHOW_ROCKSIM_FORMAT_WARNING"; //Preferences related to 3D graphics public static final String OPENGL_ENABLED = "OpenGL_Is_Enabled"; public static final String OPENGL_ENABLE_AA = "OpenGL_Antialiasing_Is_Enabled"; public static final String OPENGL_USE_FBO = "OpenGL_Use_FBO"; public static final String ROCKET_INFO_FONT_SIZE = "RocketInfoFontSize"; //Preferences Related to Simulations public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation"; public static final String AUTO_RUN_SIMULATIONS = "AutoRunSimulations"; public static final String LAUNCH_ROD_LENGTH = "LaunchRodLength"; public static final String LAUNCH_INTO_WIND = "LaunchIntoWind"; public static final String LAUNCH_ROD_ANGLE = "LaunchRodAngle"; public static final String LAUNCH_ROD_DIRECTION = "LaunchRodDirection"; public static final String WIND_DIRECTION = "WindDirection"; public static final String WIND_AVERAGE = "WindAverage"; public static final String WIND_TURBULANCE = "WindTurbulence"; public static final String LAUNCH_ALTITUDE = "LaunchAltitude"; public static final String LAUNCH_LATITUDE = "LaunchLatitude"; public static final String LAUNCH_LONGITUDE = "LaunchLongitude"; public static final String LAUNCH_TEMPERATURE = "LaunchTemperature"; public static final String LAUNCH_PRESSURE = "LaunchPressure"; public static final String LAUNCH_USE_ISA = "LaunchUseISA"; public static final String SIMULATION_TIME_STEP = "SimulationTimeStep"; public static final String GEODETIC_COMPUTATION = "GeodeticComputationStrategy"; private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel(); /* * ****************************************************************************************** * * Abstract methods which must be implemented by any derived class. */ public abstract boolean getBoolean(String key, boolean defaultValue); public abstract void putBoolean(String key, boolean value); public abstract int getInt(String key, int defaultValue); public abstract void putInt(String key, int value); public abstract double getDouble(String key, double defaultValue); public abstract void putDouble(String key, double value); public abstract String getString(String key, String defaultValue); public abstract void putString(String key, String value); /** * Directory represents a way to collect multiple keys together. Implementors may * choose to concatenate the directory with the key using some special character. * @param directory * @param key * @param defaultValue * @return */ public abstract String getString(String directory, String key, String defaultValue); public abstract void putString(String directory, String key, String value); public abstract java.util.prefs.Preferences getNode(String nodeName); /* * ****************************************************************************************** */ public final boolean getCheckUpdates() { return this.getBoolean(CHECK_UPDATES, BuildProperties.getDefaultCheckUpdates()); } public final void setCheckUpdates(boolean check) { this.putBoolean(CHECK_UPDATES, check); } public final boolean getConfirmSimDeletion() { return this.getBoolean(CONFIRM_DELETE_SIMULATION, true); } public final void setConfirmSimDeletion(boolean check) { this.putBoolean(CONFIRM_DELETE_SIMULATION, check); } public final boolean getAutoRunSimulations() { return this.getBoolean(AUTO_RUN_SIMULATIONS, false); } public final void setAutoRunSimulations(boolean check) { this.putBoolean(AUTO_RUN_SIMULATIONS, check); } public final boolean getLaunchIntoWind() { return this.getBoolean(LAUNCH_INTO_WIND, false); } public final void setLaunchIntoWind(boolean check) { this.putBoolean(LAUNCH_INTO_WIND, check); } public final boolean getShowRockSimFormatWarning() { return this.getBoolean(SHOW_ROCKSIM_FORMAT_WARNING, true); } public final void setShowRockSimFormatWarning(boolean check) { this.putBoolean(SHOW_ROCKSIM_FORMAT_WARNING, check); } public final double getDefaultMach() { return Application.getPreferences().getChoice(Preferences.DEFAULT_MACH_NUMBER, 0.9, 0.3); } public final void setDefaultMach(double dfn) { double oldDFN = Application.getPreferences().getChoice(Preferences.DEFAULT_MACH_NUMBER, 0.9, 0.3); if (MathUtil.equals(oldDFN, dfn)) return; this.putDouble(Preferences.DEFAULT_MACH_NUMBER, dfn); fireChangeEvent(); } public final double getWindTurbulenceIntensity() { return Application.getPreferences().getChoice(Preferences.WIND_TURBULANCE, 0.9, 0.1); } public final void setWindTurbulenceIntensity(double wti) { double oldWTI = Application.getPreferences().getChoice(Preferences.WIND_TURBULANCE, 0.9, 0.3); if (MathUtil.equals(oldWTI, wti)) return; this.putDouble(Preferences.WIND_TURBULANCE, wti); fireChangeEvent(); } public double getLaunchRodLength() { return this.getDouble(LAUNCH_ROD_LENGTH, 1); } public void setLaunchRodLength(double launchRodLength) { if (MathUtil.equals(this.getDouble(LAUNCH_ROD_LENGTH, 1), launchRodLength)) return; this.putDouble(LAUNCH_ROD_LENGTH, launchRodLength); fireChangeEvent(); } public double getLaunchRodAngle() { return this.getDouble(LAUNCH_ROD_ANGLE, 0); } public void setLaunchRodAngle(double launchRodAngle) { launchRodAngle = MathUtil.clamp(launchRodAngle, -Math.PI / 6.0, Math.PI / 6.0); if (MathUtil.equals(this.getDouble(LAUNCH_ROD_ANGLE, 0), launchRodAngle)) return; this.putDouble(LAUNCH_ROD_ANGLE, launchRodAngle); ; fireChangeEvent(); } public double getLaunchRodDirection() { if (this.getBoolean(LAUNCH_INTO_WIND, true)) { this.setLaunchRodDirection(this.getDouble(WIND_DIRECTION, Math.PI / 2)); } return this.getDouble(WIND_DIRECTION, Math.PI / 2); } public void setLaunchRodDirection(double launchRodDirection) { launchRodDirection = MathUtil.reduce360(launchRodDirection); if (MathUtil.equals(this.getDouble(LAUNCH_ROD_DIRECTION, Math.PI / 2.0), launchRodDirection)) return; this.putDouble(LAUNCH_ROD_DIRECTION, launchRodDirection); fireChangeEvent(); } public double getWindSpeedAverage() { return this.getDouble(WIND_AVERAGE, 2); } public void setWindSpeedAverage(double windAverage) { if (MathUtil.equals(this.getDouble(WIND_AVERAGE, 2), windAverage)) return; this.putDouble(WIND_AVERAGE, MathUtil.max(windAverage, 0)); fireChangeEvent(); } public double getWindSpeedDeviation() { return this.getDouble(WIND_AVERAGE, 2) * this.getDouble(WIND_TURBULANCE, .1); } 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.reduce360(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); } public double getLaunchAltitude() { return this.getDouble(LAUNCH_ALTITUDE, 0); } public void setLaunchAltitude(double altitude) { if (MathUtil.equals(this.getDouble(LAUNCH_ALTITUDE, 0), altitude)) return; this.putDouble(LAUNCH_ALTITUDE, altitude); fireChangeEvent(); } public double getLaunchLatitude() { return this.getDouble(LAUNCH_LATITUDE, 28.61); } public void setLaunchLatitude(double launchLatitude) { launchLatitude = MathUtil.clamp(launchLatitude, -90, 90); if (MathUtil.equals(this.getDouble(LAUNCH_LATITUDE, 28.61), launchLatitude)) return; this.putDouble(LAUNCH_LATITUDE, launchLatitude); fireChangeEvent(); } public double getLaunchLongitude() { return this.getDouble(LAUNCH_LONGITUDE, -80.60); } public void setLaunchLongitude(double launchLongitude) { launchLongitude = MathUtil.clamp(launchLongitude, -180, 180); if (MathUtil.equals(this.getDouble(LAUNCH_LONGITUDE, -80.60), launchLongitude)) return; this.putDouble(LAUNCH_LONGITUDE, launchLongitude); fireChangeEvent(); } /* public GeodeticComputationStrategy getGeodeticComputation() { return geodeticComputation; } public void setGeodeticComputation(GeodeticComputationStrategy geodeticComputation) { if (this.geodeticComputation == geodeticComputation) return; if (geodeticComputation == null) { throw new IllegalArgumentException("strategy cannot be null"); } this.geodeticComputation = geodeticComputation; fireChangeEvent(); } public boolean isISAAtmosphere() { return useISA; } public void setISAAtmosphere(boolean isa) { if (isa == useISA) return; useISA = isa; fireChangeEvent(); } */ public double getLaunchTemperature() { return this.getDouble(LAUNCH_TEMPERATURE, ExtendedISAModel.STANDARD_TEMPERATURE); } public void setLaunchTemperature(double launchTemperature) { if (MathUtil.equals(this.getDouble(LAUNCH_TEMPERATURE, ExtendedISAModel.STANDARD_TEMPERATURE), launchTemperature)) return; this.putDouble(LAUNCH_TEMPERATURE, launchTemperature); fireChangeEvent(); } public double getLaunchPressure() { return this.getDouble(LAUNCH_PRESSURE, ExtendedISAModel.STANDARD_PRESSURE); } public void setLaunchPressure(double launchPressure) { if (MathUtil.equals(this.getDouble(LAUNCH_PRESSURE, ExtendedISAModel.STANDARD_PRESSURE), launchPressure)) return; this.putDouble(LAUNCH_PRESSURE, launchPressure); fireChangeEvent(); } public boolean getISAAtmosphere() { return this.getBoolean(LAUNCH_USE_ISA, true); } public void setISAAtmosphere(boolean isa) { if (this.getBoolean(LAUNCH_USE_ISA, true) == isa) { return; } this.putBoolean(LAUNCH_USE_ISA, isa); fireChangeEvent(); } /** * Returns an atmospheric model corresponding to the launch conditions. The * atmospheric models may be shared between different calls. * * @return an AtmosphericModel object. */ public AtmosphericModel getAtmosphericModel() { if (this.getBoolean(LAUNCH_USE_ISA, true)) { return ISA_ATMOSPHERIC_MODEL; } return new ExtendedISAModel(getLaunchAltitude(), this.getDouble(LAUNCH_TEMPERATURE, ExtendedISAModel.STANDARD_TEMPERATURE), this.getDouble(LAUNCH_PRESSURE, ExtendedISAModel.STANDARD_PRESSURE)); } public GeodeticComputationStrategy getGeodeticComputation() { return this.getEnum(GEODETIC_COMPUTATION, GeodeticComputationStrategy.SPHERICAL); } public void setGeodeticComputation(GeodeticComputationStrategy gcs) { this.putEnum(GEODETIC_COMPUTATION, gcs); } public double getTimeStep() { return this.getDouble(Preferences.SIMULATION_TIME_STEP, RK4SimulationStepper.RECOMMENDED_TIME_STEP); } public void setTimeStep(double timeStep) { if (MathUtil.equals(this.getDouble(SIMULATION_TIME_STEP, RK4SimulationStepper.RECOMMENDED_TIME_STEP), timeStep)) return; this.putDouble(SIMULATION_TIME_STEP, timeStep); fireChangeEvent(); } public final float getRocketInfoFontSize() { return (float) (11.0 + 3 * Application.getPreferences().getChoice(Preferences.ROCKET_INFO_FONT_SIZE, 2, 0)); } /** * Enable/Disable the auto-opening of the last edited design file on startup. */ public final void setAutoOpenLastDesignOnStartup(boolean enabled) { this.putBoolean(AUTO_OPEN_LAST_DESIGN, enabled); } /** * Answer if the auto-opening of the last edited design file on startup is enabled. * * @return true if the application should automatically open the last edited design file on startup. */ public final boolean isAutoOpenLastDesignOnStartupEnabled() { return this.getBoolean(AUTO_OPEN_LAST_DESIGN, false); } /** * Return the OpenRocket unique ID. * * @return a random ID string that stays constant between OpenRocket executions */ public final String getUniqueID() { String id = this.getString("id", null); if (id == null) { id = UniqueID.uuid(); this.putString("id", id); } return id; } /** * Returns a limited-range integer value from the preferences. If the value * in the preferences is negative or greater than max, then the default value * is returned. * * @param key The preference to retrieve. * @param max Maximum allowed value for the choice. * @param def Default value. * @return The preference value. */ public final int getChoice(String key, int max, int def) { int v = this.getInt(key, def); if ((v < 0) || (v > max)) return def; return v; } /** * Returns a limited-range double value from the preferences. If the value * in the preferences is negative or greater than max, then the default value * is returned. * * @param key The preference to retrieve. * @param max Maximum allowed value for the choice. * @param def Default value. * @return The preference value. */ public final double getChoice(String key, double max, double def) { double v = this.getDouble(key, def); if ((v < 0) || (v > max)) return def; return v; } /** * Helper method that puts an integer choice value into the preferences. * * @param key the preference key. * @param value the value to store. */ public final void putChoice(String key, int value) { this.putInt(key, value); } /** * Retrieve an enum value from the user preferences. * * @param <T> the enum type * @param key the key * @param def the default value, cannot be null * @return the value in the preferences, or the default value */ public final <T extends Enum<T>> T getEnum(String key, T def) { if (def == null) { throw new BugException("Default value cannot be null"); } String value = getString(key, null); if (value == null) { return def; } try { return Enum.valueOf(def.getDeclaringClass(), value); } catch (IllegalArgumentException e) { return def; } } /** * Store an enum value to the user preferences. * * @param key the key * @param value the value to store, or null to remove the value */ public final void putEnum(String key, Enum<?> value) { if (value == null) { putString(key, null); } else { putString(key, value.name()); } } public Color getDefaultColor(Class<? extends RocketComponent> c) { String color = get("componentColors", c, StaticFieldHolder.DEFAULT_COLORS); if (color == null) return Color.BLACK; Color clr = parseColor(color); if (clr != null) { return clr; } else { return Color.BLACK; } } public final void setDefaultColor(Class<? extends RocketComponent> c, Color color) { if (color == null) return; putString("componentColors", c.getSimpleName(), stringifyColor(color)); } /** * Retrieve a Line style for the given component. * @param c * @return */ public final LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) { String value = get("componentStyle", c, StaticFieldHolder.DEFAULT_LINE_STYLES); try { return LineStyle.valueOf(value); } catch (Exception e) { return LineStyle.SOLID; } } /** * Set a default line style for the given component. * @param c * @param style */ public final void setDefaultLineStyle(Class<? extends RocketComponent> c, LineStyle style) { if (style == null) return; putString("componentStyle", c.getSimpleName(), style.name()); } /** * Get the default material type for the given component. * @param componentClass * @param type the Material.Type to return. * @return */ public Material getDefaultComponentMaterial( Class<? extends RocketComponent> componentClass, Material.Type type) { String material = get("componentMaterials", componentClass, null); if (material != null) { try { Material m = Material.fromStorableString(material, false); if (m.getType() == type) return m; } catch (IllegalArgumentException ignore) { } } switch (type) { case LINE: return StaticFieldHolder.DEFAULT_LINE_MATERIAL; case SURFACE: return StaticFieldHolder.DEFAULT_SURFACE_MATERIAL; case BULK: return StaticFieldHolder.DEFAULT_BULK_MATERIAL; } throw new IllegalArgumentException("Unknown material type: " + type); } /** * Set the default material for a component type. * @param componentClass * @param material */ public void setDefaultComponentMaterial( Class<? extends RocketComponent> componentClass, Material material) { putString("componentMaterials", componentClass.getSimpleName(), material == null ? null : material.toStorableString()); } /** * get a net.sf.openrocket.util.Color object for the given key. * @param key * @param defaultValue * @return */ public final Color getColor(String key, Color defaultValue) { Color c = parseColor(getString(key, null)); if (c == null) { return defaultValue; } return c; } /** * set a net.sf.openrocket.util.Color preference value for the given key. * @param key * @param value */ public final void putColor(String key, Color value) { putString(key, stringifyColor(value)); } /** * Helper function to convert a string representation into a net.sf.openrocket.util.Color object. * @param color * @return */ protected static Color parseColor(String color) { if (color == null) { return null; } String[] rgb = color.split(","); if (rgb.length == 3) { try { int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255); int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255); int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255); return new Color(red, green, blue); } catch (NumberFormatException ignore) { } } return null; } /** * Helper function to convert a net.sf.openrocket.util.Color object into a * String before storing in a preference. * @param color * @return */ protected static String stringifyColor(Color color) { String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue(); return string; } /** * Special helper function which allows for a map of default values. * * First getString(directory,componentClass.getSimpleName(), null) is invoked, * if the returned value is null, the defaultMap is consulted for a value. * * @param directory * @param componentClass * @param defaultMap * @return */ protected String get(String directory, Class<? extends RocketComponent> componentClass, Map<Class<?>, String> defaultMap) { // Search preferences Class<?> c = componentClass; while (c != null && RocketComponent.class.isAssignableFrom(c)) { String value = this.getString(directory, c.getSimpleName(), null); if (value != null) return value; c = c.getSuperclass(); } if (defaultMap == null) return null; // Search defaults c = componentClass; while (RocketComponent.class.isAssignableFrom(c)) { String value = defaultMap.get(c); if (value != null) return value; c = c.getSuperclass(); } return null; } public abstract void addUserMaterial(Material m); public abstract Set<Material> getUserMaterials(); public abstract void removeUserMaterial(Material m); public abstract void setComponentFavorite(ComponentPreset preset, ComponentPreset.Type type, boolean favorite); public abstract Set<String> getComponentFavorites(ComponentPreset.Type type); /* * Within a holder class so they will load only when needed. */ private static class StaticFieldHolder { private static final Material DEFAULT_LINE_MATERIAL = Databases.findMaterial(Material.Type.LINE, "Elastic cord (round 2 mm, 1/16 in)"); private static final Material DEFAULT_SURFACE_MATERIAL = Databases.findMaterial(Material.Type.SURFACE, "Ripstop nylon"); private static final Material DEFAULT_BULK_MATERIAL = Databases.findMaterial(Material.Type.BULK, "Cardboard"); /* * Map of default line styles */ private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES = new HashMap<Class<?>, String>(); static { DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name()); DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name()); } private static final HashMap<Class<?>, String> DEFAULT_COLORS = new HashMap<Class<?>, String>(); static { DEFAULT_COLORS.put(BodyComponent.class, "0,0,240"); DEFAULT_COLORS.put(TubeFinSet.class, "0,0,200"); DEFAULT_COLORS.put(FinSet.class, "0,0,200"); DEFAULT_COLORS.put(LaunchLug.class, "0,0,180"); DEFAULT_COLORS.put(InternalComponent.class, "170,0,100"); DEFAULT_COLORS.put(MassObject.class, "0,0,0"); DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0"); } } private List<EventListener> listeners = new ArrayList<EventListener>(); private final EventObject event = new EventObject(this); @Override public void addChangeListener(StateChangeListener listener) { listeners.add(listener); } @Override public void removeChangeListener(StateChangeListener listener) { listeners.remove(listener); } private void fireChangeEvent() { // 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); } } } }