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 org.openstreetmap.josm.data.projection.Ellipsoid.WGS84; 011import static org.openstreetmap.josm.tools.Utils.toRadians; 012 013import java.awt.geom.Area; 014import java.text.DecimalFormat; 015import java.text.NumberFormat; 016import java.util.Locale; 017import java.util.Objects; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.data.Bounds; 021import org.openstreetmap.josm.tools.Logging; 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 implements ILatLon { 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 /** 049 * The inverse of the server precision 050 * @see #MAX_SERVER_PRECISION 051 */ 052 public static final double MAX_SERVER_INV_PRECISION = 1e7; 053 054 /** 055 * The (0,0) coordinates. 056 * @since 6178 057 */ 058 public static final LatLon ZERO = new LatLon(0, 0); 059 060 /** North pole. */ 061 public static final LatLon NORTH_POLE = new LatLon(90, 0); 062 /** South pole. */ 063 public static final LatLon SOUTH_POLE = new LatLon(-90, 0); 064 065 /** 066 * The normal number format for server precision coordinates 067 */ 068 public static final DecimalFormat cDdFormatter; 069 /** 070 * The number format used for high precision coordinates 071 */ 072 public static final DecimalFormat cDdHighPecisionFormatter; 073 static { 074 // Don't use the localized decimal separator. This way we can present 075 // a comma separated list of coordinates. 076 cDdFormatter = (DecimalFormat) NumberFormat.getInstance(Locale.UK); 077 cDdFormatter.applyPattern("###0.0######"); 078 cDdHighPecisionFormatter = (DecimalFormat) NumberFormat.getInstance(Locale.UK); 079 cDdHighPecisionFormatter.applyPattern("###0.0##########"); 080 } 081 082 /** 083 * Replies true if lat is in the range [-90,90] 084 * 085 * @param lat the latitude 086 * @return true if lat is in the range [-90,90] 087 */ 088 public static boolean isValidLat(double lat) { 089 return lat >= -90d && lat <= 90d; 090 } 091 092 /** 093 * Replies true if lon is in the range [-180,180] 094 * 095 * @param lon the longitude 096 * @return true if lon is in the range [-180,180] 097 */ 098 public static boolean isValidLon(double lon) { 099 return lon >= -180d && lon <= 180d; 100 } 101 102 /** 103 * Make sure longitude value is within <code>[-180, 180]</code> range. 104 * @param lon the longitude in degrees 105 * @return lon plus/minus multiples of <code>360</code>, as needed to get 106 * in <code>[-180, 180]</code> range 107 */ 108 public static double normalizeLon(double lon) { 109 if (lon >= -180 && lon <= 180) 110 return lon; 111 else { 112 lon = lon % 360.0; 113 if (lon > 180) { 114 return lon - 360; 115 } else if (lon < -180) { 116 return lon + 360; 117 } 118 return lon; 119 } 120 } 121 122 /** 123 * Replies true if lat is in the range [-90,90] and lon is in the range [-180,180] 124 * 125 * @return true if lat is in the range [-90,90] and lon is in the range [-180,180] 126 */ 127 public boolean isValid() { 128 return isValidLat(lat()) && isValidLon(lon()); 129 } 130 131 /** 132 * Clamp the lat value to be inside the world. 133 * @param value The value 134 * @return The value clamped to the world. 135 */ 136 public static double toIntervalLat(double value) { 137 return Utils.clamp(value, -90, 90); 138 } 139 140 /** 141 * Returns a valid OSM longitude [-180,+180] for the given extended longitude value. 142 * For example, a value of -181 will return +179, a value of +181 will return -179. 143 * @param value A longitude value not restricted to the [-180,+180] range. 144 * @return a valid OSM longitude [-180,+180] 145 */ 146 public static double toIntervalLon(double value) { 147 if (isValidLon(value)) 148 return value; 149 else { 150 int n = (int) (value + Math.signum(value)*180.0) / 360; 151 return value - n*360.0; 152 } 153 } 154 155 /** 156 * Constructs a new object representing the given latitude/longitude. 157 * @param lat the latitude, i.e., the north-south position in degrees 158 * @param lon the longitude, i.e., the east-west position in degrees 159 */ 160 public LatLon(double lat, double lon) { 161 super(lon, lat); 162 } 163 164 /** 165 * Creates a new LatLon object for the given coordinate 166 * @param coor The coordinates to copy from. 167 */ 168 public LatLon(ILatLon coor) { 169 super(coor.lon(), coor.lat()); 170 } 171 172 @Override 173 public double lat() { 174 return y; 175 } 176 177 @Override 178 public double lon() { 179 return x; 180 } 181 182 /** 183 * @param other other lat/lon 184 * @return <code>true</code> if the other point has almost the same lat/lon 185 * values, only differing by no more than 1 / {@link #MAX_SERVER_PRECISION MAX_SERVER_PRECISION}. 186 */ 187 public boolean equalsEpsilon(LatLon other) { 188 double p = MAX_SERVER_PRECISION / 2; 189 return Math.abs(lat()-other.lat()) <= p && Math.abs(lon()-other.lon()) <= p; 190 } 191 192 /** 193 * Determines if this lat/lon is outside of the world 194 * @return <code>true</code>, if the coordinate is outside the world, compared by using lat/lon. 195 */ 196 public boolean isOutSideWorld() { 197 Bounds b = Main.getProjection().getWorldBoundsLatLon(); 198 return lat() < b.getMinLat() || lat() > b.getMaxLat() || 199 lon() < b.getMinLon() || lon() > b.getMaxLon(); 200 } 201 202 /** 203 * Determines if this lat/lon is within the given bounding box. 204 * @param b bounding box 205 * @return <code>true</code> if this is within the given bounding box. 206 */ 207 public boolean isWithin(Bounds b) { 208 return b.contains(this); 209 } 210 211 /** 212 * Check if this is contained in given area or area is null. 213 * 214 * @param a Area 215 * @return <code>true</code> if this is contained in given area or area is null. 216 */ 217 public boolean isIn(Area a) { 218 return a == null || a.contains(x, y); 219 } 220 221 /** 222 * Computes the distance between this lat/lon and another point on the earth. 223 * Uses Haversine formular. 224 * @param other the other point. 225 * @return distance in metres. 226 */ 227 public double greatCircleDistance(LatLon other) { 228 double sinHalfLat = sin(toRadians(other.lat() - this.lat()) / 2); 229 double sinHalfLon = sin(toRadians(other.lon() - this.lon()) / 2); 230 double d = 2 * WGS84.a * asin( 231 sqrt(sinHalfLat*sinHalfLat + 232 cos(toRadians(this.lat()))*cos(toRadians(other.lat()))*sinHalfLon*sinHalfLon)); 233 // For points opposite to each other on the sphere, 234 // rounding errors could make the argument of asin greater than 1 235 // (This should almost never happen.) 236 if (java.lang.Double.isNaN(d)) { 237 Logging.error("NaN in greatCircleDistance"); 238 d = PI * WGS84.a; 239 } 240 return d; 241 } 242 243 /** 244 * Returns the heading that you have to use to get from this lat/lon to another. 245 * 246 * Angle starts from north and increases counterclockwise (!), PI/2 means west. 247 * You can get usual clockwise angle from {@link #bearing(LatLon)} method. 248 * This method is kept as deprecated because it is called from many plugins. 249 * 250 * (I don't know the original source of this formula, but see 251 * <a href="https://math.stackexchange.com/questions/720/how-to-calculate-a-heading-on-the-earths-surface">this question</a> 252 * for some hints how it is derived.) 253 * 254 * @deprecated see bearing method 255 * @param other the "destination" position 256 * @return heading in radians in the range 0 <= hd < 2*PI 257 */ 258 @Deprecated 259 public double heading(LatLon other) { 260 double hd = atan2(sin(toRadians(this.lon() - other.lon())) * cos(toRadians(other.lat())), 261 cos(toRadians(this.lat())) * sin(toRadians(other.lat())) - 262 sin(toRadians(this.lat())) * cos(toRadians(other.lat())) * cos(toRadians(this.lon() - other.lon()))); 263 hd %= 2 * PI; 264 if (hd < 0) { 265 hd += 2 * PI; 266 } 267 return hd; 268 } 269 270 /** 271 * Returns bearing from this point to another. 272 * 273 * Angle starts from north and increases clockwise, PI/2 means east. 274 * Old deprecated method {@link #heading(LatLon)} used unusual reverse angle. 275 * 276 * Please note that reverse bearing (from other point to this point) should NOT be 277 * calculated from return value of this method, because great circle path 278 * between the two points have different bearings at each position. 279 * 280 * To get bearing from another point to this point call other.bearing(this) 281 * 282 * @param other the "destination" position 283 * @return heading in radians in the range 0 <= hd < 2*PI 284 */ 285 public double bearing(LatLon other) { 286 double lat1 = toRadians(this.lat()); 287 double lat2 = toRadians(other.lat()); 288 double dlon = toRadians(other.lon() - this.lon()); 289 double bearing = atan2( 290 sin(dlon) * cos(lat2), 291 cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dlon) 292 ); 293 bearing %= 2 * PI; 294 if (bearing < 0) { 295 bearing += 2 * PI; 296 } 297 return bearing; 298 } 299 300 /** 301 * Returns this lat/lon pair in human-readable format. 302 * 303 * @return String in the format "lat=1.23456 deg, lon=2.34567 deg" 304 */ 305 public String toDisplayString() { 306 NumberFormat nf = NumberFormat.getInstance(); 307 nf.setMaximumFractionDigits(5); 308 return "lat=" + nf.format(lat()) + "\u00B0, lon=" + nf.format(lon()) + '\u00B0'; 309 } 310 311 /** 312 * Interpolate between this and a other latlon 313 * @param ll2 The other lat/lon object 314 * @param proportion The proportion to interpolate 315 * @return a new latlon at this position if proportion is 0, at the other position it proportion is 1 and lineary interpolated otherwise. 316 */ 317 public LatLon interpolate(LatLon ll2, double proportion) { 318 // this is an alternate form of this.lat() + proportion * (ll2.lat() - this.lat()) that is slightly faster 319 return new LatLon((1 - proportion) * this.lat() + proportion * ll2.lat(), 320 (1 - proportion) * this.lon() + proportion * ll2.lon()); 321 } 322 323 /** 324 * Get the center between two lat/lon points 325 * @param ll2 The other {@link LatLon} 326 * @return The center at the average coordinates of the two points. Does not take the 180° meridian into account. 327 */ 328 public LatLon getCenter(LatLon ll2) { 329 // The JIT will inline this for us, it is as fast as the normal /2 approach 330 return interpolate(ll2, .5); 331 } 332 333 /** 334 * Returns the euclidean distance from this {@code LatLon} to a specified {@code LatLon}. 335 * 336 * @param ll the specified coordinate to be measured against this {@code LatLon} 337 * @return the euclidean distance from this {@code LatLon} to a specified {@code LatLon} 338 * @since 6166 339 */ 340 public double distance(final LatLon ll) { 341 return super.distance(ll); 342 } 343 344 /** 345 * Returns the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon}. 346 * 347 * @param ll the specified coordinate to be measured against this {@code LatLon} 348 * @return the square of the euclidean distance from this {@code LatLon} to a specified {@code LatLon} 349 * @since 6166 350 */ 351 public double distanceSq(final LatLon ll) { 352 return super.distanceSq(ll); 353 } 354 355 @Override 356 public String toString() { 357 return "LatLon[lat="+lat()+",lon="+lon()+']'; 358 } 359 360 /** 361 * Returns the value rounded to OSM precisions, i.e. to {@link #MAX_SERVER_PRECISION}. 362 * @param value lat/lon value 363 * 364 * @return rounded value 365 */ 366 public static double roundToOsmPrecision(double value) { 367 return Math.round(value * MAX_SERVER_INV_PRECISION) / MAX_SERVER_INV_PRECISION; 368 } 369 370 /** 371 * Replies a clone of this lat LatLon, rounded to OSM precisions, i.e. to {@link #MAX_SERVER_PRECISION} 372 * 373 * @return a clone of this lat LatLon 374 */ 375 public LatLon getRoundedToOsmPrecision() { 376 return new LatLon( 377 roundToOsmPrecision(lat()), 378 roundToOsmPrecision(lon()) 379 ); 380 } 381 382 @Override 383 public int hashCode() { 384 return Objects.hash(x, y); 385 } 386 387 @Override 388 public boolean equals(Object obj) { 389 if (this == obj) return true; 390 if (obj == null || getClass() != obj.getClass()) return false; 391 LatLon that = (LatLon) obj; 392 return Double.compare(that.x, x) == 0 && 393 Double.compare(that.y, y) == 0; 394 } 395}