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; 009import java.util.Objects; 010 011import org.openstreetmap.josm.data.coor.ILatLon; 012import org.openstreetmap.josm.data.coor.LatLon; 013import org.openstreetmap.josm.data.osm.BBox; 014import org.openstreetmap.josm.tools.CheckParameterUtil; 015 016/** 017 * This is a simple data class for "rectangular" areas of the world, given in 018 * lat/lon min/max values. The values are {@linkplain LatLon#roundToOsmPrecision(double) rounded to server precision} 019 * 020 * @author imi 021 * 022 * @see BBox to represent invalid areas. 023 */ 024public class Bounds implements IBounds { 025 /** 026 * The minimum and maximum coordinates. 027 */ 028 private double minLat, minLon, maxLat, maxLon; 029 030 /** 031 * Gets the point that has both the minimal lat and lon coordinate 032 * @return The point 033 */ 034 @Override 035 public LatLon getMin() { 036 return new LatLon(minLat, minLon); 037 } 038 039 /** 040 * Returns min latitude of bounds. Efficient shortcut for {@code getMin().lat()}. 041 * 042 * @return min latitude of bounds. 043 * @since 6203 044 */ 045 @Override 046 public double getMinLat() { 047 return minLat; 048 } 049 050 /** 051 * Returns min longitude of bounds. Efficient shortcut for {@code getMin().lon()}. 052 * 053 * @return min longitude of bounds. 054 * @since 6203 055 */ 056 @Override 057 public double getMinLon() { 058 return minLon; 059 } 060 061 /** 062 * Gets the point that has both the maximum lat and lon coordinate 063 * @return The point 064 */ 065 @Override 066 public LatLon getMax() { 067 return new LatLon(maxLat, maxLon); 068 } 069 070 /** 071 * Returns max latitude of bounds. Efficient shortcut for {@code getMax().lat()}. 072 * 073 * @return max latitude of bounds. 074 * @since 6203 075 */ 076 @Override 077 public double getMaxLat() { 078 return maxLat; 079 } 080 081 /** 082 * Returns max longitude of bounds. Efficient shortcut for {@code getMax().lon()}. 083 * 084 * @return max longitude of bounds. 085 * @since 6203 086 */ 087 @Override 088 public double getMaxLon() { 089 return maxLon; 090 } 091 092 /** 093 * The method used by the {@link Bounds#Bounds(String, String, ParseMethod)} constructor 094 */ 095 public enum ParseMethod { 096 /** 097 * Order: minlat, minlon, maxlat, maxlon 098 */ 099 MINLAT_MINLON_MAXLAT_MAXLON, 100 /** 101 * Order: left, bottom, right, top 102 */ 103 LEFT_BOTTOM_RIGHT_TOP 104 } 105 106 /** 107 * Construct bounds out of two points. Coords will be rounded. 108 * @param min min lat/lon 109 * @param max max lat/lon 110 */ 111 public Bounds(LatLon min, LatLon max) { 112 this(min.lat(), min.lon(), max.lat(), max.lon()); 113 } 114 115 /** 116 * Constructs bounds out of two points. 117 * @param min min lat/lon 118 * @param max max lat/lon 119 * @param roundToOsmPrecision defines if lat/lon will be rounded 120 */ 121 public Bounds(LatLon min, LatLon max, boolean roundToOsmPrecision) { 122 this(min.lat(), min.lon(), max.lat(), max.lon(), roundToOsmPrecision); 123 } 124 125 /** 126 * Constructs bounds out a single point. Coords will be rounded. 127 * @param b lat/lon 128 */ 129 public Bounds(LatLon b) { 130 this(b, true); 131 } 132 133 /** 134 * Single point Bounds defined by lat/lon {@code b}. 135 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true. 136 * 137 * @param b lat/lon of given point. 138 * @param roundToOsmPrecision defines if lat/lon will be rounded. 139 */ 140 public Bounds(LatLon b, boolean roundToOsmPrecision) { 141 this(b.lat(), b.lon(), roundToOsmPrecision); 142 } 143 144 /** 145 * Single point Bounds defined by point [lat,lon]. 146 * Coordinates will be rounded to osm precision if {@code roundToOsmPrecision} is true. 147 * 148 * @param lat latitude of given point. 149 * @param lon longitude of given point. 150 * @param roundToOsmPrecision defines if lat/lon will be rounded. 151 * @since 6203 152 */ 153 public Bounds(double lat, double lon, boolean roundToOsmPrecision) { 154 // Do not call this(b, b) to avoid GPX performance issue (see #7028) until roundToOsmPrecision() is improved 155 if (roundToOsmPrecision) { 156 this.minLat = LatLon.roundToOsmPrecision(lat); 157 this.minLon = LatLon.roundToOsmPrecision(lon); 158 } else { 159 this.minLat = lat; 160 this.minLon = lon; 161 } 162 this.maxLat = this.minLat; 163 this.maxLon = this.minLon; 164 } 165 166 /** 167 * Constructs bounds out of two points. Coords will be rounded. 168 * @param minLat min lat 169 * @param minLon min lon 170 * @param maxLat max lat 171 * @param maxLon max lon 172 */ 173 public Bounds(double minLat, double minLon, double maxLat, double maxLon) { 174 this(minLat, minLon, maxLat, maxLon, true); 175 } 176 177 /** 178 * Constructs bounds out of two points. 179 * @param minLat min lat 180 * @param minLon min lon 181 * @param maxLat max lat 182 * @param maxLon max lon 183 * @param roundToOsmPrecision defines if lat/lon will be rounded 184 */ 185 public Bounds(double minLat, double minLon, double maxLat, double maxLon, boolean roundToOsmPrecision) { 186 if (roundToOsmPrecision) { 187 this.minLat = LatLon.roundToOsmPrecision(minLat); 188 this.minLon = LatLon.roundToOsmPrecision(minLon); 189 this.maxLat = LatLon.roundToOsmPrecision(maxLat); 190 this.maxLon = LatLon.roundToOsmPrecision(maxLon); 191 } else { 192 this.minLat = minLat; 193 this.minLon = minLon; 194 this.maxLat = maxLat; 195 this.maxLon = maxLon; 196 } 197 } 198 199 /** 200 * Constructs bounds out of two points. Coords will be rounded. 201 * @param coords exactly 4 values: min lat, min lon, max lat, max lon 202 * @throws IllegalArgumentException if coords does not contain 4 double values 203 */ 204 public Bounds(double... coords) { 205 this(coords, true); 206 } 207 208 /** 209 * Constructs bounds out of two points. 210 * @param coords exactly 4 values: min lat, min lon, max lat, max lon 211 * @param roundToOsmPrecision defines if lat/lon will be rounded 212 * @throws IllegalArgumentException if coords does not contain 4 double values 213 */ 214 public Bounds(double[] coords, boolean roundToOsmPrecision) { 215 CheckParameterUtil.ensureParameterNotNull(coords, "coords"); 216 if (coords.length != 4) 217 throw new IllegalArgumentException(MessageFormat.format("Expected array of length 4, got {0}", coords.length)); 218 if (roundToOsmPrecision) { 219 this.minLat = LatLon.roundToOsmPrecision(coords[0]); 220 this.minLon = LatLon.roundToOsmPrecision(coords[1]); 221 this.maxLat = LatLon.roundToOsmPrecision(coords[2]); 222 this.maxLon = LatLon.roundToOsmPrecision(coords[3]); 223 } else { 224 this.minLat = coords[0]; 225 this.minLon = coords[1]; 226 this.maxLat = coords[2]; 227 this.maxLon = coords[3]; 228 } 229 } 230 231 /** 232 * Parse the bounds in order {@link ParseMethod#MINLAT_MINLON_MAXLAT_MAXLON} 233 * @param asString The string 234 * @param separator The separation regex 235 */ 236 public Bounds(String asString, String separator) { 237 this(asString, separator, ParseMethod.MINLAT_MINLON_MAXLAT_MAXLON); 238 } 239 240 /** 241 * Parse the bounds from a given string and round to OSM precision 242 * @param asString The string 243 * @param separator The separation regex 244 * @param parseMethod The order of the numbers 245 */ 246 public Bounds(String asString, String separator, ParseMethod parseMethod) { 247 this(asString, separator, parseMethod, true); 248 } 249 250 /** 251 * Parse the bounds from a given string 252 * @param asString The string 253 * @param separator The separation regex 254 * @param parseMethod The order of the numbers 255 * @param roundToOsmPrecision Whether to round to OSM precision 256 */ 257 public Bounds(String asString, String separator, ParseMethod parseMethod, boolean roundToOsmPrecision) { 258 CheckParameterUtil.ensureParameterNotNull(asString, "asString"); 259 String[] components = asString.split(separator, -1); 260 if (components.length != 4) 261 throw new IllegalArgumentException( 262 MessageFormat.format("Exactly four doubles expected in string, got {0}: {1}", components.length, asString)); 263 double[] values = new double[4]; 264 for (int i = 0; i < 4; i++) { 265 try { 266 values[i] = Double.parseDouble(components[i]); 267 } catch (NumberFormatException e) { 268 throw new IllegalArgumentException(MessageFormat.format("Illegal double value ''{0}''", components[i]), e); 269 } 270 } 271 272 switch (parseMethod) { 273 case LEFT_BOTTOM_RIGHT_TOP: 274 this.minLat = initLat(values[1], roundToOsmPrecision); 275 this.minLon = initLon(values[0], roundToOsmPrecision); 276 this.maxLat = initLat(values[3], roundToOsmPrecision); 277 this.maxLon = initLon(values[2], roundToOsmPrecision); 278 break; 279 case MINLAT_MINLON_MAXLAT_MAXLON: 280 default: 281 this.minLat = initLat(values[0], roundToOsmPrecision); 282 this.minLon = initLon(values[1], roundToOsmPrecision); 283 this.maxLat = initLat(values[2], roundToOsmPrecision); 284 this.maxLon = initLon(values[3], roundToOsmPrecision); 285 } 286 } 287 288 protected static double initLat(double value, boolean roundToOsmPrecision) { 289 if (!LatLon.isValidLat(value)) 290 throw new IllegalArgumentException(tr("Illegal latitude value ''{0}''", value)); 291 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value; 292 } 293 294 protected static double initLon(double value, boolean roundToOsmPrecision) { 295 if (!LatLon.isValidLon(value)) 296 throw new IllegalArgumentException(tr("Illegal longitude value ''{0}''", value)); 297 return roundToOsmPrecision ? LatLon.roundToOsmPrecision(value) : value; 298 } 299 300 /** 301 * Creates new {@code Bounds} from an existing one. 302 * @param other The bounds to copy 303 */ 304 public Bounds(final Bounds other) { 305 this(other.minLat, other.minLon, other.maxLat, other.maxLon); 306 } 307 308 /** 309 * Creates new {@code Bounds} from a rectangle. 310 * @param rect The rectangle 311 */ 312 public Bounds(Rectangle2D rect) { 313 this(rect.getMinY(), rect.getMinX(), rect.getMaxY(), rect.getMaxX()); 314 } 315 316 /** 317 * Creates new bounds around a coordinate pair <code>center</code>. The 318 * new bounds shall have an extension in latitude direction of <code>latExtent</code>, 319 * and in longitude direction of <code>lonExtent</code>. 320 * 321 * @param center the center coordinate pair. Must not be null. 322 * @param latExtent the latitude extent. > 0 required. 323 * @param lonExtent the longitude extent. > 0 required. 324 * @throws IllegalArgumentException if center is null 325 * @throws IllegalArgumentException if latExtent <= 0 326 * @throws IllegalArgumentException if lonExtent <= 0 327 */ 328 public Bounds(LatLon center, double latExtent, double lonExtent) { 329 CheckParameterUtil.ensureParameterNotNull(center, "center"); 330 if (latExtent <= 0.0) 331 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "latExtent", latExtent)); 332 if (lonExtent <= 0.0) 333 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' > 0.0 expected, got {1}", "lonExtent", lonExtent)); 334 335 this.minLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() - latExtent / 2)); 336 this.minLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() - lonExtent / 2)); 337 this.maxLat = LatLon.roundToOsmPrecision(LatLon.toIntervalLat(center.lat() + latExtent / 2)); 338 this.maxLon = LatLon.roundToOsmPrecision(LatLon.toIntervalLon(center.lon() + lonExtent / 2)); 339 } 340 341 /** 342 * Creates BBox with same coordinates. 343 * 344 * @return BBox with same coordinates. 345 * @since 6203 346 */ 347 public BBox toBBox() { 348 return new BBox(minLon, minLat, maxLon, maxLat); 349 } 350 351 @Override 352 public String toString() { 353 return "Bounds["+minLat+','+minLon+','+maxLat+','+maxLon+']'; 354 } 355 356 /** 357 * Converts this bounds to a human readable short string 358 * @param format The number format to use 359 * @return The string 360 */ 361 public String toShortString(DecimalFormat format) { 362 return format.format(minLat) + ' ' 363 + format.format(minLon) + " / " 364 + format.format(maxLat) + ' ' 365 + format.format(maxLon); 366 } 367 368 /** 369 * Returns center of the bounding box. 370 * @return Center of the bounding box. 371 */ 372 @Override 373 public LatLon getCenter() { 374 if (crosses180thMeridian()) { 375 double lat = (minLat + maxLat) / 2; 376 double lon = (minLon + maxLon - 360.0) / 2; 377 if (lon < -180.0) { 378 lon += 360.0; 379 } 380 return new LatLon(lat, lon); 381 } else { 382 return new LatLon((minLat + maxLat) / 2, (minLon + maxLon) / 2); 383 } 384 } 385 386 /** 387 * Extend the bounds if necessary to include the given point. 388 * @param ll The point to include into these bounds 389 */ 390 public void extend(LatLon ll) { 391 extend(ll.lat(), ll.lon()); 392 } 393 394 /** 395 * Extend the bounds if necessary to include the given point [lat,lon]. 396 * Good to use if you know coordinates to avoid creation of LatLon object. 397 * @param lat Latitude of point to include into these bounds 398 * @param lon Longitude of point to include into these bounds 399 * @since 6203 400 */ 401 public void extend(final double lat, final double lon) { 402 if (lat < minLat) { 403 minLat = LatLon.roundToOsmPrecision(lat); 404 } 405 if (lat > maxLat) { 406 maxLat = LatLon.roundToOsmPrecision(lat); 407 } 408 if (crosses180thMeridian()) { 409 if (lon > maxLon && lon < minLon) { 410 if (Math.abs(lon - minLon) <= Math.abs(lon - maxLon)) { 411 minLon = LatLon.roundToOsmPrecision(lon); 412 } else { 413 maxLon = LatLon.roundToOsmPrecision(lon); 414 } 415 } 416 } else { 417 if (lon < minLon) { 418 minLon = LatLon.roundToOsmPrecision(lon); 419 } 420 if (lon > maxLon) { 421 maxLon = LatLon.roundToOsmPrecision(lon); 422 } 423 } 424 } 425 426 /** 427 * Extends this bounds to enclose an other bounding box 428 * @param b The other bounds to enclose 429 */ 430 public void extend(Bounds b) { 431 extend(b.minLat, b.minLon); 432 extend(b.maxLat, b.maxLon); 433 } 434 435 /** 436 * Determines if the given point {@code ll} is within these bounds. 437 * <p> 438 * Points with unknown coordinates are always outside the coordinates. 439 * @param ll The lat/lon to check 440 * @return {@code true} if {@code ll} is within these bounds, {@code false} otherwise 441 */ 442 public boolean contains(LatLon ll) { 443 // binary compatibility 444 return contains((ILatLon) ll); 445 } 446 447 /** 448 * Determines if the given point {@code ll} is within these bounds. 449 * <p> 450 * Points with unknown coordinates are always outside the coordinates. 451 * @param ll The lat/lon to check 452 * @return {@code true} if {@code ll} is within these bounds, {@code false} otherwise 453 * @since 12161 454 */ 455 @Override 456 public boolean contains(ILatLon ll) { 457 if (!ll.isLatLonKnown()) { 458 return false; 459 } 460 if (ll.lat() < minLat || ll.lat() > maxLat) 461 return false; 462 if (crosses180thMeridian()) { 463 if (ll.lon() > maxLon && ll.lon() < minLon) 464 return false; 465 } else { 466 if (ll.lon() < minLon || ll.lon() > maxLon) 467 return false; 468 } 469 return true; 470 } 471 472 private static boolean intersectsLonCrossing(IBounds crossing, IBounds notCrossing) { 473 return notCrossing.getMinLon() <= crossing.getMaxLon() || notCrossing.getMaxLon() >= crossing.getMinLon(); 474 } 475 476 /** 477 * The two bounds intersect? Compared to java Shape.intersects, if does not use 478 * the interior but the closure. (">=" instead of ">") 479 * @param b other bounds 480 * @return {@code true} if the two bounds intersect 481 */ 482 public boolean intersects(Bounds b) { 483 return intersects((IBounds) b); 484 } 485 486 @Override 487 public boolean intersects(IBounds b) { 488 if (b.getMaxLat() < minLat || b.getMinLat() > maxLat) 489 return false; 490 491 if (crosses180thMeridian() && !b.crosses180thMeridian()) { 492 return intersectsLonCrossing(this, b); 493 } else if (!crosses180thMeridian() && b.crosses180thMeridian()) { 494 return intersectsLonCrossing(b, this); 495 } else if (crosses180thMeridian() && b.crosses180thMeridian()) { 496 return true; 497 } else { 498 return b.getMaxLon() >= minLon && b.getMinLon() <= maxLon; 499 } 500 } 501 502 /** 503 * Determines if this Bounds object crosses the 180th Meridian. 504 * See http://wiki.openstreetmap.org/wiki/180th_meridian 505 * @return true if this Bounds object crosses the 180th Meridian. 506 */ 507 @Override 508 public boolean crosses180thMeridian() { 509 return this.minLon > this.maxLon; 510 } 511 512 /** 513 * Converts the lat/lon bounding box to an object of type Rectangle2D.Double 514 * @return the bounding box to Rectangle2D.Double 515 */ 516 public Rectangle2D.Double asRect() { 517 return new Rectangle2D.Double(minLon, minLat, getWidth(), getHeight()); 518 } 519 520 /** 521 * Returns the bounds width. 522 * @return the bounds width 523 * @since 14521 524 */ 525 @Override 526 public double getHeight() { 527 return maxLat-minLat; 528 } 529 530 /** 531 * Returns the bounds width. 532 * @return the bounds width 533 * @since 14521 534 */ 535 @Override 536 public double getWidth() { 537 return maxLon-minLon + (crosses180thMeridian() ? 360.0 : 0.0); 538 } 539 540 /** 541 * Gets the area of this bounds (in lat/lon space) 542 * @return The area 543 */ 544 @Override 545 public double getArea() { 546 return getWidth() * getHeight(); 547 } 548 549 /** 550 * Encodes this as a string so that it may be parsed using the {@link ParseMethod#MINLAT_MINLON_MAXLAT_MAXLON} order 551 * @param separator The separator 552 * @return The string encoded bounds 553 */ 554 public String encodeAsString(String separator) { 555 return new StringBuilder() 556 .append(minLat).append(separator).append(minLon).append(separator) 557 .append(maxLat).append(separator).append(maxLon).toString(); 558 } 559 560 /** 561 * <p>Replies true, if this bounds are <em>collapsed</em>, i.e. if the min 562 * and the max corner are equal.</p> 563 * 564 * @return true, if this bounds are <em>collapsed</em> 565 */ 566 public boolean isCollapsed() { 567 return Double.doubleToLongBits(minLat) == Double.doubleToLongBits(maxLat) 568 && Double.doubleToLongBits(minLon) == Double.doubleToLongBits(maxLon); 569 } 570 571 /** 572 * Determines if these bounds are out of the world. 573 * @return true if lat outside of range [-90,90] or lon outside of range [-180,180] 574 */ 575 public boolean isOutOfTheWorld() { 576 return 577 !LatLon.isValidLat(minLat) || 578 !LatLon.isValidLat(maxLat) || 579 !LatLon.isValidLon(minLon) || 580 !LatLon.isValidLon(maxLon); 581 } 582 583 /** 584 * Clamp the bounds to be inside the world. 585 */ 586 public void normalize() { 587 minLat = LatLon.toIntervalLat(minLat); 588 maxLat = LatLon.toIntervalLat(maxLat); 589 minLon = LatLon.toIntervalLon(minLon); 590 maxLon = LatLon.toIntervalLon(maxLon); 591 } 592 593 @Override 594 public int hashCode() { 595 return Objects.hash(minLat, minLon, maxLat, maxLon); 596 } 597 598 @Override 599 public boolean equals(Object obj) { 600 if (this == obj) return true; 601 if (obj == null || getClass() != obj.getClass()) return false; 602 Bounds bounds = (Bounds) obj; 603 return Double.compare(bounds.minLat, minLat) == 0 && 604 Double.compare(bounds.minLon, minLon) == 0 && 605 Double.compare(bounds.maxLat, maxLat) == 0 && 606 Double.compare(bounds.maxLon, maxLon) == 0; 607 } 608}