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