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