| 1 | // License: GPL. For details, see LICENSE file.
|
|---|
| 2 | package org.openstreetmap.josm.data;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.marktr;
|
|---|
| 5 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 6 |
|
|---|
| 7 | import java.text.NumberFormat;
|
|---|
| 8 | import java.util.Collections;
|
|---|
| 9 | import java.util.Locale;
|
|---|
| 10 | import java.util.Map;
|
|---|
| 11 | import java.util.Optional;
|
|---|
| 12 | import java.util.concurrent.CopyOnWriteArrayList;
|
|---|
| 13 | import java.util.function.Function;
|
|---|
| 14 | import java.util.stream.Collectors;
|
|---|
| 15 | import java.util.stream.Stream;
|
|---|
| 16 |
|
|---|
| 17 | import org.openstreetmap.josm.data.preferences.StringProperty;
|
|---|
| 18 | import org.openstreetmap.josm.spi.preferences.Config;
|
|---|
| 19 | import org.openstreetmap.josm.tools.LanguageInfo;
|
|---|
| 20 |
|
|---|
| 21 | /**
|
|---|
| 22 | * A system of units used to express length and area measurements.
|
|---|
| 23 | * <p>
|
|---|
| 24 | * This class also manages one globally set system of measurement stored in the {@code ProjectionPreference}
|
|---|
| 25 | * @since 3406 (creation)
|
|---|
| 26 | * @since 6992 (extraction in this package)
|
|---|
| 27 | */
|
|---|
| 28 | public class SystemOfMeasurement {
|
|---|
| 29 |
|
|---|
| 30 | /**
|
|---|
| 31 | * Interface to notify listeners of the change of the system of measurement.
|
|---|
| 32 | * @since 8554
|
|---|
| 33 | * @since 10600 (functional interface)
|
|---|
| 34 | */
|
|---|
| 35 | @FunctionalInterface
|
|---|
| 36 | public interface SoMChangeListener {
|
|---|
| 37 | /**
|
|---|
| 38 | * The current SoM has changed.
|
|---|
| 39 | * @param oldSoM The old system of measurement
|
|---|
| 40 | * @param newSoM The new (current) system of measurement
|
|---|
| 41 | */
|
|---|
| 42 | void systemOfMeasurementChanged(String oldSoM, String newSoM);
|
|---|
| 43 | }
|
|---|
| 44 |
|
|---|
| 45 | /**
|
|---|
| 46 | * Metric system (international standard).
|
|---|
| 47 | * @since 3406
|
|---|
| 48 | */
|
|---|
| 49 | public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(marktr("Metric"), 1, "m", 1000, "km", "km/h", 3.6, 10_000, "ha");
|
|---|
| 50 |
|
|---|
| 51 | /**
|
|---|
| 52 | * Chinese system.
|
|---|
| 53 | * See <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_length_units_effective_in_1930">length units</a>,
|
|---|
| 54 | * <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_area_units_effective_in_1930">area units</a>
|
|---|
| 55 | * @since 3406
|
|---|
| 56 | */
|
|---|
| 57 | public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(marktr("Chinese"),
|
|---|
| 58 | 1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */, "km/h", 3.6, 666.0 + 2.0/3.0, "\u4ea9" /* mu */);
|
|---|
| 59 |
|
|---|
| 60 | /**
|
|---|
| 61 | * Imperial system (British Commonwealth and former British Empire).
|
|---|
| 62 | * @since 3406
|
|---|
| 63 | */
|
|---|
| 64 | public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(marktr("Imperial"),
|
|---|
| 65 | 0.3048, "ft", 1609.344, "mi", "mph", 2.23694, 4046.86, "ac");
|
|---|
| 66 |
|
|---|
| 67 | /**
|
|---|
| 68 | * Nautical mile system (navigation, polar exploration).
|
|---|
| 69 | * @since 5549
|
|---|
| 70 | */
|
|---|
| 71 | public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(marktr("Nautical Mile"),
|
|---|
| 72 | 185.2, "kbl", 1852, "NM", "kn", 1.94384);
|
|---|
| 73 |
|
|---|
| 74 | /**
|
|---|
| 75 | * Known systems of measurement.
|
|---|
| 76 | * @since 3406
|
|---|
| 77 | */
|
|---|
| 78 | public static final Map<String, SystemOfMeasurement> ALL_SYSTEMS = Collections.unmodifiableMap(
|
|---|
| 79 | Stream.of(METRIC, CHINESE, IMPERIAL, NAUTICAL_MILE)
|
|---|
| 80 | .collect(Collectors.toMap(SystemOfMeasurement::getName, Function.identity())));
|
|---|
| 81 |
|
|---|
| 82 | /**
|
|---|
| 83 | * Preferences entry for system of measurement.
|
|---|
| 84 | * @since 12674 (moved from ProjectionPreference)
|
|---|
| 85 | */
|
|---|
| 86 | public static final StringProperty PROP_SYSTEM_OF_MEASUREMENT = new StringProperty("system_of_measurement", getDefault().getName());
|
|---|
| 87 |
|
|---|
| 88 | private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<>();
|
|---|
| 89 |
|
|---|
| 90 | /**
|
|---|
| 91 | * Removes a global SoM change listener.
|
|---|
| 92 | *
|
|---|
| 93 | * @param listener the listener. Ignored if null or already absent
|
|---|
| 94 | * @since 8554
|
|---|
| 95 | */
|
|---|
| 96 | public static void removeSoMChangeListener(SoMChangeListener listener) {
|
|---|
| 97 | somChangeListeners.remove(listener);
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | /**
|
|---|
| 101 | * Adds a SoM change listener.
|
|---|
| 102 | *
|
|---|
| 103 | * @param listener the listener. Ignored if null or already registered.
|
|---|
| 104 | * @since 8554
|
|---|
| 105 | */
|
|---|
| 106 | public static void addSoMChangeListener(SoMChangeListener listener) {
|
|---|
| 107 | if (listener != null) {
|
|---|
| 108 | somChangeListeners.addIfAbsent(listener);
|
|---|
| 109 | }
|
|---|
| 110 | }
|
|---|
| 111 |
|
|---|
| 112 | protected static void fireSoMChanged(String oldSoM, String newSoM) {
|
|---|
| 113 | for (SoMChangeListener l : somChangeListeners) {
|
|---|
| 114 | l.systemOfMeasurementChanged(oldSoM, newSoM);
|
|---|
| 115 | }
|
|---|
| 116 | }
|
|---|
| 117 |
|
|---|
| 118 | /**
|
|---|
| 119 | * Returns the current global system of measurement.
|
|---|
| 120 | * @return The current system of measurement (metric system by default).
|
|---|
| 121 | * @since 8554
|
|---|
| 122 | */
|
|---|
| 123 | public static SystemOfMeasurement getSystemOfMeasurement() {
|
|---|
| 124 | return Optional.ofNullable(SystemOfMeasurement.ALL_SYSTEMS.get(PROP_SYSTEM_OF_MEASUREMENT.get()))
|
|---|
| 125 | .orElse(SystemOfMeasurement.METRIC);
|
|---|
| 126 | }
|
|---|
| 127 |
|
|---|
| 128 | /**
|
|---|
| 129 | * Sets the current global system of measurement.
|
|---|
| 130 | * @param som The system of measurement to set. Must be defined in {@link SystemOfMeasurement#ALL_SYSTEMS}.
|
|---|
| 131 | * @throws IllegalArgumentException if {@code som} is not known
|
|---|
| 132 | * @since 16985 (signature)
|
|---|
| 133 | */
|
|---|
| 134 | public static void setSystemOfMeasurement(SystemOfMeasurement som) {
|
|---|
| 135 | String somKey = som.getName();
|
|---|
| 136 | if (!SystemOfMeasurement.ALL_SYSTEMS.containsKey(somKey)) {
|
|---|
| 137 | throw new IllegalArgumentException("Invalid system of measurement: "+somKey);
|
|---|
| 138 | }
|
|---|
| 139 | String oldKey = PROP_SYSTEM_OF_MEASUREMENT.get();
|
|---|
| 140 | if (PROP_SYSTEM_OF_MEASUREMENT.put(somKey)) {
|
|---|
| 141 | fireSoMChanged(oldKey, somKey);
|
|---|
| 142 | }
|
|---|
| 143 | }
|
|---|
| 144 |
|
|---|
| 145 | /** Translatable name of this system of measurement. */
|
|---|
| 146 | private final String name;
|
|---|
| 147 | /** First value, in meters, used to translate unit according to above formula. */
|
|---|
| 148 | public final double aValue;
|
|---|
| 149 | /** Second value, in meters, used to translate unit according to above formula. */
|
|---|
| 150 | public final double bValue;
|
|---|
| 151 | /** First unit used to format text. */
|
|---|
| 152 | public final String aName;
|
|---|
| 153 | /** Second unit used to format text. */
|
|---|
| 154 | public final String bName;
|
|---|
| 155 | /** Speed value for the most common speed symbol, in meters per second
|
|---|
| 156 | * @since 10175 */
|
|---|
| 157 | public final double speedValue;
|
|---|
| 158 | /** Most common speed symbol (kmh/h, mph, kn, etc.)
|
|---|
| 159 | * @since 10175 */
|
|---|
| 160 | public final String speedName;
|
|---|
| 161 | /** Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}. Set to {@code -1} if not used.
|
|---|
| 162 | * @since 5870 */
|
|---|
| 163 | public final double areaCustomValue;
|
|---|
| 164 | /** Specific optional area unit. Set to {@code null} if not used.
|
|---|
| 165 | * @since 5870 */
|
|---|
| 166 | public final String areaCustomName;
|
|---|
| 167 |
|
|---|
| 168 | /**
|
|---|
| 169 | * System of measurement. Currently covers only length (and area) units.
|
|---|
| 170 | *
|
|---|
| 171 | * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
|
|---|
| 172 | * x_a == x_m / aValue
|
|---|
| 173 | *
|
|---|
| 174 | * @param name Translatable name of this system of measurement
|
|---|
| 175 | * @param aValue First value, in meters, used to translate unit according to above formula.
|
|---|
| 176 | * @param aName First unit used to format text.
|
|---|
| 177 | * @param bValue Second value, in meters, used to translate unit according to above formula.
|
|---|
| 178 | * @param bName Second unit used to format text.
|
|---|
| 179 | * @param speedName the most common speed symbol (kmh/h, mph, kn, etc.)
|
|---|
| 180 | * @param speedValue the speed value for the most common speed symbol, for 1 meter per second
|
|---|
| 181 | * @since 15395
|
|---|
| 182 | */
|
|---|
| 183 | public SystemOfMeasurement(String name, double aValue, String aName, double bValue, String bName, String speedName, double speedValue) {
|
|---|
| 184 | this(name, aValue, aName, bValue, bName, speedName, speedValue, -1, null);
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | /**
|
|---|
| 188 | * System of measurement. Currently covers only length (and area) units.
|
|---|
| 189 | *
|
|---|
| 190 | * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
|
|---|
| 191 | * x_a == x_m / aValue
|
|---|
| 192 | *
|
|---|
| 193 | * @param name Translatable name of this system of measurement
|
|---|
| 194 | * @param aValue First value, in meters, used to translate unit according to above formula.
|
|---|
| 195 | * @param aName First unit used to format text.
|
|---|
| 196 | * @param bValue Second value, in meters, used to translate unit according to above formula.
|
|---|
| 197 | * @param bName Second unit used to format text.
|
|---|
| 198 | * @param speedName the most common speed symbol (kmh/h, mph, kn, etc.)
|
|---|
| 199 | * @param speedValue the speed value for the most common speed symbol, for 1 meter per second
|
|---|
| 200 | * @param areaCustomValue Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}.
|
|---|
| 201 | * Set to {@code -1} if not used.
|
|---|
| 202 | * @param areaCustomName Specific optional area unit. Set to {@code null} if not used.
|
|---|
| 203 | *
|
|---|
| 204 | * @since 15395
|
|---|
| 205 | */
|
|---|
| 206 | public SystemOfMeasurement(String name, double aValue, String aName, double bValue, String bName, String speedName, double speedValue,
|
|---|
| 207 | double areaCustomValue, String areaCustomName) {
|
|---|
| 208 | this.name = name;
|
|---|
| 209 | this.aValue = aValue;
|
|---|
| 210 | this.aName = aName;
|
|---|
| 211 | this.bValue = bValue;
|
|---|
| 212 | this.bName = bName;
|
|---|
| 213 | this.speedValue = speedValue;
|
|---|
| 214 | this.speedName = speedName;
|
|---|
| 215 | this.areaCustomValue = areaCustomValue;
|
|---|
| 216 | this.areaCustomName = areaCustomName;
|
|---|
| 217 | }
|
|---|
| 218 |
|
|---|
| 219 | /**
|
|---|
| 220 | * Returns the text describing the given distance in this system of measurement.
|
|---|
| 221 | * @param dist The distance in metres
|
|---|
| 222 | * @return The text describing the given distance in this system of measurement.
|
|---|
| 223 | */
|
|---|
| 224 | public String getDistText(double dist) {
|
|---|
| 225 | return getDistText(dist, null, 0.01);
|
|---|
| 226 | }
|
|---|
| 227 |
|
|---|
| 228 | /**
|
|---|
| 229 | * Returns the text describing the given distance in this system of measurement.
|
|---|
| 230 | * @param dist The distance in metres
|
|---|
| 231 | * @param format A {@link NumberFormat} to format the area value
|
|---|
| 232 | * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
|
|---|
| 233 | * @return The text describing the given distance in this system of measurement.
|
|---|
| 234 | * @since 6422
|
|---|
| 235 | */
|
|---|
| 236 | public String getDistText(final double dist, final NumberFormat format, final double threshold) {
|
|---|
| 237 | double a = dist / aValue;
|
|---|
| 238 | if (a > bValue / aValue && !Config.getPref().getBoolean("system_of_measurement.use_only_lower_unit", false))
|
|---|
| 239 | return formatText(dist / bValue, bName, format);
|
|---|
| 240 | else if (a < threshold)
|
|---|
| 241 | return "< " + formatText(threshold, aName, format);
|
|---|
| 242 | else
|
|---|
| 243 | return formatText(a, aName, format);
|
|---|
| 244 | }
|
|---|
| 245 |
|
|---|
| 246 | /**
|
|---|
| 247 | * Returns the text describing the given area in this system of measurement.
|
|---|
| 248 | * @param area The area in square metres
|
|---|
| 249 | * @return The text describing the given area in this system of measurement.
|
|---|
| 250 | * @since 5560
|
|---|
| 251 | */
|
|---|
| 252 | public String getAreaText(double area) {
|
|---|
| 253 | return getAreaText(area, null, 0.01);
|
|---|
| 254 | }
|
|---|
| 255 |
|
|---|
| 256 | /**
|
|---|
| 257 | * Returns the text describing the given area in this system of measurement.
|
|---|
| 258 | * @param area The area in square metres
|
|---|
| 259 | * @param format A {@link NumberFormat} to format the area value
|
|---|
| 260 | * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
|
|---|
| 261 | * @return The text describing the given area in this system of measurement.
|
|---|
| 262 | * @since 6422
|
|---|
| 263 | */
|
|---|
| 264 | public String getAreaText(final double area, final NumberFormat format, final double threshold) {
|
|---|
| 265 | double a = area / (aValue*aValue);
|
|---|
| 266 | boolean lowerOnly = Config.getPref().getBoolean("system_of_measurement.use_only_lower_unit", false);
|
|---|
| 267 | boolean customAreaOnly = Config.getPref().getBoolean("system_of_measurement.use_only_custom_area_unit", false);
|
|---|
| 268 | if ((!lowerOnly && areaCustomValue > 0 && a > areaCustomValue / (aValue*aValue)
|
|---|
| 269 | && a < (bValue*bValue) / (aValue*aValue)) || customAreaOnly)
|
|---|
| 270 | return formatText(area / areaCustomValue, areaCustomName, format);
|
|---|
| 271 | else if (!lowerOnly && a >= (bValue*bValue) / (aValue*aValue))
|
|---|
| 272 | return formatText(area / (bValue * bValue), bName + '\u00b2', format);
|
|---|
| 273 | else if (a < threshold)
|
|---|
| 274 | return "< " + formatText(threshold, aName + '\u00b2', format);
|
|---|
| 275 | else
|
|---|
| 276 | return formatText(a, aName + '\u00b2', format);
|
|---|
| 277 | }
|
|---|
| 278 |
|
|---|
| 279 | /**
|
|---|
| 280 | * Returns the translatable name of this system of measurement.
|
|---|
| 281 | * @return the translatable name of this system of measurement
|
|---|
| 282 | * @since 15395
|
|---|
| 283 | */
|
|---|
| 284 | public String getName() {
|
|---|
| 285 | return name;
|
|---|
| 286 | }
|
|---|
| 287 |
|
|---|
| 288 | /**
|
|---|
| 289 | * Returns the localized name of this system of measurement
|
|---|
| 290 | * @return the localized name
|
|---|
| 291 | */
|
|---|
| 292 | @Override
|
|---|
| 293 | public String toString() {
|
|---|
| 294 | return tr(name);
|
|---|
| 295 | }
|
|---|
| 296 |
|
|---|
| 297 | /**
|
|---|
| 298 | * Returns the default system of measurement for the current country.
|
|---|
| 299 | * @return the default system of measurement for the current country
|
|---|
| 300 | * @since 15395
|
|---|
| 301 | */
|
|---|
| 302 | public static SystemOfMeasurement getDefault() {
|
|---|
| 303 | final String country = Optional.ofNullable(System.getenv("LC_MEASUREMENT"))
|
|---|
| 304 | .map(LanguageInfo::getLocale)
|
|---|
| 305 | .orElse(Locale.getDefault())
|
|---|
| 306 | .getCountry();
|
|---|
| 307 | if ("US".equals(country))
|
|---|
| 308 | // https://en.wikipedia.org/wiki/Metrication_in_the_United_States#Current_use
|
|---|
| 309 | // Imperial units still used in transportation and Earth sciences
|
|---|
| 310 | return IMPERIAL;
|
|---|
| 311 | else
|
|---|
| 312 | return METRIC;
|
|---|
| 313 | }
|
|---|
| 314 |
|
|---|
| 315 | private static String formatText(double v, String unit, NumberFormat format) {
|
|---|
| 316 | if (format != null) {
|
|---|
| 317 | return format.format(v) + ' ' + unit;
|
|---|
| 318 | }
|
|---|
| 319 | return String.format(Locale.US, v < 9.999999 ? "%.2f %s" : "%.1f %s", v, unit);
|
|---|
| 320 | }
|
|---|
| 321 | }
|
|---|