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