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