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     */
118    public static double toIntervalLon(double value) {
119        if (isValidLon(value))
120            return value;
121        else {
122            int n = (int) (value + Math.signum(value)*180.0) / 360;
123            return value - n*360.0;
124        }
125    }
126
127    /**
128     * Replies the coordinate in degrees/minutes/seconds format
129     * @param pCoordinate The coordinate to convert
130     * @return The coordinate in degrees/minutes/seconds format
131     */
132    public static String dms(double pCoordinate) {
133
134        double tAbsCoord = Math.abs(pCoordinate);
135        int tDegree = (int) tAbsCoord;
136        double tTmpMinutes = (tAbsCoord - tDegree) * 60;
137        int tMinutes = (int) tTmpMinutes;
138        double tSeconds = (tTmpMinutes - tMinutes) * 60;
139
140        String sDegrees = Integer.toString(tDegree);
141        String sMinutes = cDmsMinuteFormatter.format(tMinutes);
142        String sSeconds = cDmsSecondFormatter.format(tSeconds);
143
144        if (cDms60.equals(sSeconds)) {
145            sSeconds = cDms00;
146            sMinutes = cDmsMinuteFormatter.format(tMinutes+1);
147        }
148        if ("60".equals(sMinutes)) {
149            sMinutes = "00";
150            sDegrees = Integer.toString(tDegree+1);
151        }
152
153        return sDegrees + '\u00B0' + sMinutes + '\'' + sSeconds + '\"';
154    }
155
156    /**
157     * Replies the coordinate in degrees/minutes format
158     * @param pCoordinate The coordinate to convert
159     * @return The coordinate in degrees/minutes format
160     */
161    public static String dm(double pCoordinate) {
162
163        double tAbsCoord = Math.abs(pCoordinate);
164        int tDegree = (int) tAbsCoord;
165        double tMinutes = (tAbsCoord - tDegree) * 60;
166
167        String sDegrees = Integer.toString(tDegree);
168        String sMinutes = cDmMinuteFormatter.format(tMinutes);
169
170        if (sMinutes.equals(cDm60)) {
171            sMinutes = cDm00;
172            sDegrees = Integer.toString(tDegree+1);
173        }
174
175        return sDegrees + '\u00B0' + sMinutes + '\'';
176    }
177
178    /**
179     * Constructs a new {@link LatLon}
180     * @param lat the latitude, i.e., the north-south position in degrees
181     * @param lon the longitude, i.e., the east-west position in degrees
182     */
183    public LatLon(double lat, double lon) {
184        super(lon, lat);
185    }
186
187    protected LatLon(LatLon coor) {
188        super(coor.lon(), coor.lat());
189    }
190
191    public LatLon(ICoordinate coor) {
192        this(coor.getLat(), coor.getLon());
193    }
194
195
196    /**
197     * Returns the latitude, i.e., the north-south position in degrees.
198     * @return the latitude
199     */
200    public double lat() {
201        return y;
202    }
203
204    public static final String SOUTH = trc("compass", "S");
205    public static final String NORTH = trc("compass", "N");
206
207    public String latToString(CoordinateFormat d) {
208        switch(d) {
209        case DECIMAL_DEGREES: return cDdFormatter.format(y);
210        case DEGREES_MINUTES_SECONDS: return dms(y) + ((y < 0) ? SOUTH : NORTH);
211        case NAUTICAL: return dm(y) + ((y < 0) ? SOUTH : NORTH);
212        case EAST_NORTH: return cDdFormatter.format(Main.getProjection().latlon2eastNorth(this).north());
213        default: return "ERR";
214        }
215    }
216
217    /**
218     * Returns the longitude, i.e., the east-west position in degrees.
219     * @return the longitude
220     */
221    public double lon() {
222        return x;
223    }
224
225    public static final String WEST = trc("compass", "W");
226    public static final String EAST = trc("compass", "E");
227
228    public String lonToString(CoordinateFormat d) {
229        switch(d) {
230        case DECIMAL_DEGREES: return cDdFormatter.format(x);
231        case DEGREES_MINUTES_SECONDS: return dms(x) + ((x < 0) ? WEST : EAST);
232        case NAUTICAL: return dm(x) + ((x < 0) ? WEST : EAST);
233        case EAST_NORTH: return cDdFormatter.format(Main.getProjection().latlon2eastNorth(this).east());
234        default: return "ERR";
235        }
236    }
237
238    /**
239     * @return <code>true</code> if the other point has almost the same lat/lon
240     * values, only differing by no more than
241     * 1 / {@link #MAX_SERVER_PRECISION MAX_SERVER_PRECISION}.
242     */
243    public boolean equalsEpsilon(LatLon other) {
244        double p = MAX_SERVER_PRECISION / 2;
245        return Math.abs(lat()-other.lat()) <= p && Math.abs(lon()-other.lon()) <= p;
246    }
247
248    /**
249     * @return <code>true</code>, if the coordinate is outside the world, compared
250     * by using lat/lon.
251     */
252    public boolean isOutSideWorld() {
253        Bounds b = Main.getProjection().getWorldBoundsLatLon();
254        return lat() < b.getMinLat() || lat() > b.getMaxLat() ||
255                lon() < b.getMinLon() || lon() > b.getMaxLon();
256    }
257
258    /**
259     * @return <code>true</code> if this is within the given bounding box.
260     */
261    public boolean isWithin(Bounds b) {
262        return b.contains(this);
263    }
264
265    /**
266     * Check if this is contained in given area or area is null.
267     *
268     * @param a Area
269     * @return <code>true</code> if this is contained in given area or area is null.
270     */
271    public boolean isIn(Area a) {
272        return a == null || a.contains(x, y);
273    }
274
275    /**
276     * Computes the distance between this lat/lon and another point on the earth.
277     * Uses Haversine formular.
278     * @param other the other point.
279     * @return distance in metres.
280     */
281    public double greatCircleDistance(LatLon other) {
282        double R = 6378135;
283        double sinHalfLat = sin(toRadians(other.lat() - this.lat()) / 2);
284        double sinHalfLon = sin(toRadians(other.lon() - this.lon()) / 2);
285        double d = 2 * R * asin(
286                sqrt(sinHalfLat*sinHalfLat +
287                        cos(toRadians(this.lat()))*cos(toRadians(other.lat()))*sinHalfLon*sinHalfLon));
288        // For points opposite to each other on the sphere,
289        // rounding errors could make the argument of asin greater than 1
290        // (This should almost never happen.)
291        if (java.lang.Double.isNaN(d)) {
292            Main.error("NaN in greatCircleDistance");
293            d = PI * R;
294        }
295        return d;
296    }
297
298    /**
299     * Returns the heading, in radians, that you have to use to get from this lat/lon to another.
300     *
301     * (I don't know the original source of this formula, but see
302     * <a href="https://math.stackexchange.com/questions/720/how-to-calculate-a-heading-on-the-earths-surface">this question</a>
303     * for some hints how it is derived.)
304     *
305     * @param other the "destination" position
306     * @return heading in the range 0 &lt;= hd &lt; 2*PI
307     */
308    public double heading(LatLon other) {
309        double hd = atan2(sin(toRadians(this.lon() - other.lon())) * cos(toRadians(other.lat())),
310                cos(toRadians(this.lat())) * sin(toRadians(other.lat())) -
311                sin(toRadians(this.lat())) * cos(toRadians(other.lat())) * cos(toRadians(this.lon() - other.lon())));
312        hd %= 2 * PI;
313        if (hd < 0) {
314            hd += 2 * PI;
315        }
316        return hd;
317    }
318
319    /**
320     * Returns this lat/lon pair in human-readable format.
321     *
322     * @return String in the format "lat=1.23456 deg, lon=2.34567 deg"
323     */
324    public String toDisplayString() {
325        NumberFormat nf = NumberFormat.getInstance();
326        nf.setMaximumFractionDigits(5);
327        return "lat=" + nf.format(lat()) + "\u00B0, lon=" + nf.format(lon()) + '\u00B0';
328    }
329
330    /**
331     * Returns this lat/lon pair in human-readable format separated by {@code separator}.
332     * @return String in the format {@code "1.23456[separator]2.34567"}
333     */
334    public String toStringCSV(String separator) {
335        return Utils.join(separator, Arrays.asList(
336                latToString(CoordinateFormat.DECIMAL_DEGREES),
337                lonToString(CoordinateFormat.DECIMAL_DEGREES)
338        ));
339    }
340
341    public LatLon interpolate(LatLon ll2, double proportion) {
342        return new LatLon(this.lat() + proportion * (ll2.lat() - this.lat()),
343                this.lon() + proportion * (ll2.lon() - this.lon()));
344    }
345
346    public LatLon getCenter(LatLon ll2) {
347        return new LatLon((this.lat() + ll2.lat())/2.0, (this.lon() + ll2.lon())/2.0);
348    }
349
350    /**
351     * Returns the euclidean distance from this {@code LatLon} to a specified {@code LatLon}.
352     *
353     * @param ll the specified coordinate to be measured against this {@code LatLon}
354     * @return the euclidean distance from this {@code LatLon} to a specified {@code LatLon}
355     * @since 6166
356     */
357    public double distance(final LatLon ll) {
358        return super.distance(ll);
359    }
360
361    /**
362     * Returns the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}.
363     *
364     * @param ll the specified coordinate to be measured against this {@code LatLon}
365     * @return the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}
366     * @since 6166
367     */
368    public double distanceSq(final LatLon ll) {
369        return super.distanceSq(ll);
370    }
371
372    @Override
373    public String toString() {
374        return "LatLon[lat="+lat()+",lon="+lon()+']';
375    }
376
377    /**
378     * Returns the value rounded to OSM precisions, i.e. to
379     * LatLon.MAX_SERVER_PRECISION
380     *
381     * @return rounded value
382     */
383    public static double roundToOsmPrecision(double value) {
384        return Math.round(value * MAX_SERVER_INV_PRECISION) / MAX_SERVER_INV_PRECISION;
385    }
386
387    /**
388     * Returns the value rounded to OSM precision. This function is now the same as
389     * {@link #roundToOsmPrecision(double)}, since the rounding error has been fixed.
390     *
391     * @return rounded value
392     */
393    public static double roundToOsmPrecisionStrict(double value) {
394        return roundToOsmPrecision(value);
395    }
396
397    /**
398     * Replies a clone of this lat LatLon, rounded to OSM precisions, i.e. to
399     * MAX_SERVER_PRECISION
400     *
401     * @return a clone of this lat LatLon
402     */
403    public LatLon getRoundedToOsmPrecision() {
404        return new LatLon(
405                roundToOsmPrecision(lat()),
406                roundToOsmPrecision(lon())
407                );
408    }
409
410    /**
411     * Replies a clone of this lat LatLon, rounded to OSM precisions, i.e. to
412     * MAX_SERVER_PRECISION
413     *
414     * @return a clone of this lat LatLon
415     */
416    public LatLon getRoundedToOsmPrecisionStrict() {
417        return new LatLon(
418                roundToOsmPrecisionStrict(lat()),
419                roundToOsmPrecisionStrict(lon())
420                );
421    }
422
423    @Override
424    public int hashCode() {
425        return computeHashCode(super.hashCode());
426    }
427
428    @Override
429    public boolean equals(Object obj) {
430        if (this == obj)
431            return true;
432        if (!super.equals(obj))
433            return false;
434        if (getClass() != obj.getClass())
435            return false;
436        Coordinate other = (Coordinate) obj;
437        if (java.lang.Double.doubleToLongBits(x) != java.lang.Double.doubleToLongBits(other.x))
438            return false;
439        if (java.lang.Double.doubleToLongBits(y) != java.lang.Double.doubleToLongBits(other.y))
440            return false;
441        return true;
442    }
443
444    public ICoordinate toCoordinate() {
445        return new org.openstreetmap.gui.jmapviewer.Coordinate(lat(), lon());
446    }
447}