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