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