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.gui.print.PrintSettings;
26 import net.sf.openrocket.l10n.Translator;
27 import net.sf.openrocket.logging.LogHelper;
28 import net.sf.openrocket.material.Material;
29 import net.sf.openrocket.rocketcomponent.BodyComponent;
30 import net.sf.openrocket.rocketcomponent.FinSet;
31 import net.sf.openrocket.rocketcomponent.InternalComponent;
32 import net.sf.openrocket.rocketcomponent.LaunchLug;
33 import net.sf.openrocket.rocketcomponent.MassObject;
34 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
35 import net.sf.openrocket.rocketcomponent.Rocket;
36 import net.sf.openrocket.rocketcomponent.RocketComponent;
37 import net.sf.openrocket.simulation.FlightDataType;
38 import net.sf.openrocket.simulation.GUISimulationConditions;
39 import net.sf.openrocket.simulation.RK4SimulationStepper;
40 import net.sf.openrocket.startup.Application;
41 import net.sf.openrocket.unit.UnitGroup;
45 private static final LogHelper log = Application.getLogger();
47 private static final String SPLIT_CHARACTER = "|";
51 * Whether to use the debug-node instead of the normal node.
53 private static final boolean DEBUG;
55 DEBUG = (System.getProperty("openrocket.debug.prefs") != null);
59 * Whether to clear all preferences at application startup. This has an effect only
62 private static final boolean CLEARPREFS = true;
65 * The node name to use in the Java preferences storage.
67 private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket");
71 * Load property file only when necessary.
73 private static class BuildPropertyHolder {
75 public static final Properties PROPERTIES;
76 public static final String BUILD_VERSION;
77 public static final String BUILD_SOURCE;
78 public static final boolean DEFAULT_CHECK_UPDATES;
82 InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
84 throw new MissingResourceException(
85 "build.properties not found, distribution built wrong" +
86 " classpath:" + System.getProperty("java.class.path"),
87 "build.properties", "build.version");
90 PROPERTIES = new Properties();
94 String version = PROPERTIES.getProperty("build.version");
95 if (version == null) {
96 throw new MissingResourceException(
97 "build.version not found in property file",
98 "build.properties", "build.version");
100 BUILD_VERSION = version.trim();
102 BUILD_SOURCE = PROPERTIES.getProperty("build.source");
103 if (BUILD_SOURCE == null) {
104 throw new MissingResourceException(
105 "build.source not found in property file",
106 "build.properties", "build.source");
109 String value = PROPERTIES.getProperty("build.checkupdates");
111 DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
113 DEFAULT_CHECK_UPDATES = true;
115 } catch (IOException e) {
116 throw new MissingResourceException(
117 "Error reading build.properties",
118 "build.properties", "build.version");
123 public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
125 public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
127 public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
129 // Preferences related to data export
130 public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator";
131 public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment";
132 public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment";
133 public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments";
134 public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
136 public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
138 private static final String CHECK_UPDATES = "CheckUpdates";
139 public static final String LAST_UPDATE = "LastUpdateVersion";
141 public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch";
142 public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar";
146 public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
150 * Node to this application's preferences.
151 * @deprecated Use the static methods instead.
154 public static final Preferences NODE;
155 private static final Preferences PREFNODE;
158 // Clear the preferences if debug mode and clearprefs is defined
160 Preferences root = Preferences.userRoot();
161 if (DEBUG && CLEARPREFS) {
163 if (root.nodeExists(NODENAME)) {
164 root.node(NODENAME).removeNode();
166 } catch (BackingStoreException e) {
167 throw new BugException("Unable to clear preference node", e);
170 PREFNODE = root.node(NODENAME);
177 ///////// Default component attributes
179 private static final HashMap<Class<?>, String> DEFAULT_COLORS =
180 new HashMap<Class<?>, String>();
182 DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
183 DEFAULT_COLORS.put(FinSet.class, "0,0,200");
184 DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
185 DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
186 DEFAULT_COLORS.put(MassObject.class, "0,0,0");
187 DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
191 private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES =
192 new HashMap<Class<?>, String>();
194 DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
195 DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
200 * Within a holder class so they will load only when needed.
202 private static class DefaultMaterialHolder {
203 private static final Translator trans = Application.getTranslator();
205 //// Elastic cord (round 2mm, 1/16 in)
206 private static final Material DEFAULT_LINE_MATERIAL =
207 Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"),
210 private static final Material DEFAULT_SURFACE_MATERIAL =
211 Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false);
213 private static final Material DEFAULT_BULK_MATERIAL =
214 Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false);
217 //////////////////////
221 * Return the OpenRocket version number.
223 public static String getVersion() {
224 return BuildPropertyHolder.BUILD_VERSION;
229 * Return the OpenRocket build source (e.g. "default" or "Debian")
231 public static String getBuildSource() {
232 return BuildPropertyHolder.BUILD_SOURCE;
237 * Return the OpenRocket unique ID.
239 * @return a random ID string that stays constant between OpenRocket executions
241 public static String getUniqueID() {
242 String id = PREFNODE.get("id", null);
244 id = UniqueID.uuid();
245 PREFNODE.put("id", id);
253 * Store the current OpenRocket version into the preferences to allow for preferences migration.
255 private static void storeVersion() {
256 PREFNODE.put("OpenRocketVersion", getVersion());
261 * Returns a limited-range integer value from the preferences. If the value
262 * in the preferences is negative or greater than max, then the default value
265 * @param key The preference to retrieve.
266 * @param max Maximum allowed value for the choice.
267 * @param def Default value.
268 * @return The preference value.
270 public static int getChoise(String key, int max, int def) {
271 int v = PREFNODE.getInt(key, def);
272 if ((v < 0) || (v > max))
279 * Helper method that puts an integer choice value into the preferences.
281 * @param key the preference key.
282 * @param value the value to store.
284 public static void putChoise(String key, int value) {
285 PREFNODE.putInt(key, value);
291 * Return a string preference.
293 * @param key the preference key.
294 * @param def the default if no preference is stored
295 * @return the preference value
297 public static String getString(String key, String def) {
298 return PREFNODE.get(key, def);
302 * Set a string preference.
304 * @param key the preference key
305 * @param value the value to set, or <code>null</code> to remove the key
307 public static void putString(String key, String value) {
309 PREFNODE.remove(key);
312 PREFNODE.put(key, value);
318 * Retrieve an enum value from the user preferences.
320 * @param <T> the enum type
322 * @param def the default value, cannot be null
323 * @return the value in the preferences, or the default value
325 public static <T extends Enum<T>> T getEnum(String key, T def) {
327 throw new BugException("Default value cannot be null");
330 String value = getString(key, null);
336 return Enum.valueOf(def.getDeclaringClass(), value);
337 } catch (IllegalArgumentException e) {
343 * Store an enum value to the user preferences.
346 * @param value the value to store, or null to remove the value
348 public static void putEnum(String key, Enum<?> value) {
350 putString(key, null);
352 putString(key, value.name());
358 * Return a boolean preference.
360 * @param key the preference key
361 * @param def the default if no preference is stored
362 * @return the preference value
364 public static boolean getBoolean(String key, boolean def) {
365 return PREFNODE.getBoolean(key, def);
369 * Set a boolean preference.
371 * @param key the preference key
372 * @param value the value to set
374 public static void putBoolean(String key, boolean value) {
375 PREFNODE.putBoolean(key, value);
381 * Return a preferences object for the specified node name.
383 * @param nodeName the node name
384 * @return the preferences object for that node
386 public static Preferences getNode(String nodeName) {
387 return PREFNODE.node(nodeName);
396 public static boolean getCheckUpdates() {
397 return PREFNODE.getBoolean(CHECK_UPDATES, BuildPropertyHolder.DEFAULT_CHECK_UPDATES);
400 public static void setCheckUpdates(boolean check) {
401 PREFNODE.putBoolean(CHECK_UPDATES, check);
405 public static File getDefaultDirectory() {
406 String file = PREFNODE.get("defaultDirectory", null);
409 return new File(file);
412 public static void setDefaultDirectory(File dir) {
417 d = dir.getAbsolutePath();
419 PREFNODE.put("defaultDirectory", d);
425 * Return a list of files/directories to be loaded as custom thrust curves.
427 * If this property has not been set, the directory "ThrustCurves" in the user
428 * application directory will be used. The directory will be created if it does not
431 * @return a list of files to load as thrust curves.
433 public static List<File> getUserThrustCurveFiles() {
434 List<File> list = new ArrayList<File>();
436 String files = getString(USER_THRUST_CURVES_KEY, null);
438 // Default to application directory
439 File tcdir = getDefaultUserThrustCurveFile();
440 if (!tcdir.isDirectory()) {
445 for (String file : files.split("\\" + SPLIT_CHARACTER)) {
447 if (file.length() > 0) {
448 list.add(new File(file));
456 public static File getDefaultUserThrustCurveFile() {
457 File appdir = SystemInfo.getUserApplicationDirectory();
458 File tcdir = new File(appdir, "ThrustCurves");
464 * Set the list of files/directories to be loaded as custom thrust curves.
466 * @param files the files to load, or <code>null</code> to reset to default value.
468 public static void setUserThrustCurveFiles(List<File> files) {
470 putString(USER_THRUST_CURVES_KEY, null);
476 for (File file : files) {
477 if (str.length() > 0) {
478 str += SPLIT_CHARACTER;
480 str += file.getAbsolutePath();
482 putString(USER_THRUST_CURVES_KEY, str);
489 public static Color getDefaultColor(Class<? extends RocketComponent> c) {
490 String color = get("componentColors", c, DEFAULT_COLORS);
494 Color clr = parseColor(color);
502 public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
505 set("componentColors", c, stringifyColor(color));
509 private static Color parseColor(String color) {
514 String[] rgb = color.split(",");
515 if (rgb.length == 3) {
517 int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255);
518 int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255);
519 int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255);
520 return new Color(red, green, blue);
521 } catch (NumberFormatException ignore) {
528 private static String stringifyColor(Color color) {
529 String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
535 public static Color getMotorBorderColor() {
536 // TODO: MEDIUM: Motor color (settable?)
537 return new Color(0, 0, 0, 200);
541 public static Color getMotorFillColor() {
542 // TODO: MEDIUM: Motor fill color (settable?)
543 return new Color(0, 0, 0, 100);
547 public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
548 String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
550 return LineStyle.valueOf(value);
551 } catch (Exception e) {
552 return LineStyle.SOLID;
556 public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
560 set("componentStyle", c, style.name());
565 * Return the DPI setting of the monitor. This is either the setting provided
566 * by the system or a user-specified DPI setting.
568 * @return the DPI setting to use.
570 public static double getDPI() {
571 int dpi = PREFNODE.getInt("DPI", 0); // Tenths of a dpi
574 dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
583 public static double getDefaultMach() {
584 // TODO: HIGH: implement custom default mach number
591 public static Material getDefaultComponentMaterial(
592 Class<? extends RocketComponent> componentClass,
593 Material.Type type) {
595 String material = get("componentMaterials", componentClass, null);
596 if (material != null) {
598 Material m = Material.fromStorableString(material, false);
599 if (m.getType() == type)
601 } catch (IllegalArgumentException ignore) {
607 return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL;
609 return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL;
611 return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL;
613 throw new IllegalArgumentException("Unknown material type: " + type);
616 public static void setDefaultComponentMaterial(
617 Class<? extends RocketComponent> componentClass, Material material) {
619 set("componentMaterials", componentClass,
620 material == null ? null : material.toStorableString());
624 public static int getMaxThreadCount() {
625 return Runtime.getRuntime().availableProcessors();
630 * Return whether to use additional safety code checks.
632 public static boolean useSafetyChecks() {
633 // Currently default to false unless openrocket.debug.safetycheck is defined
634 String s = System.getProperty("openrocket.debug.safetycheck");
635 if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) {
642 public static Point getWindowPosition(Class<?> c) {
644 String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null);
649 if (pref.indexOf(',') < 0)
653 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
654 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
655 } catch (NumberFormatException e) {
658 return new Point(x, y);
661 public static void setWindowPosition(Class<?> c, Point p) {
662 PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
669 public static Dimension getWindowSize(Class<?> c) {
671 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
676 if (pref.indexOf(',') < 0)
680 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
681 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
682 } catch (NumberFormatException e) {
685 return new Dimension(x, y);
688 public static void setWindowSize(Class<?> c, Dimension d) {
689 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
696 public static PrintSettings getPrintSettings() {
697 PrintSettings settings = new PrintSettings();
700 c = parseColor(getString("print.template.fillColor", null));
702 settings.setTemplateFillColor(c);
705 c = parseColor(getString("print.template.borderColor", null));
707 settings.setTemplateBorderColor(c);
710 settings.setPaperSize(getEnum("print.paper.size", settings.getPaperSize()));
711 settings.setPaperOrientation(getEnum("print.paper.orientation", settings.getPaperOrientation()));
716 public static void setPrintSettings(PrintSettings settings) {
717 putString("print.template.fillColor", stringifyColor(settings.getTemplateFillColor()));
718 putString("print.template.borderColor", stringifyColor(settings.getTemplateBorderColor()));
719 putEnum("print.paper.size", settings.getPaperSize());
720 putEnum("print.paper.orientation", settings.getPaperOrientation());
723 //// Background flight data computation
725 public static boolean computeFlightInBackground() {
726 return PREFNODE.getBoolean("backgroundFlight", true);
729 public static Simulation getBackgroundSimulation(Rocket rocket) {
730 Simulation s = new Simulation(rocket);
731 GUISimulationConditions cond = s.getConditions();
733 cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2);
734 cond.setWindSpeedAverage(1.0);
735 cond.setWindSpeedDeviation(0.1);
736 cond.setLaunchRodLength(5);
742 ///////// Export variables
744 public static boolean isExportSelected(FlightDataType type) {
745 Preferences prefs = PREFNODE.node("exports");
746 return prefs.getBoolean(type.getName(), false);
749 public static void setExportSelected(FlightDataType type, boolean selected) {
750 Preferences prefs = PREFNODE.node("exports");
751 prefs.putBoolean(type.getName(), selected);
756 ///////// Default unit storage
758 public static void loadDefaultUnits() {
759 Preferences prefs = PREFNODE.node("units");
762 for (String key : prefs.keys()) {
763 UnitGroup group = UnitGroup.UNITS.get(key);
768 group.setDefaultUnit(prefs.get(key, null));
769 } catch (IllegalArgumentException ignore) {
773 } catch (BackingStoreException e) {
774 ExceptionHandler.handleErrorCondition(e);
778 public static void storeDefaultUnits() {
779 Preferences prefs = PREFNODE.node("units");
781 for (String key : UnitGroup.UNITS.keySet()) {
782 UnitGroup group = UnitGroup.UNITS.get(key);
783 if (group == null || group.getUnitCount() < 2)
786 prefs.put(key, group.getDefaultUnit().getUnit());
792 //// Material storage
796 * Add a user-defined material to the preferences. The preferences are
797 * first checked for an existing material matching the provided one using
798 * {@link Material#equals(Object)}.
800 * @param m the material to add.
802 public static void addUserMaterial(Material m) {
803 Preferences prefs = PREFNODE.node("userMaterials");
806 // Check whether material already exists
807 if (getUserMaterials().contains(m)) {
811 // Add material using next free key (key is not used when loading)
812 String mat = m.toStorableString();
813 for (int i = 0;; i++) {
814 String key = "material" + i;
815 if (prefs.get(key, null) == null) {
824 * Remove a user-defined material from the preferences. The matching is performed
825 * using {@link Material#equals(Object)}.
827 * @param m the material to remove.
829 public static void removeUserMaterial(Material m) {
830 Preferences prefs = PREFNODE.node("userMaterials");
834 // Iterate through materials and remove all keys with a matching material
835 for (String key : prefs.keys()) {
836 String value = prefs.get(key, null);
839 Material existing = Material.fromStorableString(value, true);
840 if (existing.equals(m)) {
844 } catch (IllegalArgumentException ignore) {
849 } catch (BackingStoreException e) {
850 throw new IllegalStateException("Cannot read preferences!", e);
856 * Return a set of all user-defined materials in the preferences. The materials
857 * are created marked as user-defined.
859 * @return a set of all user-defined materials.
861 public static Set<Material> getUserMaterials() {
862 Preferences prefs = PREFNODE.node("userMaterials");
864 HashSet<Material> materials = new HashSet<Material>();
867 for (String key : prefs.keys()) {
868 String value = prefs.get(key, null);
871 Material m = Material.fromStorableString(value, true);
874 } catch (IllegalArgumentException e) {
875 log.warn("Illegal material string " + value);
880 } catch (BackingStoreException e) {
881 throw new IllegalStateException("Cannot read preferences!", e);
890 private static String get(String directory,
891 Class<? extends RocketComponent> componentClass,
892 Map<Class<?>, String> defaultMap) {
894 // Search preferences
895 Class<?> c = componentClass;
896 Preferences prefs = PREFNODE.node(directory);
897 while (c != null && RocketComponent.class.isAssignableFrom(c)) {
898 String value = prefs.get(c.getSimpleName(), null);
901 c = c.getSuperclass();
904 if (defaultMap == null)
909 while (RocketComponent.class.isAssignableFrom(c)) {
910 String value = defaultMap.get(c);
913 c = c.getSuperclass();
920 private static void set(String directory, Class<? extends RocketComponent> componentClass,
922 Preferences prefs = PREFNODE.node(directory);
924 prefs.remove(componentClass.getSimpleName());
926 prefs.put(componentClass.getSimpleName(), value);