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