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