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