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