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