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