1 package net.sf.openrocket.util;
4 import java.awt.Dimension;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.util.ArrayList;
10 import java.util.Collections;
11 import java.util.HashMap;
12 import java.util.HashSet;
13 import java.util.List;
14 import java.util.Locale;
16 import java.util.MissingResourceException;
17 import java.util.Properties;
19 import java.util.prefs.BackingStoreException;
20 import java.util.prefs.Preferences;
22 import net.sf.openrocket.arch.SystemInfo;
23 import net.sf.openrocket.database.Databases;
24 import net.sf.openrocket.document.Simulation;
25 import net.sf.openrocket.gui.main.ExceptionHandler;
26 import net.sf.openrocket.gui.print.PrintSettings;
27 import net.sf.openrocket.l10n.L10N;
28 import net.sf.openrocket.l10n.Translator;
29 import net.sf.openrocket.logging.LogHelper;
30 import net.sf.openrocket.material.Material;
31 import net.sf.openrocket.rocketcomponent.BodyComponent;
32 import net.sf.openrocket.rocketcomponent.FinSet;
33 import net.sf.openrocket.rocketcomponent.InternalComponent;
34 import net.sf.openrocket.rocketcomponent.LaunchLug;
35 import net.sf.openrocket.rocketcomponent.MassObject;
36 import net.sf.openrocket.rocketcomponent.RecoveryDevice;
37 import net.sf.openrocket.rocketcomponent.Rocket;
38 import net.sf.openrocket.rocketcomponent.RocketComponent;
39 import net.sf.openrocket.simulation.FlightDataType;
40 import net.sf.openrocket.simulation.RK4SimulationStepper;
41 import net.sf.openrocket.simulation.SimulationOptions;
42 import net.sf.openrocket.startup.Application;
43 import net.sf.openrocket.unit.UnitGroup;
47 private static final LogHelper log = Application.getLogger();
49 private static final String SPLIT_CHARACTER = "|";
52 private static final List<Locale> SUPPORTED_LOCALES;
54 List<Locale> list = new ArrayList<Locale>();
55 for (String lang : new String[] { "en", "de", "es", "fr" }) {
56 list.add(new Locale(lang));
58 SUPPORTED_LOCALES = Collections.unmodifiableList(list);
63 * Whether to use the debug-node instead of the normal node.
65 private static final boolean DEBUG;
67 DEBUG = (System.getProperty("openrocket.debug.prefs") != null);
71 * Whether to clear all preferences at application startup. This has an effect only
74 private static final boolean CLEARPREFS = true;
77 * The node name to use in the Java preferences storage.
79 private static final String NODENAME = (DEBUG ? "OpenRocket-debug" : "OpenRocket");
83 * Load property file only when necessary.
85 private static class BuildPropertyHolder {
87 public static final Properties PROPERTIES;
88 public static final String BUILD_VERSION;
89 public static final String BUILD_SOURCE;
90 public static final boolean DEFAULT_CHECK_UPDATES;
94 InputStream is = ClassLoader.getSystemResourceAsStream("build.properties");
96 throw new MissingResourceException(
97 "build.properties not found, distribution built wrong" +
98 " classpath:" + System.getProperty("java.class.path"),
99 "build.properties", "build.version");
102 PROPERTIES = new Properties();
106 String version = PROPERTIES.getProperty("build.version");
107 if (version == null) {
108 throw new MissingResourceException(
109 "build.version not found in property file",
110 "build.properties", "build.version");
112 BUILD_VERSION = version.trim();
114 BUILD_SOURCE = PROPERTIES.getProperty("build.source");
115 if (BUILD_SOURCE == null) {
116 throw new MissingResourceException(
117 "build.source not found in property file",
118 "build.properties", "build.source");
121 String value = PROPERTIES.getProperty("build.checkupdates");
123 DEFAULT_CHECK_UPDATES = Boolean.parseBoolean(value);
125 DEFAULT_CHECK_UPDATES = true;
127 } catch (IOException e) {
128 throw new MissingResourceException(
129 "Error reading build.properties",
130 "build.properties", "build.version");
135 public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
137 public static final String USER_THRUST_CURVES_KEY = "UserThrustCurves";
139 public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
141 // Preferences related to data export
142 public static final String EXPORT_FIELD_SEPARATOR = "ExportFieldSeparator";
143 public static final String EXPORT_SIMULATION_COMMENT = "ExportSimulationComment";
144 public static final String EXPORT_FIELD_NAME_COMMENT = "ExportFieldDescriptionComment";
145 public static final String EXPORT_EVENT_COMMENTS = "ExportEventComments";
146 public static final String EXPORT_COMMENT_CHARACTER = "ExportCommentCharacter";
148 public static final String PLOT_SHOW_POINTS = "ShowPlotPoints";
150 private static final String CHECK_UPDATES = "CheckUpdates";
151 public static final String LAST_UPDATE = "LastUpdateVersion";
153 public static final String MOTOR_DIAMETER_FILTER = "MotorDiameterMatch";
154 public static final String MOTOR_HIDE_SIMILAR = "MotorHideSimilar";
158 public static final String PREFERRED_THRUST_CURVE_MOTOR_NODE = "preferredThrustCurveMotors";
161 private static final Preferences PREFNODE;
164 // Clear the preferences if debug mode and clearprefs is defined
166 Preferences root = Preferences.userRoot();
167 if (DEBUG && CLEARPREFS) {
169 if (root.nodeExists(NODENAME)) {
170 root.node(NODENAME).removeNode();
172 } catch (BackingStoreException e) {
173 throw new BugException("Unable to clear preference node", e);
176 PREFNODE = root.node(NODENAME);
182 ///////// Default component attributes
184 private static final HashMap<Class<?>, String> DEFAULT_COLORS =
185 new HashMap<Class<?>, String>();
187 DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
188 DEFAULT_COLORS.put(FinSet.class, "0,0,200");
189 DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
190 DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
191 DEFAULT_COLORS.put(MassObject.class, "0,0,0");
192 DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
196 private static final HashMap<Class<?>, String> DEFAULT_LINE_STYLES =
197 new HashMap<Class<?>, String>();
199 DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
200 DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
205 * Within a holder class so they will load only when needed.
207 private static class DefaultMaterialHolder {
208 private static final Translator trans = Application.getTranslator();
210 //// Elastic cord (round 2mm, 1/16 in)
211 private static final Material DEFAULT_LINE_MATERIAL =
212 Databases.findMaterial(Material.Type.LINE, trans.get("Databases.materials.Elasticcordround2mm"),
215 private static final Material DEFAULT_SURFACE_MATERIAL =
216 Databases.findMaterial(Material.Type.SURFACE, trans.get("Databases.materials.Ripstopnylon"), 0.067, false);
218 private static final Material DEFAULT_BULK_MATERIAL =
219 Databases.findMaterial(Material.Type.BULK, trans.get("Databases.materials.Cardboard"), 680, false);
222 //////////////////////
226 * Return the OpenRocket version number.
228 public static String getVersion() {
229 return BuildPropertyHolder.BUILD_VERSION;
234 * Return the OpenRocket build source (e.g. "default" or "Debian")
236 public static String getBuildSource() {
237 return BuildPropertyHolder.BUILD_SOURCE;
242 * Return the OpenRocket unique ID.
244 * @return a random ID string that stays constant between OpenRocket executions
246 public static String getUniqueID() {
247 String id = PREFNODE.get("id", null);
249 id = UniqueID.uuid();
250 PREFNODE.put("id", id);
258 * Store the current OpenRocket version into the preferences to allow for preferences migration.
260 private static void storeVersion() {
261 PREFNODE.put("OpenRocketVersion", getVersion());
266 * Returns a limited-range integer value from the preferences. If the value
267 * in the preferences is negative or greater than max, then the default value
270 * @param key The preference to retrieve.
271 * @param max Maximum allowed value for the choice.
272 * @param def Default value.
273 * @return The preference value.
275 public static int getChoise(String key, int max, int def) {
276 int v = PREFNODE.getInt(key, def);
277 if ((v < 0) || (v > max))
284 * Helper method that puts an integer choice value into the preferences.
286 * @param key the preference key.
287 * @param value the value to store.
289 public static void putChoise(String key, int value) {
290 PREFNODE.putInt(key, value);
296 * Return a string preference.
298 * @param key the preference key.
299 * @param def the default if no preference is stored
300 * @return the preference value
302 public static String getString(String key, String def) {
303 return PREFNODE.get(key, def);
307 * Set a string preference.
309 * @param key the preference key
310 * @param value the value to set, or <code>null</code> to remove the key
312 public static void putString(String key, String value) {
314 PREFNODE.remove(key);
317 PREFNODE.put(key, value);
323 * Retrieve an enum value from the user preferences.
325 * @param <T> the enum type
327 * @param def the default value, cannot be null
328 * @return the value in the preferences, or the default value
330 public static <T extends Enum<T>> T getEnum(String key, T def) {
332 throw new BugException("Default value cannot be null");
335 String value = getString(key, null);
341 return Enum.valueOf(def.getDeclaringClass(), value);
342 } catch (IllegalArgumentException e) {
348 * Store an enum value to the user preferences.
351 * @param value the value to store, or null to remove the value
353 public static void putEnum(String key, Enum<?> value) {
355 putString(key, null);
357 putString(key, value.name());
363 * Return a boolean preference.
365 * @param key the preference key
366 * @param def the default if no preference is stored
367 * @return the preference value
369 public static boolean getBoolean(String key, boolean def) {
370 return PREFNODE.getBoolean(key, def);
374 * Set a boolean preference.
376 * @param key the preference key
377 * @param value the value to set
379 public static void putBoolean(String key, boolean value) {
380 PREFNODE.putBoolean(key, value);
384 public static int getInt( String key, int defaultValue ) {
385 return PREFNODE.getInt(key, defaultValue);
388 public static void putInt( String key , int value ) {
389 PREFNODE.putInt(key, value );
393 * Return a preferences object for the specified node name.
395 * @param nodeName the node name
396 * @return the preferences object for that node
398 public static Preferences getNode(String nodeName) {
399 return PREFNODE.node(nodeName);
406 public static List<Locale> getSupportedLocales() {
407 return SUPPORTED_LOCALES;
410 public static Locale getUserLocale() {
411 String locale = getString("locale", null);
412 return L10N.toLocale(locale);
415 public static void setUserLocale(Locale l) {
417 putString("locale", null);
419 putString("locale", l.toString());
425 public static boolean getCheckUpdates() {
426 return PREFNODE.getBoolean(CHECK_UPDATES, BuildPropertyHolder.DEFAULT_CHECK_UPDATES);
429 public static void setCheckUpdates(boolean check) {
430 PREFNODE.putBoolean(CHECK_UPDATES, check);
434 public static File getDefaultDirectory() {
435 String file = PREFNODE.get("defaultDirectory", null);
438 return new File(file);
441 public static void setDefaultDirectory(File dir) {
446 d = dir.getAbsolutePath();
448 PREFNODE.put("defaultDirectory", d);
454 * Return a list of files/directories to be loaded as custom thrust curves.
456 * If this property has not been set, the directory "ThrustCurves" in the user
457 * application directory will be used. The directory will be created if it does not
460 * @return a list of files to load as thrust curves.
462 public static List<File> getUserThrustCurveFiles() {
463 List<File> list = new ArrayList<File>();
465 String files = getString(USER_THRUST_CURVES_KEY, null);
467 // Default to application directory
468 File tcdir = getDefaultUserThrustCurveFile();
469 if (!tcdir.isDirectory()) {
474 for (String file : files.split("\\" + SPLIT_CHARACTER)) {
476 if (file.length() > 0) {
477 list.add(new File(file));
485 public static File getDefaultUserThrustCurveFile() {
486 File appdir = SystemInfo.getUserApplicationDirectory();
487 File tcdir = new File(appdir, "ThrustCurves");
493 * Set the list of files/directories to be loaded as custom thrust curves.
495 * @param files the files to load, or <code>null</code> to reset to default value.
497 public static void setUserThrustCurveFiles(List<File> files) {
499 putString(USER_THRUST_CURVES_KEY, null);
505 for (File file : files) {
506 if (str.length() > 0) {
507 str += SPLIT_CHARACTER;
509 str += file.getAbsolutePath();
511 putString(USER_THRUST_CURVES_KEY, str);
518 public static Color getDefaultColor(Class<? extends RocketComponent> c) {
519 String color = get("componentColors", c, DEFAULT_COLORS);
523 Color clr = parseColor(color);
531 public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
534 set("componentColors", c, stringifyColor(color));
538 private static Color parseColor(String color) {
543 String[] rgb = color.split(",");
544 if (rgb.length == 3) {
546 int red = MathUtil.clamp(Integer.parseInt(rgb[0]), 0, 255);
547 int green = MathUtil.clamp(Integer.parseInt(rgb[1]), 0, 255);
548 int blue = MathUtil.clamp(Integer.parseInt(rgb[2]), 0, 255);
549 return new Color(red, green, blue);
550 } catch (NumberFormatException ignore) {
557 private static String stringifyColor(Color color) {
558 String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
564 public static Color getMotorBorderColor() {
565 // TODO: MEDIUM: Motor color (settable?)
566 return new Color(0, 0, 0, 200);
570 public static Color getMotorFillColor() {
571 // TODO: MEDIUM: Motor fill color (settable?)
572 return new Color(0, 0, 0, 100);
576 public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
577 String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
579 return LineStyle.valueOf(value);
580 } catch (Exception e) {
581 return LineStyle.SOLID;
585 public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
589 set("componentStyle", c, style.name());
593 public static double getDefaultMach() {
594 // TODO: HIGH: implement custom default mach number
598 public static Material getDefaultComponentMaterial(
599 Class<? extends RocketComponent> componentClass,
600 Material.Type type) {
602 String material = get("componentMaterials", componentClass, null);
603 if (material != null) {
605 Material m = Material.fromStorableString(material, false);
606 if (m.getType() == type)
608 } catch (IllegalArgumentException ignore) {
614 return DefaultMaterialHolder.DEFAULT_LINE_MATERIAL;
616 return DefaultMaterialHolder.DEFAULT_SURFACE_MATERIAL;
618 return DefaultMaterialHolder.DEFAULT_BULK_MATERIAL;
620 throw new IllegalArgumentException("Unknown material type: " + type);
623 public static void setDefaultComponentMaterial(
624 Class<? extends RocketComponent> componentClass, Material material) {
626 set("componentMaterials", componentClass,
627 material == null ? null : material.toStorableString());
631 public static int getMaxThreadCount() {
632 return Runtime.getRuntime().availableProcessors();
637 * Return whether to use additional safety code checks.
639 public static boolean useSafetyChecks() {
640 // Currently default to false unless openrocket.debug.safetycheck is defined
641 String s = System.getProperty("openrocket.debug.safetycheck");
642 if (s != null && !(s.equalsIgnoreCase("false") || s.equalsIgnoreCase("off"))) {
649 public static Point getWindowPosition(Class<?> c) {
651 String pref = PREFNODE.node("windows").get("position." + c.getCanonicalName(), null);
656 if (pref.indexOf(',') < 0)
660 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
661 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
662 } catch (NumberFormatException e) {
665 return new Point(x, y);
668 public static void setWindowPosition(Class<?> c, Point p) {
669 PREFNODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
676 public static Dimension getWindowSize(Class<?> c) {
678 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
683 if (pref.indexOf(',') < 0)
687 x = Integer.parseInt(pref.substring(0, pref.indexOf(',')));
688 y = Integer.parseInt(pref.substring(pref.indexOf(',') + 1));
689 } catch (NumberFormatException e) {
692 return new Dimension(x, y);
696 public static boolean isWindowMaximized(Class<?> c) {
697 String pref = PREFNODE.node("windows").get("size." + c.getCanonicalName(), null);
698 return "max".equals(pref);
701 public static void setWindowSize(Class<?> c, Dimension d) {
702 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
706 public static void setWindowMaximized(Class<?> c) {
707 PREFNODE.node("windows").put("size." + c.getCanonicalName(), "max");
714 public static PrintSettings getPrintSettings() {
715 PrintSettings settings = new PrintSettings();
718 c = parseColor(getString("print.template.fillColor", null));
720 settings.setTemplateFillColor(c);
723 c = parseColor(getString("print.template.borderColor", null));
725 settings.setTemplateBorderColor(c);
728 settings.setPaperSize(getEnum("print.paper.size", settings.getPaperSize()));
729 settings.setPaperOrientation(getEnum("print.paper.orientation", settings.getPaperOrientation()));
734 public static void setPrintSettings(PrintSettings settings) {
735 putString("print.template.fillColor", stringifyColor(settings.getTemplateFillColor()));
736 putString("print.template.borderColor", stringifyColor(settings.getTemplateBorderColor()));
737 putEnum("print.paper.size", settings.getPaperSize());
738 putEnum("print.paper.orientation", settings.getPaperOrientation());
741 //// Background flight data computation
743 public static boolean computeFlightInBackground() {
744 return PREFNODE.getBoolean("backgroundFlight", true);
747 public static Simulation getBackgroundSimulation(Rocket rocket) {
748 Simulation s = new Simulation(rocket);
749 SimulationOptions cond = s.getOptions();
751 cond.setTimeStep(RK4SimulationStepper.RECOMMENDED_TIME_STEP * 2);
752 cond.setWindSpeedAverage(1.0);
753 cond.setWindSpeedDeviation(0.1);
754 cond.setLaunchRodLength(5);
760 ///////// Export variables
762 public static boolean isExportSelected(FlightDataType type) {
763 Preferences prefs = PREFNODE.node("exports");
764 return prefs.getBoolean(type.getName(), false);
767 public static void setExportSelected(FlightDataType type, boolean selected) {
768 Preferences prefs = PREFNODE.node("exports");
769 prefs.putBoolean(type.getName(), selected);
774 ///////// Default unit storage
776 public static void loadDefaultUnits() {
777 Preferences prefs = PREFNODE.node("units");
780 for (String key : prefs.keys()) {
781 UnitGroup group = UnitGroup.UNITS.get(key);
786 group.setDefaultUnit(prefs.get(key, null));
787 } catch (IllegalArgumentException ignore) {
791 } catch (BackingStoreException e) {
792 ExceptionHandler.handleErrorCondition(e);
796 public static void storeDefaultUnits() {
797 Preferences prefs = PREFNODE.node("units");
799 for (String key : UnitGroup.UNITS.keySet()) {
800 UnitGroup group = UnitGroup.UNITS.get(key);
801 if (group == null || group.getUnitCount() < 2)
804 prefs.put(key, group.getDefaultUnit().getUnit());
810 //// Material storage
814 * Add a user-defined material to the preferences. The preferences are
815 * first checked for an existing material matching the provided one using
816 * {@link Material#equals(Object)}.
818 * @param m the material to add.
820 public static void addUserMaterial(Material m) {
821 Preferences prefs = PREFNODE.node("userMaterials");
824 // Check whether material already exists
825 if (getUserMaterials().contains(m)) {
829 // Add material using next free key (key is not used when loading)
830 String mat = m.toStorableString();
831 for (int i = 0;; i++) {
832 String key = "material" + i;
833 if (prefs.get(key, null) == null) {
842 * Remove a user-defined material from the preferences. The matching is performed
843 * using {@link Material#equals(Object)}.
845 * @param m the material to remove.
847 public static void removeUserMaterial(Material m) {
848 Preferences prefs = PREFNODE.node("userMaterials");
852 // Iterate through materials and remove all keys with a matching material
853 for (String key : prefs.keys()) {
854 String value = prefs.get(key, null);
857 Material existing = Material.fromStorableString(value, true);
858 if (existing.equals(m)) {
862 } catch (IllegalArgumentException ignore) {
867 } catch (BackingStoreException e) {
868 throw new IllegalStateException("Cannot read preferences!", e);
874 * Return a set of all user-defined materials in the preferences. The materials
875 * are created marked as user-defined.
877 * @return a set of all user-defined materials.
879 public static Set<Material> getUserMaterials() {
880 Preferences prefs = PREFNODE.node("userMaterials");
882 HashSet<Material> materials = new HashSet<Material>();
885 for (String key : prefs.keys()) {
886 String value = prefs.get(key, null);
889 Material m = Material.fromStorableString(value, true);
892 } catch (IllegalArgumentException e) {
893 log.warn("Illegal material string " + value);
898 } catch (BackingStoreException e) {
899 throw new IllegalStateException("Cannot read preferences!", e);
908 private static String get(String directory,
909 Class<? extends RocketComponent> componentClass,
910 Map<Class<?>, String> defaultMap) {
912 // Search preferences
913 Class<?> c = componentClass;
914 Preferences prefs = PREFNODE.node(directory);
915 while (c != null && RocketComponent.class.isAssignableFrom(c)) {
916 String value = prefs.get(c.getSimpleName(), null);
919 c = c.getSuperclass();
922 if (defaultMap == null)
927 while (RocketComponent.class.isAssignableFrom(c)) {
928 String value = defaultMap.get(c);
931 c = c.getSuperclass();
938 private static void set(String directory, Class<? extends RocketComponent> componentClass,
940 Preferences prefs = PREFNODE.node(directory);
942 prefs.remove(componentClass.getSimpleName());
944 prefs.put(componentClass.getSimpleName(), value);