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.concurrent.CopyOnWriteArrayList;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.gui.preferences.projection.ProjectionPreference;
015
016/**
017 * A system of units used to express length and area measurements.
018 * <p>
019 * This class also manages one globally set system of measurement stored in the {@link ProjectionPreference}
020 * @since 3406 (creation)
021 * @since 6992 (extraction in this package)
022 */
023public class SystemOfMeasurement {
024
025    /**
026     * Interface to notify listeners of the change of the system of measurement.
027     * @since 8554
028     * @since 10600 (functional interface)
029     */
030    @FunctionalInterface
031    public interface SoMChangeListener {
032        /**
033         * The current SoM has changed.
034         * @param oldSoM The old system of measurement
035         * @param newSoM The new (current) system of measurement
036         */
037        void systemOfMeasurementChanged(String oldSoM, String newSoM);
038    }
039
040    /**
041     * Metric system (international standard).
042     * @since 3406
043     */
044    public static final SystemOfMeasurement METRIC = new SystemOfMeasurement(1, "m", 1000, "km", "km/h", 3.6, 10_000, "ha");
045
046    /**
047     * Chinese system.
048     * See <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_length_units_effective_in_1930">length units</a>,
049     * <a href="https://en.wikipedia.org/wiki/Chinese_units_of_measurement#Chinese_area_units_effective_in_1930">area units</a>
050     * @since 3406
051     */
052    public static final SystemOfMeasurement CHINESE = new SystemOfMeasurement(1.0/3.0, "\u5e02\u5c3a" /* chi */, 500, "\u5e02\u91cc" /* li */,
053            "km/h", 3.6, 666.0 + 2.0/3.0, "\u4ea9" /* mu */);
054
055    /**
056     * Imperial system (British Commonwealth and former British Empire).
057     * @since 3406
058     */
059    public static final SystemOfMeasurement IMPERIAL = new SystemOfMeasurement(0.3048, "ft", 1609.344, "mi", "mph", 2.23694, 4046.86, "ac");
060
061    /**
062     * Nautical mile system (navigation, polar exploration).
063     * @since 5549
064     */
065    public static final SystemOfMeasurement NAUTICAL_MILE = new SystemOfMeasurement(185.2, "kbl", 1852, "NM", "kn", 1.94384);
066
067    /**
068     * Known systems of measurement.
069     * @since 3406
070     */
071    public static final Map<String, SystemOfMeasurement> ALL_SYSTEMS;
072    static {
073        Map<String, SystemOfMeasurement> map = new LinkedHashMap<>();
074        map.put(marktr("Metric"), METRIC);
075        map.put(marktr("Chinese"), CHINESE);
076        map.put(marktr("Imperial"), IMPERIAL);
077        map.put(marktr("Nautical Mile"), NAUTICAL_MILE);
078        ALL_SYSTEMS = Collections.unmodifiableMap(map);
079    }
080
081    private static final CopyOnWriteArrayList<SoMChangeListener> somChangeListeners = new CopyOnWriteArrayList<>();
082
083    /**
084     * Removes a global SoM change listener.
085     *
086     * @param listener the listener. Ignored if null or already absent
087     * @since 8554
088     */
089    public static void removeSoMChangeListener(SoMChangeListener listener) {
090        somChangeListeners.remove(listener);
091    }
092
093    /**
094     * Adds a SoM change listener.
095     *
096     * @param listener the listener. Ignored if null or already registered.
097     * @since 8554
098     */
099    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}