001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005 006import java.text.NumberFormat; 007import java.util.LinkedHashMap; 008import java.util.Locale; 009import java.util.Map; 010import java.util.concurrent.CopyOnWriteArrayList; 011 012import org.openstreetmap.josm.Main; 013import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference; 014 015/** 016 * A system of units used to express length and area measurements. 017 * <p> 018 * This class also manages one globally set system of measurement stored in the {@link ProjectionPreference} 019 * @since 3406 (creation) 020 * @since 6992 (extraction in this package) 021 */ 022public class SystemOfMeasurement { 023 024 /** 025 * Interface to notify listeners of the change of the system of measurement. 026 * @since 8554 027 */ 028 public interface SoMChangeListener { 029 /** 030 * The current SoM has changed. 031 * @param oldSoM The old system of measurement 032 * @param newSoM The new (current) system of measurement 033 */ 034 void systemOfMeasurementChanged(String oldSoM, String newSoM); 035 } 036 037 /** 038 * Metric system (international standard). 039 * @since 3406 040 */ 041 public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(1, "m", 1000, "km", 10000, "ha"); 042 043 /** 044 * Chinese system. 045 * @since 3406 046 */ 047 public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */); 048 049 /** 050 * Imperial system (British Commonwealth and former British Empire). 051 * @since 3406 052 */ 053 public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", 4046.86, "ac"); 054 055 /** 056 * Nautical mile system (navigation, polar exploration). 057 * @since 5549 058 */ 059 public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(185.2, "kbl", 1852, "NM"); 060 061 /** 062 * Known systems of measurement. 063 * @since 3406 064 */ 065 public static final Map<String, SystemOfMeasurement> ALL_SYSTEMS; 066 static { 067 ALL_SYSTEMS = new LinkedHashMap<>(); 068 ALL_SYSTEMS.put(marktr("Metric"), METRIC); 069 ALL_SYSTEMS.put(marktr("Chinese"), CHINESE); 070 ALL_SYSTEMS.put(marktr("Imperial"), IMPERIAL); 071 ALL_SYSTEMS.put(marktr("Nautical Mile"), NAUTICAL_MILE); 072 } 073 074 private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<>(); 075 076 /** 077 * Removes a global SoM change listener. 078 * 079 * @param listener the listener. Ignored if null or already absent 080 * @since 8554 081 */ 082 public static void removeSoMChangeListener(SoMChangeListener listener) { 083 somChangeListeners.remove(listener); 084 } 085 086 /** 087 * Adds a SoM change listener. 088 * 089 * @param listener the listener. Ignored if null or already registered. 090 * @since 8554 091 */ 092 public static void addSoMChangeListener(SoMChangeListener listener) { 093 if (listener != null) { 094 somChangeListeners.addIfAbsent(listener); 095 } 096 } 097 098 protected static void fireSoMChanged(String oldSoM, String newSoM) { 099 for (SoMChangeListener l : somChangeListeners) { 100 l.systemOfMeasurementChanged(oldSoM, newSoM); 101 } 102 } 103 104 /** 105 * Returns the current global system of measurement. 106 * @return The current system of measurement (metric system by default). 107 * @since 8554 108 */ 109 public static SystemOfMeasurement getSystemOfMeasurement() { 110 SystemOfMeasurement som = SystemOfMeasurement.ALL_SYSTEMS.get(ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get()); 111 if (som == null) 112 return SystemOfMeasurement.METRIC; 113 return som; 114 } 115 116 /** 117 * Sets the current global system of measurement. 118 * @param somKey The system of measurement key. Must be defined in {@link SystemOfMeasurement#ALL_SYSTEMS}. 119 * @throws IllegalArgumentException if {@code somKey} is not known 120 * @since 8554 121 */ 122 public static void setSystemOfMeasurement(String somKey) { 123 if (!SystemOfMeasurement.ALL_SYSTEMS.containsKey(somKey)) { 124 throw new IllegalArgumentException("Invalid system of measurement: "+somKey); 125 } 126 String oldKey = ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.get(); 127 if (ProjectionPreference.PROP_SYSTEM_OF_MEASUREMENT.put(somKey)) { 128 fireSoMChanged(oldKey, somKey); 129 } 130 } 131 132 /** First value, in meters, used to translate unit according to above formula. */ 133 public final double aValue; 134 /** Second value, in meters, used to translate unit according to above formula. */ 135 public final double bValue; 136 /** First unit used to format text. */ 137 public final String aName; 138 /** Second unit used to format text. */ 139 public final String bName; 140 /** Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}. Set to {@code -1} if not used. 141 * @since 5870 */ 142 public final double areaCustomValue; 143 /** Specific optional area unit. Set to {@code null} if not used. 144 * @since 5870 */ 145 public final String areaCustomName; 146 147 /** 148 * System of measurement. Currently covers only length (and area) units. 149 * 150 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as 151 * x_a == x_m / aValue 152 * 153 * @param aValue First value, in meters, used to translate unit according to above formula. 154 * @param aName First unit used to format text. 155 * @param bValue Second value, in meters, used to translate unit according to above formula. 156 * @param bName Second unit used to format text. 157 */ 158 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName) { 159 this(aValue, aName, bValue, bName, -1, null); 160 } 161 162 /** 163 * System of measurement. Currently covers only length (and area) units. 164 * 165 * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as 166 * x_a == x_m / aValue 167 * 168 * @param aValue First value, in meters, used to translate unit according to above formula. 169 * @param aName First unit used to format text. 170 * @param bValue Second value, in meters, used to translate unit according to above formula. 171 * @param bName Second unit used to format text. 172 * @param areaCustomValue Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}. 173 * Set to {@code -1} if not used. 174 * @param areaCustomName Specific optional area unit. Set to {@code null} if not used. 175 * 176 * @since 5870 177 */ 178 public SystemOfMeasurement(double aValue, String aName, double bValue, String bName, double areaCustomValue, String areaCustomName) { 179 this.aValue = aValue; 180 this.aName = aName; 181 this.bValue = bValue; 182 this.bName = bName; 183 this.areaCustomValue = areaCustomValue; 184 this.areaCustomName = areaCustomName; 185 } 186 187 /** 188 * Returns the text describing the given distance in this system of measurement. 189 * @param dist The distance in metres 190 * @return The text describing the given distance in this system of measurement. 191 */ 192 public String getDistText(double dist) { 193 return getDistText(dist, null, 0.01); 194 } 195 196 /** 197 * Returns the text describing the given distance in this system of measurement. 198 * @param dist The distance in metres 199 * @param format A {@link NumberFormat} to format the area value 200 * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"} 201 * @return The text describing the given distance in this system of measurement. 202 * @since 6422 203 */ 204 public String getDistText(final double dist, final NumberFormat format, final double threshold) { 205 double a = dist / aValue; 206 if (!Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false) && a > bValue / aValue) 207 return formatText(dist / bValue, bName, format); 208 else if (a < threshold) 209 return "< " + formatText(threshold, aName, format); 210 else 211 return formatText(a, aName, format); 212 } 213 214 /** 215 * Returns the text describing the given area in this system of measurement. 216 * @param area The area in square metres 217 * @return The text describing the given area in this system of measurement. 218 * @since 5560 219 */ 220 public String getAreaText(double area) { 221 return getAreaText(area, null, 0.01); 222 } 223 224 /** 225 * Returns the text describing the given area in this system of measurement. 226 * @param area The area in square metres 227 * @param format A {@link NumberFormat} to format the area value 228 * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"} 229 * @return The text describing the given area in this system of measurement. 230 * @since 6422 231 */ 232 public String getAreaText(final double area, final NumberFormat format, final double threshold) { 233 double a = area / (aValue*aValue); 234 boolean lowerOnly = Main.pref.getBoolean("system_of_measurement.use_only_lower_unit", false); 235 boolean customAreaOnly = Main.pref.getBoolean("system_of_measurement.use_only_custom_area_unit", false); 236 if ((!lowerOnly && areaCustomValue > 0 && a > areaCustomValue / (aValue*aValue) 237 && a < (bValue*bValue) / (aValue*aValue)) || customAreaOnly) 238 return formatText(area / areaCustomValue, areaCustomName, format); 239 else if (!lowerOnly && a >= (bValue*bValue) / (aValue*aValue)) 240 return formatText(area / (bValue * bValue), bName + '\u00b2', format); 241 else if (a < threshold) 242 return "< " + formatText(threshold, aName + '\u00b2', format); 243 else 244 return formatText(a, aName + '\u00b2', format); 245 } 246 247 private static String formatText(double v, String unit, NumberFormat format) { 248 if (format != null) { 249 return format.format(v) + ' ' + unit; 250 } 251 return String.format(Locale.US, "%." + (v < 9.999999 ? 2 : 1) + "f %s", v, unit); 252 } 253}