1 package net.sf.openrocket.util;
4 import java.awt.Dimension;
6 import java.awt.Toolkit;
8 import java.io.IOException;
9 import java.io.InputStream;
10 import java.util.ArrayList;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
15 import java.util.MissingResourceException;
16 import java.util.Properties;
18 import java.util.prefs.BackingStoreException;
19 import java.util.prefs.Preferences;
21 import net.sf.openrocket.arch.SystemInfo;
22 import net.sf.openrocket.database.Databases;
23 import net.sf.openrocket.document.Simulation;
24 import net.sf.openrocket.gui.main.ExceptionHandler;
25 import net.sf.openrocket.l10n.Translator;
26 import net.sf.openrocket.logging.LogHelper;
27 import net.sf.openrocket.material.Material;
28 import net.sf.openrocket.rocketcomponent.BodyComponent;
29 import net.sf.openrocket.rocketcomponent.FinSet;
30 import net.sf.openrocket.rocketcomponent.InternalComponent;
31 import net.sf.openrocket.rocketcomponent.LaunchLug;
32 import net.sf.openrocket.rocketcomponent.MassObject;
33 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
34 import net.sf.openrocket.rocketcomponent.Rocket;
35 import net.sf.openrocket.rocketcomponent.RocketComponent;
36 import net.sf.openrocket.simulation.FlightDataType;
37 import net.sf.openrocket.simulation.GUISimulationConditions;
38 import net.sf.openrocket.simulation.RK4SimulationStepper;
39 import net.sf.openrocket.startup.Application;
40 import net.sf.openrocket.unit.UnitGroup;
44 private static final LogHelper log = Application.getLogger();
46 private static final String SPLIT_CHARACTER = "|";
50 * Whether to use the debug-node instead of the normal node.
52 private static final boolean DEBUG;
54 DEBUG = (System.getProperty("openrocket.debug.prefs") != null);
58 * Whether to clear all preferences at application startup. This has an effect only
61 private static final boolean CLEARPREFS = true;
64 * The node name to use in the Java preferences storage.
66 private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket");
70 * Load property file only when necessary.
72 private static class BuildPropertyHolder {
74 public static final Properties PROPERTIES;
75 public static final String BUILD_VERSION;
76 public static final String BUILD_SOURCE;
77 public static final boolean DEFAULT_CHECK_UPDATES;
81 InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
83 throw new MissingResourceException(
84 "build.properties not found, distribution built wrong" +
85 " classpath:" + System.getProperty("java.class.path"),
86 "build.properties", "build.version");
89 PROPERTIES = new Properties();
93 String version = PROPERTIES.getProperty("build.version");
94 if (version == null) {
95 throw new MissingResourceException(
96 "build.version not found in property file",
97 "build.properties", "build.version");
99 BUILD_VERSION = version.trim();
101 BUILD_SOURCE = PROPERTIES.getProperty("build.source");
102 if (BUILD_SOURCE == null) {
103 throw new MissingResourceException(
104 "build.source not found in property file",
105 "build.properties", "build.source");
108 String value = PROPERTIES.getProperty("build.checkupdates");
110 DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
112 DEFAULT_CHECK_UPDATES = true;
114 } catch (IOException e) {
115 throw new MissingResourceException(
116 "Error reading build.properties",
117 "build.properties", "build.version");
122 public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
124 public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
126 public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
128 // Preferences related to data export
129 public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator";
130 public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment";
131 public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment";
132 public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments";
133 public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
135 public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
137 private static final String CHECK_UPDATES = "CheckUpdates";
138 public static final String LAST_UPDATE = "LastUpdateVersion";
140 public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch";
141 public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar";
145 public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
149 * Node to this application's preferences.
150 * @deprecated Use the static methods instead.
153 public static final Preferences NODE;
154 private static final Preferences PREFNODE;
157 // Clear the preferences if debug mode and clearprefs is defined
159 Preferences root = Preferences.userRoot();
160 if (DEBUG && CLEARPREFS) {
162 if (root.nodeExists(NODENAME)) {
163 root.node(NODENAME).removeNode();
165 } catch (BackingStoreException e) {
166 throw new BugException("Unable to clear preference node", e);
169 PREFNODE = root.node(NODENAME);
176 ///////// Default component attributes
178 private static final HashMap<Class<?>, String> DEFAULT_COLORS =
179 new HashMap<Class<?>, String>();
181 DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
182 DEFAULT_COLORS.put(FinSet.class, "0,0,200");
183 DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
184 DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
185 DEFAULT_COLORS.put(MassObject.class, "0,0,0");
186 DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
190 private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES =
191 new HashMap<Class<?>, String>();
193 DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
194 DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
199 * Within a holder class so they will load only when needed.
201 private static class DefaultMaterialHolder {
202 private static final Translator trans = Application.getTranslator();
204 //// Elastic cord (round 2mm, 1/16 in)
205 private static final Material DEFAULT_LINE_MATERIAL =
206 Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"),
209 private static final Material DEFAULT_SURFACE_MATERIAL =
210 Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false);
212 private static final Material DEFAULT_BULK_MATERIAL =
213 Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false);
216 //////////////////////
220 * Return the OpenRocket version number.
222 public static String getVersion() {
223 return BuildPropertyHolder.BUILD_VERSION;
228 * Return the OpenRocket build source (e.g. "default" or "Debian")
230 public static String getBuildSource() {
231 return BuildPropertyHolder.BUILD_SOURCE;
236 * Return the OpenRocket unique ID.
238 * @return a random ID string that stays constant between OpenRocket executions
240 public static String getUniqueID() {
241 String id = PREFNODE.get("id", null);
243 id = UniqueID.uuid();
244 PREFNODE.put("id", id);
252 * Store the current OpenRocket version into the preferences to allow for preferences migration.
254 private static void storeVersion() {
255 PREFNODE.put("OpenRocketVersion", getVersion());
260 * Returns a limited-range integer value from the preferences. If the value
261 * in the preferences is negative or greater than max, then the default value
264 * @param key The preference to retrieve.
265 * @param max Maximum allowed value for the choice.
266 * @param def Default value.
267 * @return The preference value.
269 public static int getChoise(String key, int max, int def) {
270 int v = PREFNODE.getInt(key, def);
271 if ((v < 0) || (v > max))
278 * Helper method that puts an integer choice value into the preferences.
280 * @param key the preference key.
281 * @param value the value to store.
283 public static void putChoise(String key, int value) {
284 PREFNODE.putInt(key, value);
290 * Return a string preference.
292 * @param key the preference key.
293 * @param def the default if no preference is stored
294 * @return the preference value
296 public static String getString(String key, String def) {
297 return PREFNODE.get(key, def);
301 * Set a string preference.
303 * @param key the preference key
304 * @param value the value to set, or <code>null</code> to remove the key
306 public static void putString(String key, String value) {
308 PREFNODE.remove(key);
311 PREFNODE.put(key, value);
316 * Return a boolean preference.
318 * @param key the preference key
319 * @param def the default if no preference is stored
320 * @return the preference value
322 public static boolean getBoolean(String key, boolean def) {
323 return PREFNODE.getBoolean(key, def);
327 * Set a boolean preference.
329 * @param key the preference key
330 * @param value the value to set
332 public static void putBoolean(String key, boolean value) {
333 PREFNODE.putBoolean(key, value);
339 * Return a preferences object for the specified node name.
341 * @param nodeName the node name
342 * @return the preferences object for that node
344 public static Preferences getNode(String nodeName) {
345 return PREFNODE.node(nodeName);
354 public static boolean getCheckUpdates() {
355 return PREFNODE.getBoolean(CHECK_UPDATES, BuildPropertyHolder.DEFAULT_CHECK_UPDATES);
358 public static void setCheckUpdates(boolean check) {
359 PREFNODE.putBoolean(CHECK_UPDATES, check);
363 public static File getDefaultDirectory() {
364 String file = PREFNODE.get("defaultDirectory", null);
367 return new File(file);
370 public static void setDefaultDirectory(File dir) {
375 d = dir.getAbsolutePath();
377 PREFNODE.put("defaultDirectory", d);
383 * Return a list of files/directories to be loaded as custom thrust curves.
385 * If this property has not been set, the directory "ThrustCurves" in the user
386 * application directory will be used. The directory will be created if it does not
389 * @return a list of files to load as thrust curves.
391 public static List<File> getUserThrustCurveFiles() {
392 List<File> list = new ArrayList<File>();
394 String files = getString(USER_THRUST_CURVES_KEY, null);
396 // Default to application directory
397 File tcdir = getDefaultUserThrustCurveFile();
398 if (!tcdir.isDirectory()) {
403 for (String file : files.split("\\" + SPLIT_CHARACTER)) {
405 if (file.length() > 0) {
406 list.add(new File(file));
414 public static File getDefaultUserThrustCurveFile() {
415 File appdir = SystemInfo.getUserApplicationDirectory();
416 File tcdir = new File(appdir, "ThrustCurves");
422 * Set the list of files/directories to be loaded as custom thrust curves.
424 * @param files the files to load, or <code>null</code> to reset to default value.
426 public static void setUserThrustCurveFiles(List<File> files) {
428 putString(USER_THRUST_CURVES_KEY, null);
434 for (File file : files) {
435 if (str.length() > 0) {
436 str += SPLIT_CHARACTER;
438 str += file.getAbsolutePath();
440 putString(USER_THRUST_CURVES_KEY, str);
447 public static Color getDefaultColor(Class<? extends RocketComponent> c) {
448 String color = get("componentColors", c, DEFAULT_COLORS);
452 String[] rgb = color.split(",");
453 if (rgb.length == 3) {
455 int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255);
456 int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255);
457 int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255);
458 return new Color(red, green, blue);
459 } catch (NumberFormatException ignore) {
466 public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
469 String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
470 set("componentColors", c, string);
473 public static Color getMotorBorderColor() {
474 // TODO: MEDIUM: Motor color (settable?)
475 return new Color(0, 0, 0, 200);
479 public static Color getMotorFillColor() {
480 // TODO: MEDIUM: Motor fill color (settable?)
481 return new Color(0, 0, 0, 100);
485 public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
486 String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
488 return LineStyle.valueOf(value);
489 } catch (Exception e) {
490 return LineStyle.SOLID;
494 public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
498 set("componentStyle", c, style.name());
503 * Return the DPI setting of the monitor. This is either the setting provided
504 * by the system or a user-specified DPI setting.
506 * @return the DPI setting to use.
508 public static double getDPI() {
509 int dpi = PREFNODE.getInt("DPI", 0); // Tenths of a dpi
512 dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
521 public static double getDefaultMach() {
522 // TODO: HIGH: implement custom default mach number
529 public static Material getDefaultComponentMaterial(
530 Class<? extends RocketComponent> componentClass,
531 Material.Type type) {
533 String material = get("componentMaterials", componentClass, null);
534 if (material != null) {
536 Material m = Material.fromStorableString(material, false);
537 if (m.getType() == type)
539 } catch (IllegalArgumentException ignore) {
545 return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL;
547 return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL;
549 return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL;
551 throw new IllegalArgumentException("Unknown material type: " + type);
554 public static void setDefaultComponentMaterial(
555 Class<? extends RocketComponent> componentClass, Material material) {
557 set("componentMaterials", componentClass,
558 material == null ? null : material.toStorableString());
562 public static int getMaxThreadCount() {
563 return Runtime.getRuntime().availableProcessors();
568 * Return whether to use additional safety code checks.
570 public static boolean useSafetyChecks() {
571 // Currently default to false unless openrocket.debug.safetycheck is defined
572 String s = System.getProperty("openrocket.debug.safetycheck");
573 if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) {
580 public static Point getWindowPosition(Class<?> c) {
582 String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null);
587 if (pref.indexOf(',') < 0)
591 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
592 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
593 } catch (NumberFormatException e) {
596 return new Point(x, y);
599 public static void setWindowPosition(Class<?> c, Point p) {
600 PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
607 public static Dimension getWindowSize(Class<?> c) {
609 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
614 if (pref.indexOf(',') < 0)
618 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
619 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
620 } catch (NumberFormatException e) {
623 return new Dimension(x, y);
626 public static void setWindowSize(Class<?> c, Dimension d) {
627 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
632 //// Background flight data computation
634 public static boolean computeFlightInBackground() {
635 return PREFNODE.getBoolean("backgroundFlight", true);
638 public static Simulation getBackgroundSimulation(Rocket rocket) {
639 Simulation s = new Simulation(rocket);
640 GUISimulationConditions cond = s.getConditions();
642 cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2);
643 cond.setWindSpeedAverage(1.0);
644 cond.setWindSpeedDeviation(0.1);
645 cond.setLaunchRodLength(5);
651 ///////// Export variables
653 public static boolean isExportSelected(FlightDataType type) {
654 Preferences prefs = PREFNODE.node("exports");
655 return prefs.getBoolean(type.getName(), false);
658 public static void setExportSelected(FlightDataType type, boolean selected) {
659 Preferences prefs = PREFNODE.node("exports");
660 prefs.putBoolean(type.getName(), selected);
665 ///////// Default unit storage
667 public static void loadDefaultUnits() {
668 Preferences prefs = PREFNODE.node("units");
671 for (String key : prefs.keys()) {
672 UnitGroup group = UnitGroup.UNITS.get(key);
677 group.setDefaultUnit(prefs.get(key, null));
678 } catch (IllegalArgumentException ignore) {
682 } catch (BackingStoreException e) {
683 ExceptionHandler.handleErrorCondition(e);
687 public static void storeDefaultUnits() {
688 Preferences prefs = PREFNODE.node("units");
690 for (String key : UnitGroup.UNITS.keySet()) {
691 UnitGroup group = UnitGroup.UNITS.get(key);
692 if (group == null || group.getUnitCount() < 2)
695 prefs.put(key, group.getDefaultUnit().getUnit());
701 //// Material storage
705 * Add a user-defined material to the preferences. The preferences are
706 * first checked for an existing material matching the provided one using
707 * {@link Material#equals(Object)}.
709 * @param m the material to add.
711 public static void addUserMaterial(Material m) {
712 Preferences prefs = PREFNODE.node("userMaterials");
715 // Check whether material already exists
716 if (getUserMaterials().contains(m)) {
720 // Add material using next free key (key is not used when loading)
721 String mat = m.toStorableString();
722 for (int i = 0;; i++) {
723 String key = "material" + i;
724 if (prefs.get(key, null) == null) {
733 * Remove a user-defined material from the preferences. The matching is performed
734 * using {@link Material#equals(Object)}.
736 * @param m the material to remove.
738 public static void removeUserMaterial(Material m) {
739 Preferences prefs = PREFNODE.node("userMaterials");
743 // Iterate through materials and remove all keys with a matching material
744 for (String key : prefs.keys()) {
745 String value = prefs.get(key, null);
748 Material existing = Material.fromStorableString(value, true);
749 if (existing.equals(m)) {
753 } catch (IllegalArgumentException ignore) {
758 } catch (BackingStoreException e) {
759 throw new IllegalStateException("Cannot read preferences!", e);
765 * Return a set of all user-defined materials in the preferences. The materials
766 * are created marked as user-defined.
768 * @return a set of all user-defined materials.
770 public static Set<Material> getUserMaterials() {
771 Preferences prefs = PREFNODE.node("userMaterials");
773 HashSet<Material> materials = new HashSet<Material>();
776 for (String key : prefs.keys()) {
777 String value = prefs.get(key, null);
780 Material m = Material.fromStorableString(value, true);
783 } catch (IllegalArgumentException e) {
784 log.warn("Illegal material string " + value);
789 } catch (BackingStoreException e) {
790 throw new IllegalStateException("Cannot read preferences!", e);
799 private static String get(String directory,
800 Class<? extends RocketComponent> componentClass,
801 Map<Class<?>, String> defaultMap) {
803 // Search preferences
804 Class<?> c = componentClass;
805 Preferences prefs = PREFNODE.node(directory);
806 while (c != null && RocketComponent.class.isAssignableFrom(c)) {
807 String value = prefs.get(c.getSimpleName(), null);
810 c = c.getSuperclass();
813 if (defaultMap == null)
818 while (RocketComponent.class.isAssignableFrom(c)) {
819 String value = defaultMap.get(c);
822 c = c.getSuperclass();
829 private static void set(String directory, Class<? extends RocketComponent> componentClass,
831 Preferences prefs = PREFNODE.node(directory);
833 prefs.remove(componentClass.getSimpleName());
835 prefs.put(componentClass.getSimpleName(), value);