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