| 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;
|
|---|
| 7 | import java.util.Collections;
|
|---|
| 8 | import java.util.LinkedHashMap;
|
|---|
| 9 | import java.util.Locale;
|
|---|
| 10 | import java.util.Map;
|
|---|
| 11 | import java.util.concurrent.CopyOnWriteArrayList;
|
|---|
| 12 |
|
|---|
| 13 | import org.openstreetmap.josm.Main;
|
|---|
| 14 | import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
|
|---|
| 15 |
|
|---|
| 16 | /**
|
|---|
| 17 | * A system of units used to express length and area measurements.
|
|---|
| 18 | * <p>
|
|---|
| 19 | * This class also manages one globally set system of measurement stored in the {@link ProjectionPreference}
|
|---|
| 20 | * @since 3406 (creation)
|
|---|
| 21 | * @since 6992 (extraction in this package)
|
|---|
| 22 | */
|
|---|
| 23 | public class SystemOfMeasurement {
|
|---|
| 24 |
|
|---|
| 25 | /**
|
|---|
| 26 | * Interface to notify listeners of the change of the system of measurement.
|
|---|
| 27 | * @since 8554
|
|---|
| 28 | * @since 10600 (functional interface)
|
|---|
| 29 | */
|
|---|
| 30 | @FunctionalInterface
|
|---|
| 31 | public interface SoMChangeListener {
|
|---|
| 32 | /**
|
|---|
| 33 | * The current SoM has changed.
|
|---|
| 34 | * @param oldSoM The old system of measurement
|
|---|
| 35 | * @param newSoM The new (current) system of measurement
|
|---|
| 36 | */
|
|---|
| 37 | void systemOfMeasurementChanged(String oldSoM, String newSoM);
|
|---|
| 38 | }
|
|---|
| 39 |
|
|---|
| 40 | /**
|
|---|
| 41 | * Metric system (international standard).
|
|---|
| 42 | * @since 3406
|
|---|
| 43 | */
|
|---|
| 44 | public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(1, "m", 1000, "km", "km/h", 3.6, 10_000, "ha");
|
|---|
| 45 |
|
|---|
| 46 | /**
|
|---|
| 47 | * Chinese system.
|
|---|
| 48 | * See <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_length_units_effective_in_1930">length units</a>,
|
|---|
| 49 | * <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_area_units_effective_in_1930">area units</a>
|
|---|
| 50 | * @since 3406
|
|---|
| 51 | */
|
|---|
| 52 | public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */,
|
|---|
| 53 | "km/h", 3.6, 666.0 + 2.0/3.0, "\u4ea9" /* mu */);
|
|---|
| 54 |
|
|---|
| 55 | /**
|
|---|
| 56 | * Imperial system (British Commonwealth and former British Empire).
|
|---|
| 57 | * @since 3406
|
|---|
| 58 | */
|
|---|
| 59 | public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", "mph", 2.23694, 4046.86, "ac");
|
|---|
| 60 |
|
|---|
| 61 | /**
|
|---|
| 62 | * Nautical mile system (navigation, polar exploration).
|
|---|
| 63 | * @since 5549
|
|---|
| 64 | */
|
|---|
| 65 | public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(185.2, "kbl", 1852, "NM", "kn", 1.94384);
|
|---|
| 66 |
|
|---|
| 67 | /**
|
|---|
| 68 | * Known systems of measurement.
|
|---|
| 69 | * @since 3406
|
|---|
| 70 | */
|
|---|
| 71 | public static final Map<String, SystemOfMeasurement> ALL_SYSTEMS;
|
|---|
| 72 | static {
|
|---|
| 73 | Map<String, SystemOfMeasurement> map = new LinkedHashMap<>();
|
|---|
| 74 | map.put(marktr("Metric"), METRIC);
|
|---|
| 75 | map.put(marktr("Chinese"), CHINESE);
|
|---|
| 76 | map.put(marktr("Imperial"), IMPERIAL);
|
|---|
| 77 | map.put(marktr("Nautical Mile"), NAUTICAL_MILE);
|
|---|
| 78 | ALL_SYSTEMS = Collections.unmodifiableMap(map);
|
|---|
| 79 | }
|
|---|
| 80 |
|
|---|
| 81 | private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<>();
|
|---|
| 82 |
|
|---|
| 83 | /**
|
|---|
| 84 | * Removes a global SoM change listener.
|
|---|
| 85 | *
|
|---|
| 86 | * @param listener the listener. Ignored if null or already absent
|
|---|
| 87 | * @since 8554
|
|---|
| 88 | */
|
|---|
| 89 | public static void removeSoMChangeListener(SoMChangeListener listener) {
|
|---|
| 90 | somChangeListeners.remove(listener);
|
|---|
| 91 | }
|
|---|
| 92 |
|
|---|
| 93 | /**
|
|---|
| 94 | * Adds a SoM change listener.
|
|---|
| 95 | *
|
|---|
| 96 | * @param listener the listener. Ignored if null or already registered.
|
|---|
| 97 | * @since 8554
|
|---|
| 98 | */
|
|---|
| 99 | 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 | }
|
|---|