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.logging.LogHelper;
26 import net.sf.openrocket.material.Material;
27 import net.sf.openrocket.rocketcomponent.BodyComponent;
28 import net.sf.openrocket.rocketcomponent.FinSet;
29 import net.sf.openrocket.rocketcomponent.InternalComponent;
30 import net.sf.openrocket.rocketcomponent.LaunchLug;
31 import net.sf.openrocket.rocketcomponent.MassObject;
32 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
33 import net.sf.openrocket.rocketcomponent.Rocket;
34 import net.sf.openrocket.rocketcomponent.RocketComponent;
35 import net.sf.openrocket.simulation.FlightDataType;
36 import net.sf.openrocket.simulation.GUISimulationConditions;
37 import net.sf.openrocket.simulation.RK4SimulationStepper;
38 import net.sf.openrocket.startup.Application;
39 import net.sf.openrocket.unit.UnitGroup;
43 private static final LogHelper log = Application.getLogger();
45 private static final String SPLIT_CHARACTER = "|";
49 * Whether to use the debug-node instead of the normal node.
51 private static final boolean DEBUG;
53 DEBUG = (System.getProperty("openrocket.debug.prefs") != null);
57 * Whether to clear all preferences at application startup. This has an effect only
60 private static final boolean CLEARPREFS = true;
63 * The node name to use in the Java preferences storage.
65 private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket");
69 * Load property file only when necessary.
71 private static class BuildPropertyHolder {
73 public static final Properties PROPERTIES;
74 public static final String BUILD_VERSION;
75 public static final String BUILD_SOURCE;
76 public static final boolean DEFAULT_CHECK_UPDATES;
80 InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
82 throw new MissingResourceException(
83 "build.properties not found, distribution built wrong" +
84 " classpath:" + System.getProperty("java.class.path"),
85 "build.properties", "build.version");
88 PROPERTIES = new Properties();
92 String version = PROPERTIES.getProperty("build.version");
93 if (version == null) {
94 throw new MissingResourceException(
95 "build.version not found in property file",
96 "build.properties", "build.version");
98 BUILD_VERSION = version.trim();
100 BUILD_SOURCE = PROPERTIES.getProperty("build.source");
101 if (BUILD_SOURCE == null) {
102 throw new MissingResourceException(
103 "build.source not found in property file",
104 "build.properties", "build.source");
107 String value = PROPERTIES.getProperty("build.checkupdates");
109 DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
111 DEFAULT_CHECK_UPDATES = true;
113 } catch (IOException e) {
114 throw new MissingResourceException(
115 "Error reading build.properties",
116 "build.properties", "build.version");
121 public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
123 public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
125 public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
127 // Preferences related to data export
128 public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator";
129 public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment";
130 public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment";
131 public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments";
132 public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
134 public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
136 private static final String CHECK_UPDATES = "CheckUpdates";
137 public static final String LAST_UPDATE = "LastUpdateVersion";
139 public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch";
140 public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar";
144 public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
148 * Node to this application's preferences.
149 * @deprecated Use the static methods instead.
152 public static final Preferences NODE;
153 private static final Preferences PREFNODE;
156 // Clear the preferences if debug mode and clearprefs is defined
158 Preferences root = Preferences.userRoot();
159 if (DEBUG && CLEARPREFS) {
161 if (root.nodeExists(NODENAME)) {
162 root.node(NODENAME).removeNode();
164 } catch (BackingStoreException e) {
165 throw new BugException("Unable to clear preference node", e);
168 PREFNODE = root.node(NODENAME);
175 ///////// Default component attributes
177 private static final HashMap<Class<?>, String> DEFAULT_COLORS =
178 new HashMap<Class<?>, String>();
180 DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
181 DEFAULT_COLORS.put(FinSet.class, "0,0,200");
182 DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
183 DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
184 DEFAULT_COLORS.put(MassObject.class, "0,0,0");
185 DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
189 private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES =
190 new HashMap<Class<?>, String>();
192 DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
193 DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
198 * Within a holder class so they will load only when needed.
200 private static class DefaultMaterialHolder {
201 private static final Material DEFAULT_LINE_MATERIAL =
202 Databases.findMaterial(Material.Type.LINE, "Elastic cord (round 2mm, 1/16 in)",
204 private static final Material DEFAULT_SURFACE_MATERIAL =
205 Databases.findMaterial(Material.Type.SURFACE, "Ripstop nylon", 0.067, false);
206 private static final Material DEFAULT_BULK_MATERIAL =
207 Databases.findMaterial(Material.Type.BULK, "Cardboard", 680, false);
210 //////////////////////
214 * Return the OpenRocket version number.
216 public static String getVersion() {
217 return BuildPropertyHolder.BUILD_VERSION;
222 * Return the OpenRocket build source (e.g. "default" or "Debian")
224 public static String getBuildSource() {
225 return BuildPropertyHolder.BUILD_SOURCE;
230 * Return the OpenRocket unique ID.
232 * @return a random ID string that stays constant between OpenRocket executions
234 public static String getUniqueID() {
235 String id = PREFNODE.get("id", null);
237 id = UniqueID.uuid();
238 PREFNODE.put("id", id);
246 * Store the current OpenRocket version into the preferences to allow for preferences migration.
248 private static void storeVersion() {
249 PREFNODE.put("OpenRocketVersion", getVersion());
254 * Returns a limited-range integer value from the preferences. If the value
255 * in the preferences is negative or greater than max, then the default value
258 * @param key The preference to retrieve.
259 * @param max Maximum allowed value for the choice.
260 * @param def Default value.
261 * @return The preference value.
263 public static int getChoise(String key, int max, int def) {
264 int v = PREFNODE.getInt(key, def);
265 if ((v < 0) || (v > max))
272 * Helper method that puts an integer choice value into the preferences.
274 * @param key the preference key.
275 * @param value the value to store.
277 public static void putChoise(String key, int value) {
278 PREFNODE.putInt(key, value);
284 * Return a string preference.
286 * @param key the preference key.
287 * @param def the default if no preference is stored
288 * @return the preference value
290 public static String getString(String key, String def) {
291 return PREFNODE.get(key, def);
295 * Set a string preference.
297 * @param key the preference key
298 * @param value the value to set, or <code>null</code> to remove the key
300 public static void putString(String key, String value) {
302 PREFNODE.remove(key);
305 PREFNODE.put(key, value);
310 * Return a boolean preference.
312 * @param key the preference key
313 * @param def the default if no preference is stored
314 * @return the preference value
316 public static boolean getBoolean(String key, boolean def) {
317 return PREFNODE.getBoolean(key, def);
321 * Set a boolean preference.
323 * @param key the preference key
324 * @param value the value to set
326 public static void putBoolean(String key, boolean value) {
327 PREFNODE.putBoolean(key, value);
333 * Return a preferences object for the specified node name.
335 * @param nodeName the node name
336 * @return the preferences object for that node
338 public static Preferences getNode(String nodeName) {
339 return PREFNODE.node(nodeName);
348 public static boolean getCheckUpdates() {
349 return PREFNODE.getBoolean(CHECK_UPDATES, BuildPropertyHolder.DEFAULT_CHECK_UPDATES);
352 public static void setCheckUpdates(boolean check) {
353 PREFNODE.putBoolean(CHECK_UPDATES, check);
357 public static File getDefaultDirectory() {
358 String file = PREFNODE.get("defaultDirectory", null);
361 return new File(file);
364 public static void setDefaultDirectory(File dir) {
369 d = dir.getAbsolutePath();
371 PREFNODE.put("defaultDirectory", d);
377 * Return a list of files/directories to be loaded as custom thrust curves.
379 * If this property has not been set, the directory "ThrustCurves" in the user
380 * application directory will be used. The directory will be created if it does not
383 * @return a list of files to load as thrust curves.
385 public static List<File> getUserThrustCurveFiles() {
386 List<File> list = new ArrayList<File>();
388 String files = getString(USER_THRUST_CURVES_KEY, null);
390 // Default to application directory
391 File tcdir = getDefaultUserThrustCurveFile();
392 if (!tcdir.isDirectory()) {
397 for (String file : files.split("\\" + SPLIT_CHARACTER)) {
399 if (file.length() > 0) {
400 list.add(new File(file));
408 public static File getDefaultUserThrustCurveFile() {
409 File appdir = SystemInfo.getUserApplicationDirectory();
410 File tcdir = new File(appdir, "ThrustCurves");
416 * Set the list of files/directories to be loaded as custom thrust curves.
418 * @param files the files to load, or <code>null</code> to reset to default value.
420 public static void setUserThrustCurveFiles(List<File> files) {
422 putString(USER_THRUST_CURVES_KEY, null);
428 for (File file : files) {
429 if (str.length() > 0) {
430 str += SPLIT_CHARACTER;
432 str += file.getAbsolutePath();
434 putString(USER_THRUST_CURVES_KEY, str);
441 public static Color getDefaultColor(Class<? extends RocketComponent> c) {
442 String color = get("componentColors", c, DEFAULT_COLORS);
446 String[] rgb = color.split(",");
447 if (rgb.length == 3) {
449 int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255);
450 int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255);
451 int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255);
452 return new Color(red, green, blue);
453 } catch (NumberFormatException ignore) {
460 public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
463 String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
464 set("componentColors", c, string);
467 public static Color getMotorBorderColor() {
468 // TODO: MEDIUM: Motor color (settable?)
469 return new Color(0, 0, 0, 200);
473 public static Color getMotorFillColor() {
474 // TODO: MEDIUM: Motor fill color (settable?)
475 return new Color(0, 0, 0, 100);
479 public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
480 String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
482 return LineStyle.valueOf(value);
483 } catch (Exception e) {
484 return LineStyle.SOLID;
488 public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
492 set("componentStyle", c, style.name());
497 * Return the DPI setting of the monitor. This is either the setting provided
498 * by the system or a user-specified DPI setting.
500 * @return the DPI setting to use.
502 public static double getDPI() {
503 int dpi = PREFNODE.getInt("DPI", 0); // Tenths of a dpi
506 dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
515 public static double getDefaultMach() {
516 // TODO: HIGH: implement custom default mach number
523 public static Material getDefaultComponentMaterial(
524 Class<? extends RocketComponent> componentClass,
525 Material.Type type) {
527 String material = get("componentMaterials", componentClass, null);
528 if (material != null) {
530 Material m = Material.fromStorableString(material, false);
531 if (m.getType() == type)
533 } catch (IllegalArgumentException ignore) {
539 return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL;
541 return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL;
543 return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL;
545 throw new IllegalArgumentException("Unknown material type: " + type);
548 public static void setDefaultComponentMaterial(
549 Class<? extends RocketComponent> componentClass, Material material) {
551 set("componentMaterials", componentClass,
552 material == null ? null : material.toStorableString());
556 public static int getMaxThreadCount() {
557 return Runtime.getRuntime().availableProcessors();
562 * Return whether to use additional safety code checks.
564 public static boolean useSafetyChecks() {
565 // Currently default to false unless openrocket.debug.safetycheck is defined
566 String s = System.getProperty("openrocket.debug.safetycheck");
567 if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) {
574 public static Point getWindowPosition(Class<?> c) {
576 String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null);
581 if (pref.indexOf(',') < 0)
585 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
586 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
587 } catch (NumberFormatException e) {
590 return new Point(x, y);
593 public static void setWindowPosition(Class<?> c, Point p) {
594 PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
601 public static Dimension getWindowSize(Class<?> c) {
603 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
608 if (pref.indexOf(',') < 0)
612 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
613 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
614 } catch (NumberFormatException e) {
617 return new Dimension(x, y);
620 public static void setWindowSize(Class<?> c, Dimension d) {
621 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
626 //// Background flight data computation
628 public static boolean computeFlightInBackground() {
629 return PREFNODE.getBoolean("backgroundFlight", true);
632 public static Simulation getBackgroundSimulation(Rocket rocket) {
633 Simulation s = new Simulation(rocket);
634 GUISimulationConditions cond = s.getConditions();
636 cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2);
637 cond.setWindSpeedAverage(1.0);
638 cond.setWindSpeedDeviation(0.1);
639 cond.setLaunchRodLength(5);
645 ///////// Export variables
647 public static boolean isExportSelected(FlightDataType type) {
648 Preferences prefs = PREFNODE.node("exports");
649 return prefs.getBoolean(type.getName(), false);
652 public static void setExportSelected(FlightDataType type, boolean selected) {
653 Preferences prefs = PREFNODE.node("exports");
654 prefs.putBoolean(type.getName(), selected);
659 ///////// Default unit storage
661 public static void loadDefaultUnits() {
662 Preferences prefs = PREFNODE.node("units");
665 for (String key : prefs.keys()) {
666 UnitGroup group = UnitGroup.UNITS.get(key);
671 group.setDefaultUnit(prefs.get(key, null));
672 } catch (IllegalArgumentException ignore) {
676 } catch (BackingStoreException e) {
677 ExceptionHandler.handleErrorCondition(e);
681 public static void storeDefaultUnits() {
682 Preferences prefs = PREFNODE.node("units");
684 for (String key : UnitGroup.UNITS.keySet()) {
685 UnitGroup group = UnitGroup.UNITS.get(key);
686 if (group == null || group.getUnitCount() < 2)
689 prefs.put(key, group.getDefaultUnit().getUnit());
695 //// Material storage
699 * Add a user-defined material to the preferences. The preferences are
700 * first checked for an existing material matching the provided one using
701 * {@link Material#equals(Object)}.
703 * @param m the material to add.
705 public static void addUserMaterial(Material m) {
706 Preferences prefs = PREFNODE.node("userMaterials");
709 // Check whether material already exists
710 if (getUserMaterials().contains(m)) {
714 // Add material using next free key (key is not used when loading)
715 String mat = m.toStorableString();
716 for (int i = 0;; i++) {
717 String key = "material" + i;
718 if (prefs.get(key, null) == null) {
727 * Remove a user-defined material from the preferences. The matching is performed
728 * using {@link Material#equals(Object)}.
730 * @param m the material to remove.
732 public static void removeUserMaterial(Material m) {
733 Preferences prefs = PREFNODE.node("userMaterials");
737 // Iterate through materials and remove all keys with a matching material
738 for (String key : prefs.keys()) {
739 String value = prefs.get(key, null);
742 Material existing = Material.fromStorableString(value, true);
743 if (existing.equals(m)) {
747 } catch (IllegalArgumentException ignore) {
752 } catch (BackingStoreException e) {
753 throw new IllegalStateException("Cannot read preferences!", e);
759 * Return a set of all user-defined materials in the preferences. The materials
760 * are created marked as user-defined.
762 * @return a set of all user-defined materials.
764 public static Set<Material> getUserMaterials() {
765 Preferences prefs = PREFNODE.node("userMaterials");
767 HashSet<Material> materials = new HashSet<Material>();
770 for (String key : prefs.keys()) {
771 String value = prefs.get(key, null);
774 Material m = Material.fromStorableString(value, true);
777 } catch (IllegalArgumentException e) {
778 log.warn("Illegal material string " + value);
783 } catch (BackingStoreException e) {
784 throw new IllegalStateException("Cannot read preferences!", e);
793 private static String get(String directory,
794 Class<? extends RocketComponent> componentClass,
795 Map<Class<?>, String> defaultMap) {
797 // Search preferences
798 Class<?> c = componentClass;
799 Preferences prefs = PREFNODE.node(directory);
800 while (c != null && RocketComponent.class.isAssignableFrom(c)) {
801 String value = prefs.get(c.getSimpleName(), null);
804 c = c.getSuperclass();
807 if (defaultMap == null)
812 while (RocketComponent.class.isAssignableFrom(c)) {
813 String value = defaultMap.get(c);
816 c = c.getSuperclass();
823 private static void set(String directory, Class<? extends RocketComponent> componentClass,
825 Preferences prefs = PREFNODE.node(directory);
827 prefs.remove(componentClass.getSimpleName());
829 prefs.put(componentClass.getSimpleName(), value);