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.LatLon; 010import org.openstreetmap.josm.data.coor.QuadTiling; 011import org.openstreetmap.josm.tools.Utils; 012 013/** 014 * A BBox represents an area in lat/lon space. It is used for the quad tree. 015 * 016 * In contrast to a {@link Bounds} object, a BBox can represent an invalid (empty) area. 017 */ 018public class BBox { 019 020 protected double xmin = Double.POSITIVE_INFINITY; 021 protected double xmax = Double.NEGATIVE_INFINITY; 022 protected double ymin = Double.POSITIVE_INFINITY; 023 protected double ymax = Double.NEGATIVE_INFINITY; 024 025 /** 026 * Constructs a new (invalid) BBox 027 */ 028 public BBox() { 029 // Nothing to do 030 } 031 032 /** 033 * Constructs a new {@code BBox} defined by a single point. 034 * 035 * @param x X coordinate 036 * @param y Y coordinate 037 * @since 6203 038 */ 039 public BBox(final double x, final double y) { 040 add(x, y); 041 } 042 043 /** 044 * Constructs a new {@code BBox} defined by points <code>a</code> and <code>b</code>. 045 * Result is minimal BBox containing both points if they are both valid, else undefined 046 * 047 * @param a first point 048 * @param b second point 049 */ 050 public BBox(LatLon a, LatLon b) { 051 this(a.lon(), a.lat(), b.lon(), b.lat()); 052 } 053 054 /** 055 * Constructs a new {@code BBox} from another one. 056 * 057 * @param copy the BBox to copy 058 */ 059 public BBox(BBox copy) { 060 this.xmin = copy.xmin; 061 this.xmax = copy.xmax; 062 this.ymin = copy.ymin; 063 this.ymax = copy.ymax; 064 } 065 066 /** 067 * Create minimal BBox so that {@code this.bounds(ax,ay)} and {@code this.bounds(bx,by)} will both return true 068 * @param ax left or right X value (-180 .. 180) 069 * @param ay top or bottom Y value (-90 .. 90) 070 * @param bx left or right X value (-180 .. 180) 071 * @param by top or bottom Y value (-90 .. 90) 072 */ 073 public BBox(double ax, double ay, double bx, double by) { 074 if (!(Double.isNaN(ax) || Double.isNaN(ay) || Double.isNaN(bx) || Double.isNaN(by))) { 075 add(ax, ay); 076 add(bx, by); 077 } 078 // otherwise use default which is an invalid BBox 079 } 080 081 /** 082 * Create BBox for all nodes of the way with known coordinates. 083 * If no node has a known coordinate, an invalid BBox is returned. 084 * @param w the way 085 */ 086 public BBox(Way w) { 087 w.getNodes().forEach(n -> add(n.getCoor())); 088 } 089 090 /** 091 * Create BBox for a node. An invalid BBox is returned if the coordinates are not known. 092 * @param n the node 093 */ 094 public BBox(Node n) { 095 if (n.isLatLonKnown()) { 096 add(n.getCoor()); 097 } 098 } 099 100 /** 101 * Add a point to an existing BBox. Extends this bbox if necessary so that this.bounds(c) will return true 102 * if c is a valid LatLon instance. 103 * @param c a LatLon point 104 */ 105 public final void add(LatLon c) { 106 if (c != null && c.isValid()) { 107 add(c.lon(), c.lat()); 108 } 109 } 110 111 /** 112 * Extends this bbox to include the point (x, y) 113 * @param x X coordinate 114 * @param y Y coordinate 115 */ 116 public final void add(double x, double y) { 117 if (!Double.isNaN(x) && !Double.isNaN(y)) { 118 xmin = Math.min(xmin, x); 119 xmax = Math.max(xmax, x); 120 ymin = Math.min(ymin, y); 121 ymax = Math.max(ymax, y); 122 } 123 } 124 125 /** 126 * Extends this bbox to include the bbox other. Does nothing if other is not valid. 127 * @param other a bbox 128 */ 129 public final void add(BBox other) { 130 if (other.isValid()) { 131 xmin = Math.min(xmin, other.xmin); 132 xmax = Math.max(xmax, other.xmax); 133 ymin = Math.min(ymin, other.ymin); 134 ymax = Math.max(ymax, other.ymax); 135 } 136 } 137 138 /** 139 * Extends this bbox to include the bbox of the primitive extended by extraSpace. 140 * @param primitive an OSM primitive 141 * @param extraSpace the value to extend the primitives bbox. Unit is in LatLon degrees. 142 */ 143 public void addPrimitive(OsmPrimitive primitive, double extraSpace) { 144 BBox primBbox = primitive.getBBox(); 145 add(primBbox.xmin - extraSpace, primBbox.ymin - extraSpace); 146 add(primBbox.xmax + extraSpace, primBbox.ymax + extraSpace); 147 } 148 149 /** 150 * Gets the height of the bbox. 151 * @return The difference between ymax and ymin. 0 for invalid bboxes. 152 */ 153 public double height() { 154 if (isValid()) { 155 return ymax - ymin; 156 } else { 157 return 0; 158 } 159 } 160 161 /** 162 * Gets the width of the bbox. 163 * @return The difference between xmax and xmin. 0 for invalid bboxes. 164 */ 165 public double width() { 166 if (isValid()) { 167 return xmax - xmin; 168 } else { 169 return 0; 170 } 171 } 172 173 /** 174 * Tests, whether the bbox {@code b} lies completely inside this bbox. 175 * @param b bounding box 176 * @return {@code true} if {@code b} lies completely inside this bbox 177 */ 178 public boolean bounds(BBox b) { 179 return xmin <= b.xmin && xmax >= b.xmax 180 && ymin <= b.ymin && ymax >= b.ymax; 181 } 182 183 /** 184 * Tests, whether the Point {@code c} lies within the bbox. 185 * @param c point 186 * @return {@code true} if {@code c} lies within the bbox 187 */ 188 public boolean bounds(LatLon c) { 189 return xmin <= c.lon() && xmax >= c.lon() 190 && ymin <= c.lat() && ymax >= c.lat(); 191 } 192 193 /** 194 * Tests, whether two BBoxes intersect as an area. 195 * I.e. whether there exists a point that lies in both of them. 196 * @param b other bounding box 197 * @return {@code true} if this bbox intersects with the other 198 */ 199 public boolean intersects(BBox b) { 200 return xmin <= b.xmax && xmax >= b.xmin 201 && ymin <= b.ymax && ymax >= b.ymin; 202 } 203 204 /** 205 * Returns the top-left point. 206 * @return The top-left point 207 */ 208 public LatLon getTopLeft() { 209 return new LatLon(ymax, xmin); 210 } 211 212 /** 213 * Returns the latitude of top-left point. 214 * @return The latitude of top-left point 215 * @since 6203 216 */ 217 public double getTopLeftLat() { 218 return ymax; 219 } 220 221 /** 222 * Returns the longitude of top-left point. 223 * @return The longitude of top-left point 224 * @since 6203 225 */ 226 public double getTopLeftLon() { 227 return xmin; 228 } 229 230 /** 231 * Returns the bottom-right point. 232 * @return The bottom-right point 233 */ 234 public LatLon getBottomRight() { 235 return new LatLon(ymin, xmax); 236 } 237 238 /** 239 * Returns the latitude of bottom-right point. 240 * @return The latitude of bottom-right point 241 * @since 6203 242 */ 243 public double getBottomRightLat() { 244 return ymin; 245 } 246 247 /** 248 * Returns the longitude of bottom-right point. 249 * @return The longitude of bottom-right point 250 * @since 6203 251 */ 252 public double getBottomRightLon() { 253 return xmax; 254 } 255 256 /** 257 * Gets the center of this BBox. 258 * @return The center. 259 */ 260 public LatLon getCenter() { 261 return new LatLon(ymin + (ymax-ymin)/2.0, xmin + (xmax-xmin)/2.0); 262 } 263 264 byte getIndex(final int level) { 265 266 byte idx1 = QuadTiling.index(ymin, xmin, level); 267 268 final byte idx2 = QuadTiling.index(ymin, xmax, level); 269 if (idx1 == -1) idx1 = idx2; 270 else if (idx1 != idx2) return -1; 271 272 final byte idx3 = QuadTiling.index(ymax, xmin, level); 273 if (idx1 == -1) idx1 = idx3; 274 else if (idx1 != idx3) return -1; 275 276 final byte idx4 = QuadTiling.index(ymax, xmax, level); 277 if (idx1 == -1) idx1 = idx4; 278 else if (idx1 != idx4) return -1; 279 280 return idx1; 281 } 282 283 public Rectangle2D toRectangle() { 284 return new Rectangle2D.Double(xmin, ymin, xmax - xmin, ymax - ymin); 285 } 286 287 @Override 288 public int hashCode() { 289 return Objects.hash(xmin, xmax, ymin, ymax); 290 } 291 292 @Override 293 public boolean equals(Object o) { 294 if (this == o) return true; 295 if (o == null || getClass() != o.getClass()) return false; 296 BBox b = (BBox) o; 297 return Double.compare(b.xmax, xmax) == 0 && Double.compare(b.ymax, ymax) == 0 298 && Double.compare(b.xmin, xmin) == 0 && Double.compare(b.ymin, ymin) == 0; 299 } 300 301 /** 302 * @return true if the bbox covers a part of the planets surface 303 * Height and width must be non-negative, but may (both) be 0. 304 * @since 11269 305 */ 306 public boolean isValid() { 307 return xmin <= xmax && ymin <= ymax; 308 } 309 310 /** 311 * @return true if the bbox is avalid and covers a part of the planets surface 312 * @since 11269 313 */ 314 public boolean isInWorld() { 315 return isValid() && xmin >= -180.0 && xmax <= 180.0 && ymin >= -90.0 && ymax <= 90.0; 316 } 317 318 @Override 319 public String toString() { 320 return "[ x: " + xmin + " -> " + xmax + ", y: " + ymin + " -> " + ymax + " ]"; 321 } 322 323 public String toStringCSV(String separator) { 324 return Utils.join(separator, Arrays.asList( 325 LatLon.cDdFormatter.format(xmin), 326 LatLon.cDdFormatter.format(ymin), 327 LatLon.cDdFormatter.format(xmax), 328 LatLon.cDdFormatter.format(ymax))); 329 } 330}