[7937] | 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 |
|
---|
| 6 | import java.text.NumberFormat;
|
---|
[10215] | 7 | import java.util.Collections;
|
---|
[7937] | 8 | import java.util.LinkedHashMap;
|
---|
| 9 | import java.util.Locale;
|
---|
| 10 | import java.util.Map;
|
---|
[11553] | 11 | import java.util.Optional;
|
---|
[8554] | 12 | import java.util.concurrent.CopyOnWriteArrayList;
|
---|
[7937] | 13 |
|
---|
[12674] | 14 | import org.openstreetmap.josm.data.preferences.StringProperty;
|
---|
[12846] | 15 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
[7937] | 16 |
|
---|
| 17 | /**
|
---|
| 18 | * A system of units used to express length and area measurements.
|
---|
[8554] | 19 | * <p>
|
---|
[12674] | 20 | * This class also manages one globally set system of measurement stored in the {@code ProjectionPreference}
|
---|
[7937] | 21 | * @since 3406 (creation)
|
---|
[8510] | 22 | * @since 6992 (extraction in this package)
|
---|
[7937] | 23 | */
|
---|
| 24 | public class SystemOfMeasurement {
|
---|
| 25 |
|
---|
| 26 | /**
|
---|
[12674] | 27 | * Preferences entry for system of measurement.
|
---|
| 28 | * @since 12674 (moved from ProjectionPreference)
|
---|
| 29 | */
|
---|
| 30 | public static final StringProperty PROP_SYSTEM_OF_MEASUREMENT = new StringProperty("system_of_measurement", "Metric");
|
---|
| 31 |
|
---|
| 32 | /**
|
---|
[8554] | 33 | * Interface to notify listeners of the change of the system of measurement.
|
---|
[8558] | 34 | * @since 8554
|
---|
[10600] | 35 | * @since 10600 (functional interface)
|
---|
[8554] | 36 | */
|
---|
[10600] | 37 | @FunctionalInterface
|
---|
[8554] | 38 | public interface SoMChangeListener {
|
---|
| 39 | /**
|
---|
| 40 | * The current SoM has changed.
|
---|
| 41 | * @param oldSoM The old system of measurement
|
---|
| 42 | * @param newSoM The new (current) system of measurement
|
---|
| 43 | */
|
---|
| 44 | void systemOfMeasurementChanged(String oldSoM, String newSoM);
|
---|
| 45 | }
|
---|
| 46 |
|
---|
| 47 | /**
|
---|
[7937] | 48 | * Metric system (international standard).
|
---|
| 49 | * @since 3406
|
---|
| 50 | */
|
---|
[11100] | 51 | public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(1, "m", 1000, "km", "km/h", 3.6, 10_000, "ha");
|
---|
[7937] | 52 |
|
---|
| 53 | /**
|
---|
| 54 | * Chinese system.
|
---|
[10175] | 55 | * See <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_length_units_effective_in_1930">length units</a>,
|
---|
| 56 | * <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_area_units_effective_in_1930">area units</a>
|
---|
[7937] | 57 | * @since 3406
|
---|
| 58 | */
|
---|
[10175] | 59 | public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */,
|
---|
| 60 | "km/h", 3.6, 666.0 + 2.0/3.0, "\u4ea9" /* mu */);
|
---|
[7937] | 61 |
|
---|
| 62 | /**
|
---|
| 63 | * Imperial system (British Commonwealth and former British Empire).
|
---|
| 64 | * @since 3406
|
---|
| 65 | */
|
---|
[10175] | 66 | public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", "mph", 2.23694, 4046.86, "ac");
|
---|
[7937] | 67 |
|
---|
| 68 | /**
|
---|
| 69 | * Nautical mile system (navigation, polar exploration).
|
---|
| 70 | * @since 5549
|
---|
| 71 | */
|
---|
[10175] | 72 | public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(185.2, "kbl", 1852, "NM", "kn", 1.94384);
|
---|
[8510] | 73 |
|
---|
[7937] | 74 | /**
|
---|
| 75 | * Known systems of measurement.
|
---|
| 76 | * @since 3406
|
---|
| 77 | */
|
---|
| 78 | public static final Map<String, SystemOfMeasurement> ALL_SYSTEMS;
|
---|
| 79 | static {
|
---|
[10215] | 80 | Map<String, SystemOfMeasurement> map = new LinkedHashMap<>();
|
---|
| 81 | map.put(marktr("Metric"), METRIC);
|
---|
| 82 | map.put(marktr("Chinese"), CHINESE);
|
---|
| 83 | map.put(marktr("Imperial"), IMPERIAL);
|
---|
| 84 | map.put(marktr("Nautical Mile"), NAUTICAL_MILE);
|
---|
| 85 | ALL_SYSTEMS = Collections.unmodifiableMap(map);
|
---|
[7937] | 86 | }
|
---|
[8510] | 87 |
|
---|
[12542] | 88 | private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<>();
|
---|
[8554] | 89 |
|
---|
| 90 | /**
|
---|
[8558] | 91 | * Removes a global SoM change listener.
|
---|
[8554] | 92 | *
|
---|
| 93 | * @param listener the listener. Ignored if null or already absent
|
---|
[8558] | 94 | * @since 8554
|
---|
[8554] | 95 | */
|
---|
| 96 | public static void removeSoMChangeListener(SoMChangeListener listener) {
|
---|
[12542] | 97 | somChangeListeners.remove(listener);
|
---|
[8554] | 98 | }
|
---|
| 99 |
|
---|
| 100 | /**
|
---|
[8558] | 101 | * Adds a SoM change listener.
|
---|
[8554] | 102 | *
|
---|
| 103 | * @param listener the listener. Ignored if null or already registered.
|
---|
[8558] | 104 | * @since 8554
|
---|
[8554] | 105 | */
|
---|
| 106 | public static void addSoMChangeListener(SoMChangeListener listener) {
|
---|
| 107 | if (listener != null) {
|
---|
[12542] | 108 | somChangeListeners.addIfAbsent(listener);
|
---|
[8554] | 109 | }
|
---|
| 110 | }
|
---|
| 111 |
|
---|
| 112 | protected static void fireSoMChanged(String oldSoM, String newSoM) {
|
---|
[12542] | 113 | for (SoMChangeListener l : somChangeListeners) {
|
---|
[8554] | 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).
|
---|
[8558] | 121 | * @since 8554
|
---|
[8554] | 122 | */
|
---|
| 123 | public static SystemOfMeasurement getSystemOfMeasurement() {
|
---|
[12674] | 124 | return Optional.ofNullable(SystemOfMeasurement.ALL_SYSTEMS.get(PROP_SYSTEM_OF_MEASUREMENT.get()))
|
---|
[11553] | 125 | .orElse(SystemOfMeasurement.METRIC);
|
---|
[8554] | 126 | }
|
---|
| 127 |
|
---|
| 128 | /**
|
---|
| 129 | * Sets the current global system of measurement.
|
---|
| 130 | * @param somKey The system of measurement key. Must be defined in {@link SystemOfMeasurement#ALL_SYSTEMS}.
|
---|
| 131 | * @throws IllegalArgumentException if {@code somKey} is not known
|
---|
[8558] | 132 | * @since 8554
|
---|
[8554] | 133 | */
|
---|
| 134 | public static void setSystemOfMeasurement(String somKey) {
|
---|
| 135 | if (!SystemOfMeasurement.ALL_SYSTEMS.containsKey(somKey)) {
|
---|
| 136 | throw new IllegalArgumentException("Invalid system of measurement: "+somKey);
|
---|
| 137 | }
|
---|
[12674] | 138 | String oldKey = PROP_SYSTEM_OF_MEASUREMENT.get();
|
---|
| 139 | if (PROP_SYSTEM_OF_MEASUREMENT.put(somKey)) {
|
---|
[8554] | 140 | fireSoMChanged(oldKey, somKey);
|
---|
| 141 | }
|
---|
| 142 | }
|
---|
| 143 |
|
---|
[7937] | 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;
|
---|
[10175] | 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;
|
---|
[7937] | 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 aValue First value, in meters, used to translate unit according to above formula.
|
---|
| 172 | * @param aName First unit used to format text.
|
---|
| 173 | * @param bValue Second value, in meters, used to translate unit according to above formula.
|
---|
| 174 | * @param bName Second unit used to format text.
|
---|
[10175] | 175 | * @param speedName the most common speed symbol (kmh/h, mph, kn, etc.)
|
---|
| 176 | * @param speedValue the speed value for the most common speed symbol, for 1 meter per second
|
---|
| 177 | * @since 10175
|
---|
[7937] | 178 | */
|
---|
[10175] | 179 | public SystemOfMeasurement(double aValue, String aName, double bValue, String bName, String speedName, double speedValue) {
|
---|
| 180 | this(aValue, aName, bValue, bName, speedName, speedValue, -1, null);
|
---|
[7937] | 181 | }
|
---|
| 182 |
|
---|
| 183 | /**
|
---|
| 184 | * System of measurement. Currently covers only length (and area) units.
|
---|
| 185 | *
|
---|
| 186 | * If a quantity x is given in m (x_m) and in unit a (x_a) then it translates as
|
---|
| 187 | * x_a == x_m / aValue
|
---|
| 188 | *
|
---|
| 189 | * @param aValue First value, in meters, used to translate unit according to above formula.
|
---|
| 190 | * @param aName First unit used to format text.
|
---|
| 191 | * @param bValue Second value, in meters, used to translate unit according to above formula.
|
---|
| 192 | * @param bName Second unit used to format text.
|
---|
[10175] | 193 | * @param speedName the most common speed symbol (kmh/h, mph, kn, etc.)
|
---|
| 194 | * @param speedValue the speed value for the most common speed symbol, for 1 meter per second
|
---|
[7937] | 195 | * @param areaCustomValue Specific optional area value, in squared meters, between {@code aValue*aValue} and {@code bValue*bValue}.
|
---|
| 196 | * Set to {@code -1} if not used.
|
---|
| 197 | * @param areaCustomName Specific optional area unit. Set to {@code null} if not used.
|
---|
| 198 | *
|
---|
[10175] | 199 | * @since 10175
|
---|
[7937] | 200 | */
|
---|
[10175] | 201 | public SystemOfMeasurement(double aValue, String aName, double bValue, String bName, String speedName, double speedValue,
|
---|
| 202 | double areaCustomValue, String areaCustomName) {
|
---|
[7937] | 203 | this.aValue = aValue;
|
---|
| 204 | this.aName = aName;
|
---|
| 205 | this.bValue = bValue;
|
---|
| 206 | this.bName = bName;
|
---|
[10175] | 207 | this.speedValue = speedValue;
|
---|
| 208 | this.speedName = speedName;
|
---|
[7937] | 209 | this.areaCustomValue = areaCustomValue;
|
---|
| 210 | this.areaCustomName = areaCustomName;
|
---|
| 211 | }
|
---|
| 212 |
|
---|
| 213 | /**
|
---|
| 214 | * Returns the text describing the given distance in this system of measurement.
|
---|
| 215 | * @param dist The distance in metres
|
---|
| 216 | * @return The text describing the given distance in this system of measurement.
|
---|
| 217 | */
|
---|
| 218 | public String getDistText(double dist) {
|
---|
| 219 | return getDistText(dist, null, 0.01);
|
---|
| 220 | }
|
---|
| 221 |
|
---|
| 222 | /**
|
---|
| 223 | * Returns the text describing the given distance in this system of measurement.
|
---|
| 224 | * @param dist The distance in metres
|
---|
| 225 | * @param format A {@link NumberFormat} to format the area value
|
---|
| 226 | * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
|
---|
| 227 | * @return The text describing the given distance in this system of measurement.
|
---|
| 228 | * @since 6422
|
---|
| 229 | */
|
---|
| 230 | public String getDistText(final double dist, final NumberFormat format, final double threshold) {
|
---|
| 231 | double a = dist / aValue;
|
---|
[12846] | 232 | if (a > bValue / aValue && !Config.getPref().getBoolean("system_of_measurement.use_only_lower_unit", false))
|
---|
[7937] | 233 | return formatText(dist / bValue, bName, format);
|
---|
| 234 | else if (a < threshold)
|
---|
| 235 | return "< " + formatText(threshold, aName, format);
|
---|
| 236 | else
|
---|
| 237 | return formatText(a, aName, format);
|
---|
| 238 | }
|
---|
| 239 |
|
---|
| 240 | /**
|
---|
| 241 | * Returns the text describing the given area in this system of measurement.
|
---|
| 242 | * @param area The area in square metres
|
---|
| 243 | * @return The text describing the given area in this system of measurement.
|
---|
| 244 | * @since 5560
|
---|
| 245 | */
|
---|
| 246 | public String getAreaText(double area) {
|
---|
| 247 | return getAreaText(area, null, 0.01);
|
---|
| 248 | }
|
---|
| 249 |
|
---|
| 250 | /**
|
---|
| 251 | * Returns the text describing the given area in this system of measurement.
|
---|
| 252 | * @param area The area in square metres
|
---|
| 253 | * @param format A {@link NumberFormat} to format the area value
|
---|
| 254 | * @param threshold Values lower than this {@code threshold} are displayed as {@code "< [threshold]"}
|
---|
| 255 | * @return The text describing the given area in this system of measurement.
|
---|
| 256 | * @since 6422
|
---|
| 257 | */
|
---|
| 258 | public String getAreaText(final double area, final NumberFormat format, final double threshold) {
|
---|
| 259 | double a = area / (aValue*aValue);
|
---|
[12846] | 260 | boolean lowerOnly = Config.getPref().getBoolean("system_of_measurement.use_only_lower_unit", false);
|
---|
| 261 | boolean customAreaOnly = Config.getPref().getBoolean("system_of_measurement.use_only_custom_area_unit", false);
|
---|
[8540] | 262 | if ((!lowerOnly && areaCustomValue > 0 && a > areaCustomValue / (aValue*aValue)
|
---|
| 263 | && a < (bValue*bValue) / (aValue*aValue)) || customAreaOnly)
|
---|
[7937] | 264 | return formatText(area / areaCustomValue, areaCustomName, format);
|
---|
| 265 | else if (!lowerOnly && a >= (bValue*bValue) / (aValue*aValue))
|
---|
[8846] | 266 | return formatText(area / (bValue * bValue), bName + '\u00b2', format);
|
---|
[7937] | 267 | else if (a < threshold)
|
---|
[8846] | 268 | return "< " + formatText(threshold, aName + '\u00b2', format);
|
---|
[7937] | 269 | else
|
---|
[8846] | 270 | return formatText(a, aName + '\u00b2', format);
|
---|
[7937] | 271 | }
|
---|
| 272 |
|
---|
| 273 | private static String formatText(double v, String unit, NumberFormat format) {
|
---|
| 274 | if (format != null) {
|
---|
[8846] | 275 | return format.format(v) + ' ' + unit;
|
---|
[7937] | 276 | }
|
---|
[10292] | 277 | return String.format(Locale.US, v < 9.999999 ? "%.2f %s" : "%.1f %s", v, unit);
|
---|
[7937] | 278 | }
|
---|
| 279 | }
|
---|