001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.text.NumberFormat;
008import java.util.Collections;
009import java.util.Locale;
010import java.util.Map;
011import java.util.Optional;
012import java.util.concurrent.CopyOnWriteArrayList;
013import java.util.function.Function;
014import java.util.stream.Collectors;
015import java.util.stream.Stream;
016
017import org.openstreetmap.josm.data.preferences.StringProperty;
018import org.openstreetmap.josm.spi.preferences.Config;
019import org.openstreetmap.josm.tools.LanguageInfo;
020
021/**
022 * A system of units used to express length and area measurements.
023 * <p>
024 * This class also manages one globally set system of measurement stored in the {@code ProjectionPreference}
025 * @since 3406 (creation)
026 * @since 6992 (extraction in this package)
027 */
028public class SystemOfMeasurement {
029
030    /**
031     * Interface to notify listeners of the change of the system of measurement.
032     * @since 8554
033     * @since 10600 (functional interface)
034     */
035    @FunctionalInterface
036    public interface SoMChangeListener {
037        /**
038         * The current SoM has changed.
039         * @param oldSoM The old system of measurement
040         * @param newSoM The new (current) system of measurement
041         */
042        void systemOfMeasurementChanged(String oldSoM, String newSoM);
043    }
044
045    /**
046     * Metric system (international standard).
047     * @since 3406
048     */
049    public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(marktr("Metric"), 1, "m", 1000, "km", "km/h", 3.6, 10_000, "ha");
050
051    /**
052     * Chinese system.
053     * See <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_length_units_effective_in_1930">length units</a>,
054     * <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_area_units_effective_in_1930">area units</a>
055     * @since 3406
056     */
057    public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(marktr("Chinese"),
058            1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */, "km/h", 3.6, 666.0 + 2.0/3.0, "\u4ea9" /* mu */);
059
060    /**
061     * Imperial system (British Commonwealth and former British Empire).
062     * @since 3406
063     */
064    public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(marktr("Imperial"),
065            0.3048, "ft", 1609.344, "mi", "mph", 2.23694, 4046.86, "ac");
066
067    /**
068     * Nautical mile system (navigation, polar exploration).
069     * @since 5549
070     */
071    public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(marktr("Nautical Mile"),
072            185.2, "kbl", 1852, "NM", "kn", 1.94384);
073
074    /**
075     * Known systems of measurement.
076     * @since 3406
077     */
078    public static final Map<String, SystemOfMeasurement> ALL_SYSTEMS = Collections.unmodifiableMap(
079            Stream.of(METRIC, CHINESE, IMPERIAL, NAUTICAL_MILE)
080            .collect(Collectors.toMap(SystemOfMeasurement::getName, Function.identity())));
081
082    /**
083     * Preferences entry for system of measurement.
084     * @since 12674 (moved from ProjectionPreference)
085     */
086    public static final StringProperty PROP_SYSTEM_OF_MEASUREMENT = new StringProperty("system_of_measurement", getDefault().getName());
087
088    private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<>();
089
090    /**
091     * Removes a global SoM change listener.
092     *
093     * @param listener the listener. Ignored if null or already absent
094     * @since 8554
095     */
096    public static void removeSoMChangeListener(SoMChangeListener listener) {
097        somChangeListeners.remove(listener);
098    }
099
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        switch (country) {
308            case "US":
309                // https://en.wikipedia.org/wiki/Metrication_in_the_United_States#Current_use
310                // Imperial units still used in transportation and Earth sciences
311                return IMPERIAL;
312            default:
313                return METRIC;
314        }
315    }
316
317    private static String formatText(double v, String unit, NumberFormat format) {
318        if (format != null) {
319            return format.format(v) + ' ' + unit;
320        }
321        return String.format(Locale.US, v < 9.999999 ? "%.2f %s" : "%.1f %s", v, unit);
322    }
323}