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 <= hd < 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}