001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.data.osm; 003 004import java.awt.geom.Rectangle2D; 005import java.util.Objects; 006 007import org.openstreetmap.josm.data.Bounds; 008import org.openstreetmap.josm.data.coor.ILatLon; 009import org.openstreetmap.josm.data.coor.LatLon; 010import org.openstreetmap.josm.data.coor.QuadTiling; 011 012/** 013 * A BBox represents an area in lat/lon space. It is used for the quad tree. 014 * 015 * In contrast to a {@link Bounds} object, a BBox can represent an invalid (empty) area. 016 */ 017public class BBox { 018 019 protected double xmin = Double.POSITIVE_INFINITY; 020 protected double xmax = Double.NEGATIVE_INFINITY; 021 protected double ymin = Double.POSITIVE_INFINITY; 022 protected double ymax = Double.NEGATIVE_INFINITY; 023 024 /** 025 * Constructs a new (invalid) BBox 026 */ 027 public BBox() { 028 // Nothing to do 029 } 030 031 /** 032 * Constructs a new {@code BBox} defined by a single point. 033 * 034 * @param x X coordinate 035 * @param y Y coordinate 036 * @since 6203 037 */ 038 public BBox(final double x, final double y) { 039 add(x, y); 040 } 041 042 /** 043 * Constructs a new {@code BBox} defined by points <code>a</code> and <code>b</code>. 044 * Result is minimal BBox containing both points if they are both valid, else undefined 045 * 046 * @param a first point 047 * @param b second point 048 */ 049 public BBox(LatLon a, LatLon b) { 050 this(a.lon(), a.lat(), b.lon(), b.lat()); 051 } 052 053 /** 054 * Constructs a new {@code BBox} from another one. 055 * 056 * @param copy the BBox to copy 057 */ 058 public BBox(BBox copy) { 059 this.xmin = copy.xmin; 060 this.xmax = copy.xmax; 061 this.ymin = copy.ymin; 062 this.ymax = copy.ymax; 063 } 064 065 /** 066 * Creates bbox around the coordinate (x, y). 067 * Coordinate defines center of bbox, its edge will be 2*r. 068 * 069 * @param x X coordinate 070 * @param y Y coordinate 071 * @param r size 072 * @since 13140 073 */ 074 public BBox(double x, double y, double r) { 075 this(x - r, y - r, x + r, y + r); 076 } 077 078 /** 079 * Create minimal BBox so that {@code this.bounds(ax,ay)} and {@code this.bounds(bx,by)} will both return true 080 * @param ax left or right X value (-180 .. 180) 081 * @param ay top or bottom Y value (-90 .. 90) 082 * @param bx left or right X value (-180 .. 180) 083 * @param by top or bottom Y value (-90 .. 90) 084 */ 085 public BBox(double ax, double ay, double bx, double by) { 086 if (!(Double.isNaN(ax) || Double.isNaN(ay) || Double.isNaN(bx) || Double.isNaN(by))) { 087 add(ax, ay); 088 add(bx, by); 089 } 090 // otherwise use default which is an invalid BBox 091 } 092 093 /** 094 * Create BBox for all nodes of the way with known coordinates. 095 * If no node has a known coordinate, an invalid BBox is returned. 096 * @param w the way 097 */ 098 public BBox(IWay<?> w) { 099 for (INode ll : w.getNodes()) { 100 add(ll); 101 } 102 } 103 104 /** 105 * Create BBox for a node. An invalid BBox is returned if the coordinates are not known. 106 * @param n the node 107 */ 108 public BBox(INode n) { 109 this((ILatLon) n); 110 } 111 112 /** 113 * Create BBox for a given latlon. An invalid BBox is returned if the coordinates are not known. 114 * @param ll The lat lon position 115 */ 116 public BBox(ILatLon ll) { 117 add(ll); 118 } 119 120 /** 121 * Add a point to an existing BBox. Extends this bbox if necessary so that this.bounds(c) will return true 122 * if c is a valid LatLon instance. 123 * Kept for binary compatibility 124 * @param c a LatLon point 125 */ 126 public final void add(LatLon c) { 127 add((ILatLon) c); 128 } 129 130 /** 131 * Add a point to an existing BBox. Extends this bbox if necessary so that this.bounds(c) will return true 132 * if c is a valid LatLon instance. 133 * If it is invalid or <code>null</code>, this call is ignored. 134 * @param c a LatLon point. 135 */ 136 public final void add(ILatLon c) { 137 if (c != null) { 138 add(c.lon(), c.lat()); 139 } 140 } 141 142 /** 143 * Extends this bbox to include the point (x, y) 144 * @param x X coordinate 145 * @param y Y coordinate 146 */ 147 public final void add(double x, double y) { 148 if (!Double.isNaN(x) && !Double.isNaN(y)) { 149 xmin = Math.min(xmin, x); 150 xmax = Math.max(xmax, x); 151 ymin = Math.min(ymin, y); 152 ymax = Math.max(ymax, y); 153 } 154 } 155 156 /** 157 * Extends this bbox to include the bbox other. Does nothing if other is not valid. 158 * @param other a bbox 159 */ 160 public final void add(BBox other) { 161 if (other.isValid()) { 162 xmin = Math.min(xmin, other.xmin); 163 xmax = Math.max(xmax, other.xmax); 164 ymin = Math.min(ymin, other.ymin); 165 ymax = Math.max(ymax, other.ymax); 166 } 167 } 168 169 /** 170 * Extends this bbox to include the bbox of the primitive extended by extraSpace. 171 * @param primitive an OSM primitive 172 * @param extraSpace the value to extend the primitives bbox. Unit is in LatLon degrees. 173 */ 174 public void addPrimitive(OsmPrimitive primitive, double extraSpace) { 175 BBox primBbox = primitive.getBBox(); 176 add(primBbox.xmin - extraSpace, primBbox.ymin - extraSpace); 177 add(primBbox.xmax + extraSpace, primBbox.ymax + extraSpace); 178 } 179 180 /** 181 * Gets the height of the bbox. 182 * @return The difference between ymax and ymin. 0 for invalid bboxes. 183 */ 184 public double height() { 185 if (isValid()) { 186 return ymax - ymin; 187 } else { 188 return 0; 189 } 190 } 191 192 /** 193 * Gets the width of the bbox. 194 * @return The difference between xmax and xmin. 0 for invalid bboxes. 195 */ 196 public double width() { 197 if (isValid()) { 198 return xmax - xmin; 199 } else { 200 return 0; 201 } 202 } 203 204 /** 205 * Tests, whether the bbox {@code b} lies completely inside this bbox. 206 * @param b bounding box 207 * @return {@code true} if {@code b} lies completely inside this bbox 208 */ 209 public boolean bounds(BBox b) { 210 return xmin <= b.xmin && xmax >= b.xmax 211 && ymin <= b.ymin && ymax >= b.ymax; 212 } 213 214 /** 215 * Tests, whether the Point {@code c} lies within the bbox. 216 * @param c point 217 * @return {@code true} if {@code c} lies within the bbox 218 */ 219 public boolean bounds(LatLon c) { 220 return xmin <= c.lon() && xmax >= c.lon() 221 && ymin <= c.lat() && ymax >= c.lat(); 222 } 223 224 /** 225 * Tests, whether two BBoxes intersect as an area. 226 * I.e. whether there exists a point that lies in both of them. 227 * @param b other bounding box 228 * @return {@code true} if this bbox intersects with the other 229 */ 230 public boolean intersects(BBox b) { 231 return xmin <= b.xmax && xmax >= b.xmin 232 && ymin <= b.ymax && ymax >= b.ymin; 233 } 234 235 /** 236 * Returns the top-left point. 237 * @return The top-left point 238 */ 239 public LatLon getTopLeft() { 240 return new LatLon(ymax, xmin); 241 } 242 243 /** 244 * Returns the latitude of top-left point. 245 * @return The latitude of top-left point 246 * @since 6203 247 */ 248 public double getTopLeftLat() { 249 return ymax; 250 } 251 252 /** 253 * Returns the longitude of top-left point. 254 * @return The longitude of top-left point 255 * @since 6203 256 */ 257 public double getTopLeftLon() { 258 return xmin; 259 } 260 261 /** 262 * Returns the bottom-right point. 263 * @return The bottom-right point 264 */ 265 public LatLon getBottomRight() { 266 return new LatLon(ymin, xmax); 267 } 268 269 /** 270 * Returns the latitude of bottom-right point. 271 * @return The latitude of bottom-right point 272 * @since 6203 273 */ 274 public double getBottomRightLat() { 275 return ymin; 276 } 277 278 /** 279 * Returns the longitude of bottom-right point. 280 * @return The longitude of bottom-right point 281 * @since 6203 282 */ 283 public double getBottomRightLon() { 284 return xmax; 285 } 286 287 /** 288 * Gets the center of this BBox. 289 * @return The center. 290 */ 291 public LatLon getCenter() { 292 return new LatLon(ymin + (ymax-ymin)/2.0, xmin + (xmax-xmin)/2.0); 293 } 294 295 byte getIndex(final int level) { 296 297 byte idx1 = QuadTiling.index(ymin, xmin, level); 298 299 final byte idx2 = QuadTiling.index(ymin, xmax, level); 300 if (idx1 == -1) idx1 = idx2; 301 else if (idx1 != idx2) return -1; 302 303 final byte idx3 = QuadTiling.index(ymax, xmin, level); 304 if (idx1 == -1) idx1 = idx3; 305 else if (idx1 != idx3) return -1; 306 307 final byte idx4 = QuadTiling.index(ymax, xmax, level); 308 if (idx1 == -1) idx1 = idx4; 309 else if (idx1 != idx4) return -1; 310 311 return idx1; 312 } 313 314 /** 315 * Converts the bounds to a rectangle 316 * @return The rectangle in east/north space. 317 */ 318 public Rectangle2D toRectangle() { 319 return new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax - ymin); 320 } 321 322 @Override 323 public int hashCode() { 324 return Objects.hash(xmin, xmax, ymin, ymax); 325 } 326 327 @Override 328 public boolean equals(Object o) { 329 if (this == o) return true; 330 if (o == null || getClass() != o.getClass()) return false; 331 BBox b = (BBox) o; 332 return Double.compare(b.xmax, xmax) == 0 && Double.compare(b.ymax, ymax) == 0 333 && Double.compare(b.xmin, xmin) == 0 && Double.compare(b.ymin, ymin) == 0; 334 } 335 336 /** 337 * Check if bboxes are functionally equal 338 * @param other The other bbox to compare with 339 * @param maxDifference The maximum difference (in degrees) between the bboxes. May be null. 340 * @return true if they are functionally equivalent 341 * @since 15486 342 */ 343 public boolean bboxIsFunctionallyEqual(BBox other, Double maxDifference) { 344 return bboxesAreFunctionallyEqual(this, other, maxDifference); 345 } 346 347 /** 348 * Check if bboxes are functionally equal 349 * @param bbox1 A bbox to compare with another bbox 350 * @param bbox2 The other bbox to compare with 351 * @param maxDifference The maximum difference (in degrees) between the bboxes. May be null. 352 * @return true if they are functionally equivalent 353 * @since 15483 354 */ 355 public static boolean bboxesAreFunctionallyEqual(BBox bbox1, BBox bbox2, Double maxDifference) { 356 if (maxDifference == null) { 357 maxDifference = LatLon.MAX_SERVER_PRECISION; 358 } 359 return (bbox1 != null && bbox2 != null) 360 && (Math.abs(bbox1.getBottomRightLat() - bbox2.getBottomRightLat()) <= maxDifference 361 && Math.abs(bbox1.getBottomRightLon() - bbox2.getBottomRightLon()) <= maxDifference 362 && Math.abs(bbox1.getTopLeftLat() - bbox2.getTopLeftLat()) <= maxDifference 363 && Math.abs(bbox1.getTopLeftLon() - bbox2.getTopLeftLon()) <= maxDifference); 364 } 365 366 /** 367 * @return true if the bbox covers a part of the planets surface 368 * Height and width must be non-negative, but may (both) be 0. 369 * @since 11269 370 */ 371 public boolean isValid() { 372 return xmin <= xmax && ymin <= ymax; 373 } 374 375 /** 376 * @return true if the bbox is avalid and covers a part of the planets surface 377 * @since 11269 378 */ 379 public boolean isInWorld() { 380 return xmin >= -180.0 && xmax <= 180.0 && ymin >= -90.0 && ymax <= 90.0 && isValid(); 381 } 382 383 @Override 384 public String toString() { 385 return "[ x: " + xmin + " -> " + xmax + ", y: " + ymin + " -> " + ymax + " ]"; 386 } 387 388 /** 389 * Creates a CSV string for this bbox 390 * @param separator The separator to use 391 * @return A string 392 */ 393 public String toStringCSV(String separator) { 394 return String.join(separator, 395 LatLon.cDdFormatter.format(xmin), 396 LatLon.cDdFormatter.format(ymin), 397 LatLon.cDdFormatter.format(xmax), 398 LatLon.cDdFormatter.format(ymax)); 399 } 400}