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