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