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