001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.coor;
003
004import static java.lang.Math.PI;
005import static java.lang.Math.asin;
006import static java.lang.Math.atan2;
007import static java.lang.Math.cos;
008import static java.lang.Math.sin;
009import static java.lang.Math.sqrt;
010import static java.lang.Math.toRadians;
011import static org.openstreetmap.josm.tools.I18n.trc;
012
013import java.awt.geom.Area;
014import java.text.DecimalFormat;
015import java.text.NumberFormat;
016import java.util.Arrays;
017import java.util.Locale;
018
019import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
020import org.openstreetmap.josm.Main;
021import org.openstreetmap.josm.data.Bounds;
022import org.openstreetmap.josm.tools.Utils;
023
024/**
025 * LatLon are unprojected latitude / longitude coordinates.
026 * <br>
027 * <b>Latitude</b> specifies the north-south position in degrees
028 * where valid values are in the [-90,90] and positive values specify positions north of the equator.
029 * <br>
030 * <b>Longitude</b> specifies the east-west position in degrees
031 * where valid values are in the [-180,180] and positive values specify positions east of the prime meridian.
032 * <br>
033 * <img alt="lat/lon" src="https://upload.wikimedia.org/wikipedia/commons/6/62/Latitude_and_Longitude_of_the_Earth.svg">
034 * <br>
035 * This class is immutable.
036 *
037 * @author Imi
038 */
039public class LatLon extends Coordinate {
040
041    private static final long serialVersionUID = 1L;
042
043    /**
044     * Minimum difference in location to not be represented as the same position.
045     * The API returns 7 decimals.
046     */
047    public static final double MAX_SERVER_PRECISION = 1e-7;
048    public static final double MAX_SERVER_INV_PRECISION = 1e7;
049    public static final int    MAX_SERVER_DIGITS = 7;
050
051    /**
052     * The (0,0) coordinates.
053     * @since 6178
054     */
055    public static final LatLon ZERO = new LatLon(0, 0);
056
057    private static DecimalFormat cDmsMinuteFormatter = new DecimalFormat("00");
058    private static DecimalFormat cDmsSecondFormatter = new DecimalFormat("00.0");
059    private static DecimalFormat cDmMinuteFormatter = new DecimalFormat("00.000");
060    public static final DecimalFormat cDdFormatter;
061    public static final DecimalFormat cDdHighPecisionFormatter;
062    static {
063        // Don't use the localized decimal separator. This way we can present
064        // a comma separated list of coordinates.
065        cDdFormatter = (DecimalFormat) NumberFormat.getInstance(Locale.UK);
066        cDdFormatter.applyPattern("###0.0######");
067        cDdHighPecisionFormatter = (DecimalFormat) NumberFormat.getInstance(Locale.UK);
068        cDdHighPecisionFormatter.applyPattern("###0.0##########");
069    }
070
071    private static final String cDms60 = cDmsSecondFormatter.format(60.0);
072    private static final String cDms00 = cDmsSecondFormatter.format(0.0);
073    private static final String cDm60 = cDmMinuteFormatter.format(60.0);
074    private static final String cDm00 = cDmMinuteFormatter.format(0.0);
075
076    /**
077     * Replies true if lat is in the range [-90,90]
078     *
079     * @param lat the latitude
080     * @return true if lat is in the range [-90,90]
081     */
082    public static boolean isValidLat(double lat) {
083        return lat >= -90d && lat <= 90d;
084    }
085
086    /**
087     * Replies true if lon is in the range [-180,180]
088     *
089     * @param lon the longitude
090     * @return true if lon is in the range [-180,180]
091     */
092    public static boolean isValidLon(double lon) {
093        return lon >= -180d && lon <= 180d;
094    }
095
096    /**
097     * Replies true if lat is in the range [-90,90] and lon is in the range [-180,180]
098     *
099     * @return true if lat is in the range [-90,90] and lon is in the range [-180,180]
100     */
101    public boolean isValid() {
102        return isValidLat(lat()) && isValidLon(lon());
103    }
104
105    public static double toIntervalLat(double value) {
106        if (value < -90)
107            return -90;
108        if (value > 90)
109            return 90;
110        return value;
111    }
112
113    /**
114     * Returns a valid OSM longitude [-180,+180] for the given extended longitude value.
115     * For example, a value of -181 will return +179, a value of +181 will return -179.
116     * @param value A longitude value not restricted to the [-180,+180] range.
117     * @return a valid OSM longitude [-180,+180]
118     */
119    public static double toIntervalLon(double value) {
120        if (isValidLon(value))
121            return value;
122        else {
123            int n = (int) (value + Math.signum(value)*180.0) / 360;
124            return value - n*360.0;
125        }
126    }
127
128    /**
129     * Replies the coordinate in degrees/minutes/seconds format
130     * @param pCoordinate The coordinate to convert
131     * @return The coordinate in degrees/minutes/seconds format
132     */
133    public static String dms(double pCoordinate) {
134
135        double tAbsCoord = Math.abs(pCoordinate);
136        int tDegree = (int) tAbsCoord;
137        double tTmpMinutes = (tAbsCoord - tDegree) * 60;
138        int tMinutes = (int) tTmpMinutes;
139        double tSeconds = (tTmpMinutes - tMinutes) * 60;
140
141        String sDegrees = Integer.toString(tDegree);
142        String sMinutes = cDmsMinuteFormatter.format(tMinutes);
143        String sSeconds = cDmsSecondFormatter.format(tSeconds);
144
145        if (cDms60.equals(sSeconds)) {
146            sSeconds = cDms00;
147            sMinutes = cDmsMinuteFormatter.format(tMinutes+1);
148        }
149        if ("60".equals(sMinutes)) {
150            sMinutes = "00";
151            sDegrees = Integer.toString(tDegree+1);
152        }
153
154        return sDegrees + '\u00B0' + sMinutes + '\'' + sSeconds + '\"';
155    }
156
157    /**
158     * Replies the coordinate in degrees/minutes format
159     * @param pCoordinate The coordinate to convert
160     * @return The coordinate in degrees/minutes format
161     */
162    public static String dm(double pCoordinate) {
163
164        double tAbsCoord = Math.abs(pCoordinate);
165        int tDegree = (int) tAbsCoord;
166        double tMinutes = (tAbsCoord - tDegree) * 60;
167
168        String sDegrees = Integer.toString(tDegree);
169        String sMinutes = cDmMinuteFormatter.format(tMinutes);
170
171        if (sMinutes.equals(cDm60)) {
172            sMinutes = cDm00;
173            sDegrees = Integer.toString(tDegree+1);
174        }
175
176        return sDegrees + '\u00B0' + sMinutes + '\'';
177    }
178
179    /**
180     * Constructs a new {@link LatLon}
181     * @param lat the latitude, i.e., the north-south position in degrees
182     * @param lon the longitude, i.e., the east-west position in degrees
183     */
184    public LatLon(double lat, double lon) {
185        super(lon, lat);
186    }
187
188    protected LatLon(LatLon coor) {
189        super(coor.lon(), coor.lat());
190    }
191
192    public LatLon(ICoordinate coor) {
193        this(coor.getLat(), coor.getLon());
194    }
195
196
197    /**
198     * Returns the latitude, i.e., the north-south position in degrees.
199     * @return the latitude
200     */
201    public double lat() {
202        return y;
203    }
204
205    public static final String SOUTH = trc("compass", "S");
206    public static final String NORTH = trc("compass", "N");
207
208    public String latToString(CoordinateFormat d) {
209        switch(d) {
210        case DECIMAL_DEGREES: return cDdFormatter.format(y);
211        case DEGREES_MINUTES_SECONDS: return dms(y) + ((y < 0) ? SOUTH : NORTH);
212        case NAUTICAL: return dm(y) + ((y < 0) ? SOUTH : NORTH);
213        case EAST_NORTH: return cDdFormatter.format(Main.getProjection().latlon2eastNorth(this).north());
214        default: return "ERR";
215        }
216    }
217
218    /**
219     * Returns the longitude, i.e., the east-west position in degrees.
220     * @return the longitude
221     */
222    public double lon() {
223        return x;
224    }
225
226    public static final String WEST = trc("compass", "W");
227    public static final String EAST = trc("compass", "E");
228
229    public String lonToString(CoordinateFormat d) {
230        switch(d) {
231        case DECIMAL_DEGREES: return cDdFormatter.format(x);
232        case DEGREES_MINUTES_SECONDS: return dms(x) + ((x < 0) ? WEST : EAST);
233        case NAUTICAL: return dm(x) + ((x < 0) ? WEST : EAST);
234        case EAST_NORTH: return cDdFormatter.format(Main.getProjection().latlon2eastNorth(this).east());
235        default: return "ERR";
236        }
237    }
238
239    /**
240     * @param other other lat/lon
241     * @return <code>true</code> if the other point has almost the same lat/lon
242     * values, only differing by no more than 1 / {@link #MAX_SERVER_PRECISION MAX_SERVER_PRECISION}.
243     */
244    public boolean equalsEpsilon(LatLon other) {
245        double p = MAX_SERVER_PRECISION / 2;
246        return Math.abs(lat()-other.lat()) <= p && Math.abs(lon()-other.lon()) <= p;
247    }
248
249    /**
250     * Determines if this lat/lon is outside of the world
251     * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon.
252     */
253    public boolean isOutSideWorld() {
254        Bounds b = Main.getProjection().getWorldBoundsLatLon();
255        return lat() < b.getMinLat() || lat() > b.getMaxLat() ||
256                lon() < b.getMinLon() || lon() > b.getMaxLon();
257    }
258
259    /**
260     * Determines if this lat/lon is within the given bounding box.
261     * @param b bounding box
262     * @return <code>true</code> if this is within the given bounding box.
263     */
264    public boolean isWithin(Bounds b) {
265        return b.contains(this);
266    }
267
268    /**
269     * Check if this is contained in given area or area is null.
270     *
271     * @param a Area
272     * @return <code>true</code> if this is contained in given area or area is null.
273     */
274    public boolean isIn(Area a) {
275        return a == null || a.contains(x, y);
276    }
277
278    /**
279     * Computes the distance between this lat/lon and another point on the earth.
280     * Uses Haversine formular.
281     * @param other the other point.
282     * @return distance in metres.
283     */
284    public double greatCircleDistance(LatLon other) {
285        double R = 6378135;
286        double sinHalfLat = sin(toRadians(other.lat() - this.lat()) / 2);
287        double sinHalfLon = sin(toRadians(other.lon() - this.lon()) / 2);
288        double d = 2 * R * asin(
289                sqrt(sinHalfLat*sinHalfLat +
290                        cos(toRadians(this.lat()))*cos(toRadians(other.lat()))*sinHalfLon*sinHalfLon));
291        // For points opposite to each other on the sphere,
292        // rounding errors could make the argument of asin greater than 1
293        // (This should almost never happen.)
294        if (java.lang.Double.isNaN(d)) {
295            Main.error("NaN in greatCircleDistance");
296            d = PI * R;
297        }
298        return d;
299    }
300
301    /**
302     * Returns the heading, in radians, that you have to use to get from this lat/lon to another.
303     *
304     * (I don't know the original source of this formula, but see
305     * <a href="https://math.stackexchange.com/questions/720/how-to-calculate-a-heading-on-the-earths-surface">this question</a>
306     * for some hints how it is derived.)
307     *
308     * @param other the "destination" position
309     * @return heading in the range 0 &lt;= hd &lt; 2*PI
310     */
311    public double heading(LatLon other) {
312        double hd = atan2(sin(toRadians(this.lon() - other.lon())) * cos(toRadians(other.lat())),
313                cos(toRadians(this.lat())) * sin(toRadians(other.lat())) -
314                sin(toRadians(this.lat())) * cos(toRadians(other.lat())) * cos(toRadians(this.lon() - other.lon())));
315        hd %= 2 * PI;
316        if (hd < 0) {
317            hd += 2 * PI;
318        }
319        return hd;
320    }
321
322    /**
323     * Returns this lat/lon pair in human-readable format.
324     *
325     * @return String in the format "lat=1.23456 deg, lon=2.34567 deg"
326     */
327    public String toDisplayString() {
328        NumberFormat nf = NumberFormat.getInstance();
329        nf.setMaximumFractionDigits(5);
330        return "lat=" + nf.format(lat()) + "\u00B0, lon=" + nf.format(lon()) + '\u00B0';
331    }
332
333    /**
334     * Returns this lat/lon pair in human-readable format separated by {@code separator}.
335     * @param separator values separator
336     * @return String in the format {@code "1.23456[separator]2.34567"}
337     */
338    public String toStringCSV(String separator) {
339        return Utils.join(separator, Arrays.asList(
340                latToString(CoordinateFormat.DECIMAL_DEGREES),
341                lonToString(CoordinateFormat.DECIMAL_DEGREES)
342        ));
343    }
344
345    public LatLon interpolate(LatLon ll2, double proportion) {
346        return new LatLon(this.lat() + proportion * (ll2.lat() - this.lat()),
347                this.lon() + proportion * (ll2.lon() - this.lon()));
348    }
349
350    public LatLon getCenter(LatLon ll2) {
351        return new LatLon((this.lat() + ll2.lat())/2.0, (this.lon() + ll2.lon())/2.0);
352    }
353
354    /**
355     * Returns the euclidean distance from this {@code LatLon} to a specified {@code LatLon}.
356     *
357     * @param ll the specified coordinate to be measured against this {@code LatLon}
358     * @return the euclidean distance from this {@code LatLon} to a specified {@code LatLon}
359     * @since 6166
360     */
361    public double distance(final LatLon ll) {
362        return super.distance(ll);
363    }
364
365    /**
366     * Returns the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}.
367     *
368     * @param ll the specified coordinate to be measured against this {@code LatLon}
369     * @return the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}
370     * @since 6166
371     */
372    public double distanceSq(final LatLon ll) {
373        return super.distanceSq(ll);
374    }
375
376    @Override
377    public String toString() {
378        return "LatLon[lat="+lat()+",lon="+lon()+']';
379    }
380
381    /**
382     * Returns the value rounded to OSM precisions, i.e. to {@link LatLon#MAX_SERVER_PRECISION}.
383     * @param value lat/lon value
384     *
385     * @return rounded value
386     */
387    public static double roundToOsmPrecision(double value) {
388        return Math.round(value * MAX_SERVER_INV_PRECISION) / MAX_SERVER_INV_PRECISION;
389    }
390
391    /**
392     * Returns the value rounded to OSM precision. This function is now the same as
393     * {@link #roundToOsmPrecision(double)}, since the rounding error has been fixed.
394     * @param value lat/lon value
395     *
396     * @return rounded value
397     */
398    public static double roundToOsmPrecisionStrict(double value) {
399        return roundToOsmPrecision(value);
400    }
401
402    /**
403     * Replies a clone of this lat LatLon, rounded to OSM precisions, i.e. to
404     * MAX_SERVER_PRECISION
405     *
406     * @return a clone of this lat LatLon
407     */
408    public LatLon getRoundedToOsmPrecision() {
409        return new LatLon(
410                roundToOsmPrecision(lat()),
411                roundToOsmPrecision(lon())
412                );
413    }
414
415    /**
416     * Replies a clone of this lat LatLon, rounded to OSM precisions, i.e. to
417     * MAX_SERVER_PRECISION
418     *
419     * @return a clone of this lat LatLon
420     */
421    public LatLon getRoundedToOsmPrecisionStrict() {
422        return new LatLon(
423                roundToOsmPrecisionStrict(lat()),
424                roundToOsmPrecisionStrict(lon())
425                );
426    }
427
428    @Override
429    public int hashCode() {
430        return computeHashCode(super.hashCode());
431    }
432
433    @Override
434    public boolean equals(Object obj) {
435        if (this == obj)
436            return true;
437        if (!super.equals(obj))
438            return false;
439        if (getClass() != obj.getClass())
440            return false;
441        Coordinate other = (Coordinate) obj;
442        if (java.lang.Double.doubleToLongBits(x) != java.lang.Double.doubleToLongBits(other.x))
443            return false;
444        if (java.lang.Double.doubleToLongBits(y) != java.lang.Double.doubleToLongBits(other.y))
445            return false;
446        return true;
447    }
448
449    public ICoordinate toCoordinate() {
450        return new org.openstreetmap.gui.jmapviewer.Coordinate(lat(), lon());
451    }
452}