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.LinkedHashMap;
009import java.util.Locale;
010import java.util.Map;
011import java.util.Optional;
012import java.util.concurrent.CopyOnWriteArrayList;
013
014import org.openstreetmap.josm.data.preferences.StringProperty;
015import org.openstreetmap.josm.spi.preferences.Config;
016
017/**
018 * A system of units used to express length and area measurements.
019 * <p>
020 * This class also manages one globally set system of measurement stored in the {@code ProjectionPreference}
021 * @since 3406 (creation)
022 * @since 6992 (extraction in this package)
023 */
024public class SystemOfMeasurement {
025
026    /**
027     * Preferences entry for system of measurement.
028     * @since 12674 (moved from ProjectionPreference)
029     */
030    public static final StringProperty PROP_SYSTEM_OF_MEASUREMENT = new StringProperty("system_of_measurement", "Metric");
031
032    /**
033     * Interface to notify listeners of the change of the system of measurement.
034     * @since 8554
035     * @since 10600 (functional interface)
036     */
037    @FunctionalInterface
038    public interface SoMChangeListener {
039        /**
040         * The current SoM has changed.
041         * @param oldSoM The old system of measurement
042         * @param newSoM The new (current) system of measurement
043         */
044        void systemOfMeasurementChanged(String oldSoM, String newSoM);
045    }
046
047    /**
048     * Metric system (international standard).
049     * @since 3406
050     */
051    public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(1, "m", 1000, "km", "km/h", 3.6, 10_000, "ha");
052
053    /**
054     * Chinese system.
055     * See <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_length_units_effective_in_1930">length units</a>,
056     * <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_area_units_effective_in_1930">area units</a>
057     * @since 3406
058     */
059    public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */,
060            "km/h", 3.6, 666.0 + 2.0/3.0, "\u4ea9" /* mu */);
061
062    /**
063     * Imperial system (British Commonwealth and former British Empire).
064     * @since 3406
065     */
066    public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", "mph", 2.23694, 4046.86, "ac");
067
068    /**
069     * Nautical mile system (navigation, polar exploration).
070     * @since 5549
071     */
072    public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(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;
079    static {
080        Map<String, SystemOfMeasurement> map = new LinkedHashMap<>();
081        map.put(marktr("Metric"), METRIC);
082        map.put(marktr("Chinese"), CHINESE);
083        map.put(marktr("Imperial"), IMPERIAL);
084        map.put(marktr("Nautical Mile"), NAUTICAL_MILE);
085        ALL_SYSTEMS = Collections.unmodifiableMap(map);
086    }
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 somKey The system of measurement key. Must be defined in {@link SystemOfMeasurement#ALL_SYSTEMS}.
131     * @throws IllegalArgumentException if {@code somKey} is not known
132     * @since 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        }
138        String oldKey = PROP_SYSTEM_OF_MEASUREMENT.get();
139        if (PROP_SYSTEM_OF_MEASUREMENT.put(somKey)) {
140            fireSoMChanged(oldKey, somKey);
141        }
142    }
143
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;
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;
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.
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
178     */
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);
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.
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
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     *
199     * @since 10175
200     */
201    public SystemOfMeasurement(double aValue, String aName, double bValue, String bName, String speedName, double speedValue,
202            double areaCustomValue, String areaCustomName) {
203        this.aValue = aValue;
204        this.aName = aName;
205        this.bValue = bValue;
206        this.bName = bName;
207        this.speedValue = speedValue;
208        this.speedName = speedName;
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;
232        if (a > bValue / aValue && !Config.getPref().getBoolean("system_of_measurement.use_only_lower_unit", false))
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);
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);
262        if ((!lowerOnly && areaCustomValue > 0 && a > areaCustomValue / (aValue*aValue)
263                && a < (bValue*bValue) / (aValue*aValue)) || customAreaOnly)
264            return formatText(area / areaCustomValue, areaCustomName, format);
265        else if (!lowerOnly && a >= (bValue*bValue) / (aValue*aValue))
266            return formatText(area / (bValue * bValue), bName + '\u00b2', format);
267        else if (a < threshold)
268            return "< " + formatText(threshold, aName + '\u00b2', format);
269        else
270            return formatText(a, aName + '\u00b2', format);
271    }
272
273    private static String formatText(double v, String unit, NumberFormat format) {
274        if (format != null) {
275            return format.format(v) + ' ' + unit;
276        }
277        return String.format(Locale.US, v < 9.999999 ? "%.2f %s" : "%.1f %s", v, unit);
278    }
279}