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.Collections;
12 import java.util.HashMap;
13 import java.util.HashSet;
14 import java.util.List;
15 import java.util.Locale;
17 import java.util.MissingResourceException;
18 import java.util.Properties;
20 import java.util.prefs.BackingStoreException;
21 import java.util.prefs.Preferences;
23 import net.sf.openrocket.arch.SystemInfo;
24 import net.sf.openrocket.database.Databases;
25 import net.sf.openrocket.document.Simulation;
26 import net.sf.openrocket.gui.main.ExceptionHandler;
27 import net.sf.openrocket.gui.print.PrintSettings;
28 import net.sf.openrocket.l10n.L10N;
29 import net.sf.openrocket.l10n.Translator;
30 import net.sf.openrocket.logging.LogHelper;
31 import net.sf.openrocket.material.Material;
32 import net.sf.openrocket.rocketcomponent.BodyComponent;
33 import net.sf.openrocket.rocketcomponent.FinSet;
34 import net.sf.openrocket.rocketcomponent.InternalComponent;
35 import net.sf.openrocket.rocketcomponent.LaunchLug;
36 import net.sf.openrocket.rocketcomponent.MassObject;
37 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
38 import net.sf.openrocket.rocketcomponent.Rocket;
39 import net.sf.openrocket.rocketcomponent.RocketComponent;
40 import net.sf.openrocket.simulation.FlightDataType;
41 import net.sf.openrocket.simulation.SimulationOptions;
42 import net.sf.openrocket.simulation.RK4SimulationStepper;
43 import net.sf.openrocket.startup.Application;
44 import net.sf.openrocket.unit.UnitGroup;
48 private static final LogHelper log = Application.getLogger();
50 private static final String SPLIT_CHARACTER = "|";
53 private static final List<Locale> SUPPORTED_LOCALES;
55 List<Locale> list = new ArrayList<Locale>();
56 for (String lang : new String[] { "en", "de", "es", "fr" }) {
57 list.add(new Locale(lang));
59 SUPPORTED_LOCALES = Collections.unmodifiableList(list);
64 * Whether to use the debug-node instead of the normal node.
66 private static final boolean DEBUG;
68 DEBUG = (System.getProperty("openrocket.debug.prefs") != null);
72 * Whether to clear all preferences at application startup. This has an effect only
75 private static final boolean CLEARPREFS = true;
78 * The node name to use in the Java preferences storage.
80 private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket");
84 * Load property file only when necessary.
86 private static class BuildPropertyHolder {
88 public static final Properties PROPERTIES;
89 public static final String BUILD_VERSION;
90 public static final String BUILD_SOURCE;
91 public static final boolean DEFAULT_CHECK_UPDATES;
95 InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
97 throw new MissingResourceException(
98 "build.properties not found, distribution built wrong" +
99 " classpath:" + System.getProperty("java.class.path"),
100 "build.properties", "build.version");
103 PROPERTIES = new Properties();
107 String version = PROPERTIES.getProperty("build.version");
108 if (version == null) {
109 throw new MissingResourceException(
110 "build.version not found in property file",
111 "build.properties", "build.version");
113 BUILD_VERSION = version.trim();
115 BUILD_SOURCE = PROPERTIES.getProperty("build.source");
116 if (BUILD_SOURCE == null) {
117 throw new MissingResourceException(
118 "build.source not found in property file",
119 "build.properties", "build.source");
122 String value = PROPERTIES.getProperty("build.checkupdates");
124 DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
126 DEFAULT_CHECK_UPDATES = true;
128 } catch (IOException e) {
129 throw new MissingResourceException(
130 "Error reading build.properties",
131 "build.properties", "build.version");
136 public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
138 public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
140 public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
142 // Preferences related to data export
143 public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator";
144 public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment";
145 public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment";
146 public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments";
147 public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
149 public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
151 private static final String CHECK_UPDATES = "CheckUpdates";
152 public static final String LAST_UPDATE = "LastUpdateVersion";
154 public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch";
155 public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar";
159 public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
163 * Node to this application's preferences.
164 * @deprecated Use the static methods instead.
167 public static final Preferences NODE;
168 private static final Preferences PREFNODE;
171 // Clear the preferences if debug mode and clearprefs is defined
173 Preferences root = Preferences.userRoot();
174 if (DEBUG && CLEARPREFS) {
176 if (root.nodeExists(NODENAME)) {
177 root.node(NODENAME).removeNode();
179 } catch (BackingStoreException e) {
180 throw new BugException("Unable to clear preference node", e);
183 PREFNODE = root.node(NODENAME);
190 ///////// Default component attributes
192 private static final HashMap<Class<?>, String> DEFAULT_COLORS =
193 new HashMap<Class<?>, String>();
195 DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
196 DEFAULT_COLORS.put(FinSet.class, "0,0,200");
197 DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
198 DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
199 DEFAULT_COLORS.put(MassObject.class, "0,0,0");
200 DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
204 private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES =
205 new HashMap<Class<?>, String>();
207 DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
208 DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
213 * Within a holder class so they will load only when needed.
215 private static class DefaultMaterialHolder {
216 private static final Translator trans = Application.getTranslator();
218 //// Elastic cord (round 2mm, 1/16 in)
219 private static final Material DEFAULT_LINE_MATERIAL =
220 Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"),
223 private static final Material DEFAULT_SURFACE_MATERIAL =
224 Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false);
226 private static final Material DEFAULT_BULK_MATERIAL =
227 Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false);
230 //////////////////////
234 * Return the OpenRocket version number.
236 public static String getVersion() {
237 return BuildPropertyHolder.BUILD_VERSION;
242 * Return the OpenRocket build source (e.g. "default" or "Debian")
244 public static String getBuildSource() {
245 return BuildPropertyHolder.BUILD_SOURCE;
250 * Return the OpenRocket unique ID.
252 * @return a random ID string that stays constant between OpenRocket executions
254 public static String getUniqueID() {
255 String id = PREFNODE.get("id", null);
257 id = UniqueID.uuid();
258 PREFNODE.put("id", id);
266 * Store the current OpenRocket version into the preferences to allow for preferences migration.
268 private static void storeVersion() {
269 PREFNODE.put("OpenRocketVersion", getVersion());
274 * Returns a limited-range integer value from the preferences. If the value
275 * in the preferences is negative or greater than max, then the default value
278 * @param key The preference to retrieve.
279 * @param max Maximum allowed value for the choice.
280 * @param def Default value.
281 * @return The preference value.
283 public static int getChoise(String key, int max, int def) {
284 int v = PREFNODE.getInt(key, def);
285 if ((v < 0) || (v > max))
292 * Helper method that puts an integer choice value into the preferences.
294 * @param key the preference key.
295 * @param value the value to store.
297 public static void putChoise(String key, int value) {
298 PREFNODE.putInt(key, value);
304 * Return a string preference.
306 * @param key the preference key.
307 * @param def the default if no preference is stored
308 * @return the preference value
310 public static String getString(String key, String def) {
311 return PREFNODE.get(key, def);
315 * Set a string preference.
317 * @param key the preference key
318 * @param value the value to set, or <code>null</code> to remove the key
320 public static void putString(String key, String value) {
322 PREFNODE.remove(key);
325 PREFNODE.put(key, value);
331 * Retrieve an enum value from the user preferences.
333 * @param <T> the enum type
335 * @param def the default value, cannot be null
336 * @return the value in the preferences, or the default value
338 public static <T extends Enum<T>> T getEnum(String key, T def) {
340 throw new BugException("Default value cannot be null");
343 String value = getString(key, null);
349 return Enum.valueOf(def.getDeclaringClass(), value);
350 } catch (IllegalArgumentException e) {
356 * Store an enum value to the user preferences.
359 * @param value the value to store, or null to remove the value
361 public static void putEnum(String key, Enum<?> value) {
363 putString(key, null);
365 putString(key, value.name());
371 * Return a boolean preference.
373 * @param key the preference key
374 * @param def the default if no preference is stored
375 * @return the preference value
377 public static boolean getBoolean(String key, boolean def) {
378 return PREFNODE.getBoolean(key, def);
382 * Set a boolean preference.
384 * @param key the preference key
385 * @param value the value to set
387 public static void putBoolean(String key, boolean value) {
388 PREFNODE.putBoolean(key, value);
394 * Return a preferences object for the specified node name.
396 * @param nodeName the node name
397 * @return the preferences object for that node
399 public static Preferences getNode(String nodeName) {
400 return PREFNODE.node(nodeName);
407 public static List<Locale> getSupportedLocales() {
408 return SUPPORTED_LOCALES;
411 public static Locale getUserLocale() {
412 String locale = getString("locale", null);
413 return L10N.toLocale(locale);
416 public static void setUserLocale(Locale l) {
418 putString("locale", null);
420 putString("locale", l.toString());
426 public static boolean getCheckUpdates() {
427 return PREFNODE.getBoolean(CHECK_UPDATES, BuildPropertyHolder.DEFAULT_CHECK_UPDATES);
430 public static void setCheckUpdates(boolean check) {
431 PREFNODE.putBoolean(CHECK_UPDATES, check);
435 public static File getDefaultDirectory() {
436 String file = PREFNODE.get("defaultDirectory", null);
439 return new File(file);
442 public static void setDefaultDirectory(File dir) {
447 d = dir.getAbsolutePath();
449 PREFNODE.put("defaultDirectory", d);
455 * Return a list of files/directories to be loaded as custom thrust curves.
457 * If this property has not been set, the directory "ThrustCurves" in the user
458 * application directory will be used. The directory will be created if it does not
461 * @return a list of files to load as thrust curves.
463 public static List<File> getUserThrustCurveFiles() {
464 List<File> list = new ArrayList<File>();
466 String files = getString(USER_THRUST_CURVES_KEY, null);
468 // Default to application directory
469 File tcdir = getDefaultUserThrustCurveFile();
470 if (!tcdir.isDirectory()) {
475 for (String file : files.split("\\" + SPLIT_CHARACTER)) {
477 if (file.length() > 0) {
478 list.add(new File(file));
486 public static File getDefaultUserThrustCurveFile() {
487 File appdir = SystemInfo.getUserApplicationDirectory();
488 File tcdir = new File(appdir, "ThrustCurves");
494 * Set the list of files/directories to be loaded as custom thrust curves.
496 * @param files the files to load, or <code>null</code> to reset to default value.
498 public static void setUserThrustCurveFiles(List<File> files) {
500 putString(USER_THRUST_CURVES_KEY, null);
506 for (File file : files) {
507 if (str.length() > 0) {
508 str += SPLIT_CHARACTER;
510 str += file.getAbsolutePath();
512 putString(USER_THRUST_CURVES_KEY, str);
519 public static Color getDefaultColor(Class<? extends RocketComponent> c) {
520 String color = get("componentColors", c, DEFAULT_COLORS);
524 Color clr = parseColor(color);
532 public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
535 set("componentColors", c, stringifyColor(color));
539 private static Color parseColor(String color) {
544 String[] rgb = color.split(",");
545 if (rgb.length == 3) {
547 int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255);
548 int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255);
549 int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255);
550 return new Color(red, green, blue);
551 } catch (NumberFormatException ignore) {
558 private static String stringifyColor(Color color) {
559 String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
565 public static Color getMotorBorderColor() {
566 // TODO: MEDIUM: Motor color (settable?)
567 return new Color(0, 0, 0, 200);
571 public static Color getMotorFillColor() {
572 // TODO: MEDIUM: Motor fill color (settable?)
573 return new Color(0, 0, 0, 100);
577 public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
578 String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
580 return LineStyle.valueOf(value);
581 } catch (Exception e) {
582 return LineStyle.SOLID;
586 public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
590 set("componentStyle", c, style.name());
595 * Return the DPI setting of the monitor. This is either the setting provided
596 * by the system or a user-specified DPI setting.
598 * @return the DPI setting to use.
600 public static double getDPI() {
601 int dpi = PREFNODE.getInt("DPI", 0); // Tenths of a dpi
604 dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
613 public static double getDefaultMach() {
614 // TODO: HIGH: implement custom default mach number
621 public static Material getDefaultComponentMaterial(
622 Class<? extends RocketComponent> componentClass,
623 Material.Type type) {
625 String material = get("componentMaterials", componentClass, null);
626 if (material != null) {
628 Material m = Material.fromStorableString(material, false);
629 if (m.getType() == type)
631 } catch (IllegalArgumentException ignore) {
637 return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL;
639 return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL;
641 return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL;
643 throw new IllegalArgumentException("Unknown material type: " + type);
646 public static void setDefaultComponentMaterial(
647 Class<? extends RocketComponent> componentClass, Material material) {
649 set("componentMaterials", componentClass,
650 material == null ? null : material.toStorableString());
654 public static int getMaxThreadCount() {
655 return Runtime.getRuntime().availableProcessors();
660 * Return whether to use additional safety code checks.
662 public static boolean useSafetyChecks() {
663 // Currently default to false unless openrocket.debug.safetycheck is defined
664 String s = System.getProperty("openrocket.debug.safetycheck");
665 if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) {
672 public static Point getWindowPosition(Class<?> c) {
674 String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null);
679 if (pref.indexOf(',') < 0)
683 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
684 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
685 } catch (NumberFormatException e) {
688 return new Point(x, y);
691 public static void setWindowPosition(Class<?> c, Point p) {
692 PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
699 public static Dimension getWindowSize(Class<?> c) {
701 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
706 if (pref.indexOf(',') < 0)
710 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
711 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
712 } catch (NumberFormatException e) {
715 return new Dimension(x, y);
718 public static void setWindowSize(Class<?> c, Dimension d) {
719 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
726 public static PrintSettings getPrintSettings() {
727 PrintSettings settings = new PrintSettings();
730 c = parseColor(getString("print.template.fillColor", null));
732 settings.setTemplateFillColor(c);
735 c = parseColor(getString("print.template.borderColor", null));
737 settings.setTemplateBorderColor(c);
740 settings.setPaperSize(getEnum("print.paper.size", settings.getPaperSize()));
741 settings.setPaperOrientation(getEnum("print.paper.orientation", settings.getPaperOrientation()));
746 public static void setPrintSettings(PrintSettings settings) {
747 putString("print.template.fillColor", stringifyColor(settings.getTemplateFillColor()));
748 putString("print.template.borderColor", stringifyColor(settings.getTemplateBorderColor()));
749 putEnum("print.paper.size", settings.getPaperSize());
750 putEnum("print.paper.orientation", settings.getPaperOrientation());
753 //// Background flight data computation
755 public static boolean computeFlightInBackground() {
756 return PREFNODE.getBoolean("backgroundFlight", true);
759 public static Simulation getBackgroundSimulation(Rocket rocket) {
760 Simulation s = new Simulation(rocket);
761 SimulationOptions cond = s.getOptions();
763 cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2);
764 cond.setWindSpeedAverage(1.0);
765 cond.setWindSpeedDeviation(0.1);
766 cond.setLaunchRodLength(5);
772 ///////// Export variables
774 public static boolean isExportSelected(FlightDataType type) {
775 Preferences prefs = PREFNODE.node("exports");
776 return prefs.getBoolean(type.getName(), false);
779 public static void setExportSelected(FlightDataType type, boolean selected) {
780 Preferences prefs = PREFNODE.node("exports");
781 prefs.putBoolean(type.getName(), selected);
786 ///////// Default unit storage
788 public static void loadDefaultUnits() {
789 Preferences prefs = PREFNODE.node("units");
792 for (String key : prefs.keys()) {
793 UnitGroup group = UnitGroup.UNITS.get(key);
798 group.setDefaultUnit(prefs.get(key, null));
799 } catch (IllegalArgumentException ignore) {
803 } catch (BackingStoreException e) {
804 ExceptionHandler.handleErrorCondition(e);
808 public static void storeDefaultUnits() {
809 Preferences prefs = PREFNODE.node("units");
811 for (String key : UnitGroup.UNITS.keySet()) {
812 UnitGroup group = UnitGroup.UNITS.get(key);
813 if (group == null || group.getUnitCount() < 2)
816 prefs.put(key, group.getDefaultUnit().getUnit());
822 //// Material storage
826 * Add a user-defined material to the preferences. The preferences are
827 * first checked for an existing material matching the provided one using
828 * {@link Material#equals(Object)}.
830 * @param m the material to add.
832 public static void addUserMaterial(Material m) {
833 Preferences prefs = PREFNODE.node("userMaterials");
836 // Check whether material already exists
837 if (getUserMaterials().contains(m)) {
841 // Add material using next free key (key is not used when loading)
842 String mat = m.toStorableString();
843 for (int i = 0;; i++) {
844 String key = "material" + i;
845 if (prefs.get(key, null) == null) {
854 * Remove a user-defined material from the preferences. The matching is performed
855 * using {@link Material#equals(Object)}.
857 * @param m the material to remove.
859 public static void removeUserMaterial(Material m) {
860 Preferences prefs = PREFNODE.node("userMaterials");
864 // Iterate through materials and remove all keys with a matching material
865 for (String key : prefs.keys()) {
866 String value = prefs.get(key, null);
869 Material existing = Material.fromStorableString(value, true);
870 if (existing.equals(m)) {
874 } catch (IllegalArgumentException ignore) {
879 } catch (BackingStoreException e) {
880 throw new IllegalStateException("Cannot read preferences!", e);
886 * Return a set of all user-defined materials in the preferences. The materials
887 * are created marked as user-defined.
889 * @return a set of all user-defined materials.
891 public static Set<Material> getUserMaterials() {
892 Preferences prefs = PREFNODE.node("userMaterials");
894 HashSet<Material> materials = new HashSet<Material>();
897 for (String key : prefs.keys()) {
898 String value = prefs.get(key, null);
901 Material m = Material.fromStorableString(value, true);
904 } catch (IllegalArgumentException e) {
905 log.warn("Illegal material string " + value);
910 } catch (BackingStoreException e) {
911 throw new IllegalStateException("Cannot read preferences!", e);
920 private static String get(String directory,
921 Class<? extends RocketComponent> componentClass,
922 Map<Class<?>, String> defaultMap) {
924 // Search preferences
925 Class<?> c = componentClass;
926 Preferences prefs = PREFNODE.node(directory);
927 while (c != null && RocketComponent.class.isAssignableFrom(c)) {
928 String value = prefs.get(c.getSimpleName(), null);
931 c = c.getSuperclass();
934 if (defaultMap == null)
939 while (RocketComponent.class.isAssignableFrom(c)) {
940 String value = defaultMap.get(c);
943 c = c.getSuperclass();
950 private static void set(String directory, Class<? extends RocketComponent> componentClass,
952 Preferences prefs = PREFNODE.node(directory);
954 prefs.remove(componentClass.getSimpleName());
956 prefs.put(componentClass.getSimpleName(), value);