001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.geom.Rectangle2D; 007import java.text.DecimalFormat; 008import java.text.MessageFormat; 009 010import org.openstreetmap.josm.data.coor.LatLon; 011import org.openstreetmap.josm.data.osm.BBox; 012import org.openstreetmap.josm.tools.CheckParameterUtil; 013 014/** 015 * This is a simple data class for "rectangular" areas of the world, given in 016 * lat/lon min/max values. The values are rounded to LatLon.OSM_SERVER_PRECISION 017 * 018 * @author imi 019 */ 020public class Bounds { 021 /** 022 * The minimum and maximum coordinates. 023 */ 024 private double minLat, minLon, maxLat, maxLon; 025 026 public LatLon getMin() { 027 return new LatLon(minLat, minLon); 028 } 029 030 /** 031 * Returns min latitude of bounds. Efficient shortcut for {@code getMin().lat()}. 032 * 033 * @return min latitude of bounds. 034 * @since 6203 035 */ 036 public double getMinLat() { 037 return minLat; 038 } 039 040 /** 041 * Returns min longitude of bounds. Efficient shortcut for {@code getMin().lon()}. 042 * 043 * @return min longitude of bounds. 044 * @since 6203 045 */ 046 public double getMinLon() { 047 return minLon; 048 } 049 050 public LatLon getMax() { 051 return new LatLon(maxLat, maxLon); 052 } 053 054 /** 055 * Returns max latitude of bounds. Efficient shortcut for {@code getMax().lat()}. 056 * 057 * @return max latitude of bounds. 058 * @since 6203 059 */ 060 public double getMaxLat() { 061 return maxLat; 062 } 063 064 /** 065 * Returns max longitude of bounds. Efficient shortcut for {@code getMax().lon()}. 066 * 067 * @return max longitude of bounds. 068 * @since 6203 069 */ 070 public double getMaxLon() { 071 return maxLon; 072 } 073 074 public enum ParseMethod { 075 MINLAT_MINLON_MAXLAT_MAXLON, 076 LEFT_BOTTOM_RIGHT_TOP 077 } 078 079 /** 080 * Construct bounds out of two points. Coords will be rounded. 081 * @param min min lat/lon 082 * @param max max lat/lon 083 */ 084 public Bounds(LatLon min, LatLon max) { 085 this(min.lat(), min.lon(), max.lat(), max.lon()); 086 } 087 088 public Bounds(LatLon min, LatLon max, boolean roundToOsmPrecision) { 089 this(min.lat(), min.lon(), max.lat(), max.lon(), roundToOsmPrecision); 090 } 091 092 /** 093 * Constructs bounds out a single point. 094 * @param b lat/lon 095 */ 096 public Bounds(LatLon b) { 097 this(b, true); 098 } 099 100 /** 101 * Single point Bounds defined by lat/lon {@code b}. 102 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true. 103 * 104 * @param b lat/lon of given point. 105 * @param roundToOsmPrecision defines if lat/lon will be rounded. 106 */ 107 public Bounds(LatLon b, boolean roundToOsmPrecision) { 108 this(b.lat(), b.lon(), roundToOsmPrecision); 109 } 110 111 /** 112 * Single point Bounds defined by point [lat,lon]. 113 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true. 114 * 115 * @param lat latitude of given point. 116 * @param lon longitude of given point. 117 * @param roundToOsmPrecision defines if lat/lon will be rounded. 118 * @since 6203 119 */ 120 public Bounds(double lat, double lon, boolean roundToOsmPrecision) { 121 // Do not call this(b, b) to avoid GPX performance issue (see #7028) until roundToOsmPrecision() is improved 122 if (roundToOsmPrecision) { 123 this.minLat = LatLon.roundToOsmPrecision(lat); 124 this.minLon = LatLon.roundToOsmPrecision(lon); 125 } else { 126 this.minLat = lat; 127 this.minLon = lon; 128 } 129 this.maxLat = this.minLat; 130 this.maxLon = this.minLon; 131 } 132 133 public Bounds(double minlat, double minlon, double maxlat, double maxlon) { 134 this(minlat, minlon, maxlat, maxlon, true); 135 } 136 137 public Bounds(double minlat, double minlon, double maxlat, double maxlon, boolean roundToOsmPrecision) { 138 if (roundToOsmPrecision) { 139 this.minLat = LatLon.roundToOsmPrecision(minlat); 140 this.minLon = LatLon.roundToOsmPrecision(minlon); 141 this.maxLat = LatLon.roundToOsmPrecision(maxlat); 142 this.maxLon = LatLon.roundToOsmPrecision(maxlon); 143 } else { 144 this.minLat = minlat; 145 this.minLon = minlon; 146 this.maxLat = maxlat; 147 this.maxLon = maxlon; 148 } 149 } 150 151 public Bounds(double[] coords) { 152 this(coords, true); 153 } 154 155 public Bounds(double[] coords, boolean roundToOsmPrecision) { 156 CheckParameterUtil.ensureParameterNotNull(coords, "coords"); 157 if (coords.length != 4) 158 throw new IllegalArgumentException(MessageFormat.format("Expected array of length 4, got {0}", coords.length)); 159 if (roundToOsmPrecision) { 160 this.minLat = LatLon.roundToOsmPrecision(coords[0]); 161 this.minLon = LatLon.roundToOsmPrecision(coords[1]); 162 this.maxLat = LatLon.roundToOsmPrecision(coords[2]); 163 this.maxLon = LatLon.roundToOsmPrecision(coords[3]); 164 } else { 165 this.minLat = coords[0]; 166 this.minLon = coords[1]; 167 this.maxLat = coords[2]; 168 this.maxLon = coords[3]; 169 } 170 } 171 172 public Bounds(String asString, String separator) { 173 this(asString, separator, ParseMethod.MINLAT_MINLON_MAXLAT_MAXLON); 174 } 175 176 public Bounds(String asString, String separator, ParseMethod parseMethod) { 177 this(asString, separator, parseMethod, true); 178 } 179 180 public Bounds(String asString, String separator, ParseMethod parseMethod, boolean roundToOsmPrecision) { 181 CheckParameterUtil.ensureParameterNotNull(asString, "asString"); 182 String[] components = asString.split(separator); 183 if (components.length != 4) 184 throw new IllegalArgumentException( 185 MessageFormat.format("Exactly four doubles expected in string, got {0}: {1}", components.length, asString)); 186 double[] values = new double[4]; 187 for (int i = 0; i < 4; i++) { 188 try { 189 values[i] = Double.parseDouble(components[i]); 190 } catch (NumberFormatException e) { 191 throw new IllegalArgumentException(MessageFormat.format("Illegal double value ''{0}''", components[i]), e); 192 } 193 } 194 195 switch (parseMethod) { 196 case LEFT_BOTTOM_RIGHT_TOP: 197 this.minLat = initLat(values[1], roundToOsmPrecision); 198 this.minLon = initLon(values[0], roundToOsmPrecision); 199 this.maxLat = initLat(values[3], roundToOsmPrecision); 200 this.maxLon = initLon(values[2], roundToOsmPrecision); 201 break; 202 case MINLAT_MINLON_MAXLAT_MAXLON: 203 default: 204 this.minLat = initLat(values[0], roundToOsmPrecision); 205 this.minLon = initLon(values[1], roundToOsmPrecision); 206 this.maxLat = initLat(values[2], roundToOsmPrecision); 207 this.maxLon = initLon(values[3], roundToOsmPrecision); 208 } 209 } 210 211 protected static double initLat(double value, boolean roundToOsmPrecision) { 212 if (!LatLon.isValidLat(value)) 213 throw new IllegalArgumentException(tr("Illegal latitude value ''{0}''", value)); 214 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value; 215 } 216 217 protected static double initLon(double value, boolean roundToOsmPrecision) { 218 if (!LatLon.isValidLon(value)) 219 throw new IllegalArgumentException(tr("Illegal longitude value ''{0}''", value)); 220 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value; 221 } 222 223 /** 224 * Creates new {@code Bounds} from an existing one. 225 * @param other The bounds to copy 226 */ 227 public Bounds(final Bounds other) { 228 this(other.minLat, other.minLon, other.maxLat, other.maxLon); 229 } 230 231 public Bounds(Rectangle2D rect) { 232 this(rect.getMinY(), rect.getMinX(), rect.getMaxY(), rect.getMaxX()); 233 } 234 235 /** 236 * Creates new bounds around a coordinate pair <code>center</code>. The 237 * new bounds shall have an extension in latitude direction of <code>latExtent</code>, 238 * and in longitude direction of <code>lonExtent</code>. 239 * 240 * @param center the center coordinate pair. Must not be null. 241 * @param latExtent the latitude extent. > 0 required. 242 * @param lonExtent the longitude extent. > 0 required. 243 * @throws IllegalArgumentException if center is null 244 * @throws IllegalArgumentException if latExtent <= 0 245 * @throws IllegalArgumentException if lonExtent <= 0 246 */ 247 public Bounds(LatLon center, double latExtent, double lonExtent) { 248 CheckParameterUtil.ensureParameterNotNull(center, "center"); 249 if (latExtent <= 0.0) 250 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "latExtent", latExtent)); 251 if (lonExtent <= 0.0) 252 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "lonExtent", lonExtent)); 253 254 this.minLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() - latExtent / 2)); 255 this.minLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() - lonExtent / 2)); 256 this.maxLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() + latExtent / 2)); 257 this.maxLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() + lonExtent / 2)); 258 } 259 260 /** 261 * Creates BBox with same coordinates. 262 * 263 * @return BBox with same coordinates. 264 * @since 6203 265 */ 266 public BBox toBBox() { 267 return new BBox(minLon, minLat, maxLon, maxLat); 268 } 269 270 @Override 271 public String toString() { 272 return "Bounds["+minLat+','+minLon+','+maxLat+','+maxLon+']'; 273 } 274 275 public String toShortString(DecimalFormat format) { 276 return format.format(minLat) + ' ' 277 + format.format(minLon) + " / " 278 + format.format(maxLat) + ' ' 279 + format.format(maxLon); 280 } 281 282 /** 283 * @return Center of the bounding box. 284 */ 285 public LatLon getCenter() { 286 if (crosses180thMeridian()) { 287 double lat = (minLat + maxLat) / 2; 288 double lon = (minLon + maxLon - 360.0) / 2; 289 if (lon < -180.0) { 290 lon += 360.0; 291 } 292 return new LatLon(lat, lon); 293 } else { 294 return new LatLon((minLat + maxLat) / 2, (minLon + maxLon) / 2); 295 } 296 } 297 298 /** 299 * Extend the bounds if necessary to include the given point. 300 * @param ll The point to include into these bounds 301 */ 302 public void extend(LatLon ll) { 303 extend(ll.lat(), ll.lon()); 304 } 305 306 /** 307 * Extend the bounds if necessary to include the given point [lat,lon]. 308 * Good to use if you know coordinates to avoid creation of LatLon object. 309 * @param lat Latitude of point to include into these bounds 310 * @param lon Longitude of point to include into these bounds 311 * @since 6203 312 */ 313 public void extend(final double lat, final double lon) { 314 if (lat < minLat) { 315 minLat = LatLon.roundToOsmPrecision(lat); 316 } 317 if (lat > maxLat) { 318 maxLat = LatLon.roundToOsmPrecision(lat); 319 } 320 if (crosses180thMeridian()) { 321 if (lon > maxLon && lon < minLon) { 322 if (Math.abs(lon - minLon) <= Math.abs(lon - maxLon)) { 323 minLon = LatLon.roundToOsmPrecision(lon); 324 } else { 325 maxLon = LatLon.roundToOsmPrecision(lon); 326 } 327 } 328 } else { 329 if (lon < minLon) { 330 minLon = LatLon.roundToOsmPrecision(lon); 331 } 332 if (lon > maxLon) { 333 maxLon = LatLon.roundToOsmPrecision(lon); 334 } 335 } 336 } 337 338 public void extend(Bounds b) { 339 extend(b.minLat, b.minLon); 340 extend(b.maxLat, b.maxLon); 341 } 342 343 /** 344 * Determines if the given point {@code ll} is within these bounds. 345 * @param ll The lat/lon to check 346 * @return {@code true} if {@code ll} is within these bounds, {@code false} otherwise 347 */ 348 public boolean contains(LatLon ll) { 349 if (ll.lat() < minLat || ll.lat() > maxLat) 350 return false; 351 if (crosses180thMeridian()) { 352 if (ll.lon() > maxLon && ll.lon() < minLon) 353 return false; 354 } else { 355 if (ll.lon() < minLon || ll.lon() > maxLon) 356 return false; 357 } 358 return true; 359 } 360 361 private static boolean intersectsLonCrossing(Bounds crossing, Bounds notCrossing) { 362 return notCrossing.minLon <= crossing.maxLon || notCrossing.maxLon >= crossing.minLon; 363 } 364 365 /** 366 * The two bounds intersect? Compared to java Shape.intersects, if does not use 367 * the interior but the closure. (">=" instead of ">") 368 * @param b other bounds 369 * @return {@code true} if the two bounds intersect 370 */ 371 public boolean intersects(Bounds b) { 372 if (b.maxLat < minLat || b.minLat > maxLat) 373 return false; 374 375 if (crosses180thMeridian() && !b.crosses180thMeridian()) { 376 return intersectsLonCrossing(this, b); 377 } else if (!crosses180thMeridian() && b.crosses180thMeridian()) { 378 return intersectsLonCrossing(b, this); 379 } else if (crosses180thMeridian() && b.crosses180thMeridian()) { 380 return true; 381 } else { 382 return b.maxLon >= minLon && b.minLon <= maxLon; 383 } 384 } 385 386 /** 387 * Determines if this Bounds object crosses the 180th Meridian. 388 * See http://wiki.openstreetmap.org/wiki/180th_meridian 389 * @return true if this Bounds object crosses the 180th Meridian. 390 */ 391 public boolean crosses180thMeridian() { 392 return this.minLon > this.maxLon; 393 } 394 395 /** 396 * Converts the lat/lon bounding box to an object of type Rectangle2D.Double 397 * @return the bounding box to Rectangle2D.Double 398 */ 399 public Rectangle2D.Double asRect() { 400 double w = maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0); 401 return new Rectangle2D.Double(minLon, minLat, w, maxLat-minLat); 402 } 403 404 public double getArea() { 405 double w = maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0); 406 return w * (maxLat - minLat); 407 } 408 409 public String encodeAsString(String separator) { 410 StringBuilder sb = new StringBuilder(); 411 sb.append(minLat).append(separator).append(minLon) 412 .append(separator).append(maxLat).append(separator) 413 .append(maxLon); 414 return sb.toString(); 415 } 416 417 /** 418 * <p>Replies true, if this bounds are <em>collapsed</em>, i.e. if the min 419 * and the max corner are equal.</p> 420 * 421 * @return true, if this bounds are <em>collapsed</em> 422 */ 423 public boolean isCollapsed() { 424 return Double.doubleToLongBits(minLat) == Double.doubleToLongBits(maxLat) 425 && Double.doubleToLongBits(minLon) == Double.doubleToLongBits(maxLon); 426 } 427 428 public boolean isOutOfTheWorld() { 429 return 430 minLat < -90 || minLat > 90 || 431 maxLat < -90 || maxLat > 90 || 432 minLon < -180 || minLon > 180 || 433 maxLon < -180 || maxLon > 180; 434 } 435 436 public void normalize() { 437 minLat = LatLon.toIntervalLat(minLat); 438 maxLat = LatLon.toIntervalLat(maxLat); 439 minLon = LatLon.toIntervalLon(minLon); 440 maxLon = LatLon.toIntervalLon(maxLon); 441 } 442 443 @Override 444 public int hashCode() { 445 final int prime = 31; 446 int result = 1; 447 long temp; 448 temp = Double.doubleToLongBits(maxLat); 449 result = prime * result + (int) (temp ^ (temp >>> 32)); 450 temp = Double.doubleToLongBits(maxLon); 451 result = prime * result + (int) (temp ^ (temp >>> 32)); 452 temp = Double.doubleToLongBits(minLat); 453 result = prime * result + (int) (temp ^ (temp >>> 32)); 454 temp = Double.doubleToLongBits(minLon); 455 result = prime * result + (int) (temp ^ (temp >>> 32)); 456 return result; 457 } 458 459 @Override 460 public boolean equals(Object obj) { 461 if (this == obj) 462 return true; 463 if (obj == null) 464 return false; 465 if (getClass() != obj.getClass()) 466 return false; 467 Bounds other = (Bounds) obj; 468 if (Double.doubleToLongBits(maxLat) != Double.doubleToLongBits(other.maxLat)) 469 return false; 470 if (Double.doubleToLongBits(maxLon) != Double.doubleToLongBits(other.maxLon)) 471 return false; 472 if (Double.doubleToLongBits(minLat) != Double.doubleToLongBits(other.minLat)) 473 return false; 474 if (Double.doubleToLongBits(minLon) != Double.doubleToLongBits(other.minLon)) 475 return false; 476 return true; 477 } 478}