1 // Copyright 2005 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS-IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Original author: ericv@google.com (Eric Veach) 16 // Converted to D: madric@gmail.com (Vijay Nayar) 17 18 module s2.s2latlng_rect; 19 20 import algorithm = std.algorithm; 21 import edgedistances = s2.s2edge_distances; 22 import math = std.math; 23 import s2.logger; 24 import s2.r1interval; 25 import s2.s1angle; 26 import s2.s1chord_angle; 27 import s2.s1interval; 28 import s2.s2cap; 29 import s2.s2cell; 30 import s2.s2cell_id; 31 import s2.s2debug; 32 import s2.s2edge_crossings; 33 import s2.s2latlng; 34 import s2.s2point; 35 import s2.s2pointutil; 36 import s2.s2predicates; 37 import s2.s2region; 38 import s2.util.coding.coder; 39 import s2.util.math.vector; 40 41 import std.conv; 42 import std.exception; 43 44 /** 45 * An S2LatLngRect represents a closed latitude-longitude rectangle. It is 46 * capable of representing the empty and full rectangles as well as single 47 * points. Note that the latitude-longitude space is considered to have a 48 * *cylindrical* topology rather than a spherical one, i.e. the poles have 49 * multiple lat/lng representations. An S2LatLngRect may be defined so that 50 * includes some representations of a pole but not others. Use the 51 * PolarClosure() method if you want to expand a rectangle so that it contains 52 * all possible representations of any contained poles. 53 * 54 * Because S2LatLngRect uses S1Interval to store the longitude range, 55 * longitudes of -180 degrees are treated specially. Except for empty 56 * and full longitude spans, -180 degree longitudes will turn into +180 57 * degrees. This sign flip causes lng_lo() to be greater than lng_hi(), 58 * indicating that the rectangle will wrap around through -180 instead of 59 * through +179. Thus the math is consistent within the library, but the sign 60 * flip can be surprising, especially when working with map projections where 61 * -180 and +180 are at opposite ends of the flattened map. See the comments 62 * on S1Interval for more details. 63 * 64 * This class is intended to be copied by value as desired. It uses 65 * the default copy constructor and assignment operator, however it is 66 * not a "plain old datatype" (POD) because it has virtual functions. 67 */ 68 class S2LatLngRect : S2Region { 69 public: 70 // Construct a rectangle from minimum and maximum latitudes and longitudes. 71 // If lo.lng() > hi.lng(), the rectangle spans the 180 degree longitude 72 // line. Both points must be normalized, with lo.lat() <= hi.lat(). 73 // The rectangle contains all the points p such that 'lo' <= p <= 'hi', 74 // where '<=' is defined in the obvious way. 75 this(in S2LatLng lo, in S2LatLng hi) { 76 _lat = R1Interval(lo.lat().radians(), hi.lat().radians()); 77 _lng = S1Interval(lo.lng().radians(), hi.lng().radians()); 78 if (!isValid) { 79 logger.logError("Invalid rect: ", lo, ", ", hi); 80 } 81 } 82 83 // Construct a rectangle from latitude and longitude intervals. The two 84 // intervals must either be both empty or both non-empty, and the latitude 85 // interval must not extend outside [-90, +90] degrees. 86 // Note that both intervals (and hence the rectangle) are closed. 87 this(in R1Interval lat, in S1Interval lng) { 88 if (!isValid()) logger.logError("Invalid rect: ", lat, ", ", lng); 89 _lat = lat; 90 _lng = lng; 91 } 92 93 this(in S2LatLngRect rect) { 94 _lat = rect._lat; 95 _lng = rect._lng; 96 } 97 98 // The default constructor creates an empty S2LatLngRect. 99 this() { 100 _lat = R1Interval.empty(); 101 _lng = S1Interval.empty(); 102 } 103 104 // Construct a rectangle of the given size centered around the given point. 105 // "center" needs to be normalized, but "size" does not. The latitude 106 // interval of the result is clamped to [-90,90] degrees, and the longitude 107 // interval of the result is Full() if and only if the longitude size is 108 // 360 degrees or more. Examples of clamping (in degrees): 109 // 110 // center=(80,170), size=(40,60) -> lat=[60,90], lng=[140,-160] 111 // center=(10,40), size=(210,400) -> lat=[-90,90], lng=[-180,180] 112 // center=(-90,180), size=(20,50) -> lat=[-90,-80], lng=[155,-155] 113 static S2LatLngRect fromCenterSize(in S2LatLng center, in S2LatLng size) { 114 return fromPoint(center).expanded(0.5 * size); 115 } 116 117 // Construct a rectangle containing a single (normalized) point. 118 static S2LatLngRect fromPoint(in S2LatLng p) { 119 if (!p.isValid()) { 120 logger.logError("Invalid S2LatLng in S2LatLngRect.getDistance: ", p); 121 } 122 return new S2LatLngRect(p, p); 123 } 124 125 126 // Construct the minimal bounding rectangle containing the two given 127 // normalized points. This is equivalent to starting with an empty 128 // rectangle and calling AddPoint() twice. Note that it is different than 129 // the S2LatLngRect(lo, hi) constructor, where the first point is always 130 // used as the lower-left corner of the resulting rectangle. 131 static S2LatLngRect fromPointPair(in S2LatLng p1, in S2LatLng p2) { 132 if (!p1.isValid()) { 133 logger.logError("Invalid S2LatLng in S2LatLngRect.fromPointPair: ", p1); 134 } 135 if (!p2.isValid()) { 136 logger.logError("Invalid S2LatLng in S2LatLngRect.fromPointPair: ", p2); 137 } 138 139 return new S2LatLngRect( 140 R1Interval.fromPointPair(p1.lat().radians(), p2.lat().radians()), 141 S1Interval.fromPointPair(p1.lng().radians(), p2.lng().radians())); 142 } 143 144 145 // Accessor methods. 146 @property 147 S1Angle latLo() const { 148 return S1Angle.fromRadians(_lat.lo()); 149 } 150 @property 151 S1Angle latHi() const { 152 return S1Angle.fromRadians(_lat.hi()); 153 } 154 @property 155 S1Angle lngLo() const { 156 return S1Angle.fromRadians(_lng.lo()); 157 } 158 @property 159 S1Angle lngHi() const { 160 return S1Angle.fromRadians(_lng.hi()); 161 } 162 @property 163 R1Interval lat() const { 164 return _lat; 165 } 166 @property 167 S1Interval lng() const { 168 return _lng; 169 } 170 171 ref R1Interval mutableLat() { return _lat; } 172 ref S1Interval mutableLng() { return _lng; } 173 174 S2LatLng lo() const { return S2LatLng(latLo(), lngLo()); } 175 S2LatLng hi() const { return S2LatLng(latHi(), lngHi()); } 176 177 // The canonical empty and full rectangles, as derived from the Empty 178 // and Full R1 and S1 Intervals. 179 // Empty: lat_lo=1, lat_hi=0, lng_lo=Pi, lng_hi=-Pi (radians) 180 static S2LatLngRect empty() { 181 return new S2LatLngRect(); 182 } 183 184 // Full: lat_lo=-Pi/2, lat_hi=Pi/2, lng_lo=-Pi, lng_hi=Pi (radians) 185 static S2LatLngRect full() { 186 return new S2LatLngRect(fullLat(), fullLng()); 187 } 188 189 // The full allowable range of latitudes and longitudes. 190 static R1Interval fullLat() { return R1Interval(-M_PI_2, M_PI_2); } 191 static S1Interval fullLng() { return S1Interval.full(); } 192 193 // Returns true if the rectangle is valid, which essentially just means 194 // that the latitude bounds do not exceed Pi/2 in absolute value and 195 // the longitude bounds do not exceed Pi in absolute value. Also, if 196 // either the latitude or longitude bound is empty then both must be. 197 bool isValid() const { 198 // The lat/lng ranges must either be both empty or both non-empty. 199 return (math.fabs(_lat.lo()) <= M_PI_2 200 && math.fabs(_lat.hi()) <= M_PI_2 201 && _lng.isValid() 202 && _lat.isEmpty() == _lng.isEmpty()); 203 } 204 205 // Returns true if the rectangle is empty, i.e. it contains no points at all. 206 bool isEmpty() const { 207 return _lat.isEmpty(); 208 } 209 210 // Returns true if the rectangle is full, i.e. it contains all points. 211 bool isFull() const { 212 return _lat == fullLat() && _lng.isFull(); 213 } 214 215 // Returns true if the rectangle is a point, i.e. lo() == hi() 216 bool isPoint() const { 217 return _lat.lo() == _lat.hi() && _lng.lo() == _lng.hi(); 218 } 219 220 // Returns true if lng_.lo() > lng_.hi(), i.e. the rectangle crosses 221 // the 180 degree longitude line. 222 bool isInverted() const { return _lng.isInverted(); } 223 224 // Returns the k-th vertex of the rectangle (k = 0,1,2,3) in CCW order 225 // (lower left, lower right, upper right, upper left). For convenience, the 226 // argument is reduced modulo 4 to the range [0..3]. 227 S2LatLng getVertex(int k) const { 228 // Twiddle bits to return the points in CCW order (lower left, lower right, 229 // upper right, upper left). 230 int i = (k >> 1) & 1; 231 return S2LatLng.fromRadians(_lat[i], _lng[i ^ (k & 1)]); 232 } 233 234 // Returns the center of the rectangle in latitude-longitude space 235 // (in general this is not the center of the region on the sphere). 236 S2LatLng getCenter() const { 237 return S2LatLng.fromRadians(_lat.getCenter(), _lng.getCenter()); 238 } 239 240 // Returns the width and height of this rectangle in latitude-longitude 241 // space. Empty rectangles have a negative width and height. 242 S2LatLng getSize() const { 243 return S2LatLng.fromRadians(_lat.getLength(), _lng.getLength()); 244 } 245 246 // Returns the surface area of this rectangle on the unit sphere. 247 double area() const { 248 if (isEmpty()) return 0.0; 249 // This is the size difference of the two spherical caps, multiplied by 250 // the longitude ratio. 251 return lng().getLength() * (latHi().sin() - latLo().sin()); 252 } 253 254 /** 255 * Returns the true centroid of the rectangle multiplied by its surface area 256 * (see s2centroids.h for details on centroids). The result is not unit 257 * length, so you may want to normalize it. Note that in general the 258 * centroid is *not* at the center of the rectangle, and in fact it may not 259 * even be contained by the rectangle. (It is the "center of mass" of the 260 * rectangle viewed as subset of the unit sphere, i.e. it is the point in 261 * space about which this curved shape would rotate.) 262 * 263 * The reason for multiplying the result by the rectangle area is to make it 264 * easier to compute the centroid of more complicated shapes. The centroid 265 * of a union of disjoint regions can be computed simply by adding their 266 * GetCentroid() results. 267 */ 268 S2Point getCentroid() const { 269 // When a sphere is divided into slices of constant thickness by a set of 270 // parallel planes, all slices have the same surface area. This implies 271 // that the z-component of the centroid is simply the midpoint of the 272 // z-interval spanned by the S2LatLngRect. 273 // 274 // Similarly, it is easy to see that the (x,y) of the centroid lies in the 275 // plane through the midpoint of the rectangle's longitude interval. We 276 // only need to determine the distance "d" of this point from the z-axis. 277 // 278 // Let's restrict our attention to a particular z-value. In this z-plane, 279 // the S2LatLngRect is a circular arc. The centroid of this arc lies on a 280 // radial line through the midpoint of the arc, and at a distance from the 281 // z-axis of 282 // 283 // r * (sin(alpha) / alpha) 284 // 285 // where r = sqrt(1-z^2) is the radius of the arc, and "alpha" is half of 286 // the arc length (i.e., the arc covers longitudes [-alpha, alpha]). 287 // 288 // To find the centroid distance from the z-axis for the entire rectangle, 289 // we just need to integrate over the z-interval. This gives 290 // 291 // d = Integrate[sqrt(1-z^2)*sin(alpha)/alpha, z1..z2] / (z2 - z1) 292 // 293 // where [z1, z2] is the range of z-values covered by the rectangle. This 294 // simplifies to 295 // 296 // d = sin(alpha)/(2*alpha*(z2-z1))*(z2*r2 - z1*r1 + theta2 - theta1) 297 // 298 // where [theta1, theta2] is the latitude interval, z1=sin(theta1), 299 // z2=sin(theta2), r1=cos(theta1), and r2=cos(theta2). 300 // 301 // Finally, we want to return not the centroid itself, but the centroid 302 // scaled by the area of the rectangle. The area of the rectangle is 303 // 304 // A = 2 * alpha * (z2 - z1) 305 // 306 // which fortunately appears in the denominator of "d". 307 308 if (isEmpty()) return S2Point(); 309 double z1 = latLo().sin(), z2 = latHi().sin(); 310 double r1 = latLo().cos(), r2 = latHi().cos(); 311 double alpha = 0.5 * _lng.getLength(); 312 double r = math.sin(alpha) * (r2 * z2 - r1 * z1 + _lat.getLength()); 313 double lng = _lng.getCenter(); 314 double z = alpha * (z2 + z1) * (z2 - z1); // scaled by the area 315 return S2Point(r * math.cos(lng), r * math.sin(lng), z); 316 } 317 318 // More efficient version of Contains() that accepts a S2LatLng rather than 319 // an S2Point. The argument must be normalized. 320 bool contains(in S2LatLng ll) const { 321 if (!ll.isValid()) logger.logError("Invalid S2LatLng in S2LatLngRect.contains: ", ll); 322 323 return (_lat.contains(ll.lat().radians()) && _lng.contains(ll.lng().radians())); 324 } 325 326 // Returns true if and only if the given point is contained in the interior 327 // of the region (i.e. the region excluding its boundary). The point 'p' 328 // does not need to be normalized. 329 bool interiorContains(in S2Point p) const { 330 return interiorContains(S2LatLng(p)); 331 } 332 333 // More efficient version of InteriorContains() that accepts a S2LatLng 334 // rather than an S2Point. The argument must be normalized. 335 bool interiorContains(in S2LatLng ll) const { 336 if (!ll.isValid()) logger.logError("Invalid S2LatLng in S2LatLngRect.interiorContains: ", ll); 337 338 return _lat.interiorContains(ll.lat().radians()) 339 && _lng.interiorContains(ll.lng().radians()); 340 } 341 342 // Returns true if and only if the rectangle contains the given other 343 // rectangle. 344 bool contains(in S2LatLngRect other) const { 345 return _lat.contains(other._lat) && _lng.contains(other._lng); 346 } 347 348 // Returns true if and only if the interior of this rectangle contains all 349 // points of the given other rectangle (including its boundary). 350 bool interiorContains(in S2LatLngRect other) const { 351 return _lat.interiorContains(other._lat) && _lng.interiorContains(other._lng); 352 } 353 354 // Returns true if this rectangle and the given other rectangle have any 355 // points in common. 356 bool intersects(in S2LatLngRect other) const { 357 return _lat.intersects(other._lat) && _lng.intersects(other._lng); 358 } 359 360 // Returns true if this rectangle intersects the given cell. (This is an 361 // exact test and may be fairly expensive, see also MayIntersect below.) 362 bool intersects(in S2Cell cell) const { 363 // First we eliminate the cases where one region completely contains the 364 // other. Once these are disposed of, then the regions will intersect 365 // if and only if their boundaries intersect. 366 367 if (isEmpty()) return false; 368 if (contains(cell.getCenterRaw())) return true; 369 if (cell.contains(getCenter().toS2Point())) return true; 370 371 // Quick rejection test (not required for correctness). 372 if (!intersects(cell.getRectBound())) return false; 373 374 // Precompute the cell vertices as points and latitude-longitudes. We also 375 // check whether the S2Cell contains any corner of the rectangle, or 376 // vice-versa, since the edge-crossing tests only check the edge interiors. 377 378 S2Point[4] cell_v; 379 S2LatLng[4] cell_ll; 380 for (int i = 0; i < 4; ++i) { 381 cell_v[i] = cell.getVertex(i); // Must be normalized. 382 cell_ll[i] = S2LatLng(cell_v[i]); 383 if (contains(cell_ll[i])) return true; 384 if (cell.contains(getVertex(i).toS2Point())) return true; 385 } 386 387 // Now check whether the boundaries intersect. Unfortunately, a 388 // latitude-longitude rectangle does not have straight edges -- two edges 389 // are curved, and at least one of them is concave. 390 391 for (int i = 0; i < 4; ++i) { 392 S1Interval edge_lng = S1Interval.fromPointPair( 393 cell_ll[i].lng().radians(), cell_ll[(i+1) & 3].lng().radians()); 394 if (!_lng.intersects(edge_lng)) continue; 395 396 const S2Point a = cell_v[i]; 397 const S2Point b = cell_v[(i+1) & 3]; 398 if (edge_lng.contains(_lng.lo())) { 399 if (intersectsLngEdge(a, b, _lat, _lng.lo())) return true; 400 } 401 if (edge_lng.contains(_lng.hi())) { 402 if (intersectsLngEdge(a, b, _lat, _lng.hi())) return true; 403 } 404 if (intersectsLatEdge(a, b, _lat.lo(), _lng)) return true; 405 if (intersectsLatEdge(a, b, _lat.hi(), _lng)) return true; 406 } 407 return false; 408 } 409 410 // Returns true if and only if the interior of this rectangle intersects 411 // any point (including the boundary) of the given other rectangle. 412 bool interiorIntersects(in S2LatLngRect other) const { 413 return _lat.interiorIntersects(other._lat) 414 && _lng.interiorIntersects(other._lng); 415 } 416 417 // Returns true if the boundary of this rectangle intersects the given 418 // geodesic edge (v0, v1). 419 bool boundaryIntersects(in S2Point v0, in S2Point v1) const { 420 if (isEmpty()) return false; 421 if (!_lng.isFull()) { 422 if (intersectsLngEdge(v0, v1, _lat, _lng.lo())) return true; 423 if (intersectsLngEdge(v0, v1, _lat, _lng.hi())) return true; 424 } 425 if (_lat.lo() != -M_PI_2 && intersectsLatEdge(v0, v1, _lat.lo(), _lng)) { 426 return true; 427 } 428 if (_lat.hi() != M_PI_2 && intersectsLatEdge(v0, v1, _lat.hi(), _lng)) { 429 return true; 430 } 431 return false; 432 } 433 434 /** 435 * Increase the size of the bounding rectangle to include the given point. 436 * The rectangle is expanded by the minimum amount possible. The S2LatLng 437 * argument must be normalized. 438 */ 439 void addPoint(in S2Point p) { 440 addPoint(S2LatLng(p)); 441 } 442 443 void addPoint(in S2LatLng ll) { 444 if (!ll.isValid()) logger.logError("Invalid S2LatLng in S2LatLngRect::AddPoint: ", ll); 445 446 _lat.addPoint(ll.lat().radians()); 447 _lng.addPoint(ll.lng().radians()); 448 } 449 450 /** 451 * Returns a rectangle that has been expanded by margin.lat() on each side in 452 * the latitude direction, and by margin.lng() on each side in the longitude 453 * direction. If either margin is negative, then shrinks the rectangle on 454 * the corresponding sides instead. The resulting rectangle may be empty. 455 * 456 * As noted above, the latitude-longitude space has the topology of a 457 * cylinder. Longitudes "wrap around" at +/-180 degrees, while latitudes 458 * are clamped to range [-90, 90]. This means that any expansion (positive 459 * or negative) of the full longitude range remains full (since the 460 * "rectangle" is actually a continuous band around the cylinder), while 461 * expansion of the full latitude range remains full only if the margin is 462 * positive. 463 * 464 * If either the latitude or longitude interval becomes empty after 465 * expansion by a negative margin, the result is empty. 466 * 467 * Note that if an expanded rectangle contains a pole, it may not contain 468 * all possible lat/lng representations of that pole (see header above). 469 * Use the PolarClosure() method if you do not want this behavior. 470 * 471 * If you are trying to grow a rectangle by a certain *distance* on the 472 * sphere (e.g. 5km), use the ExpandedByDistance() method instead. 473 */ 474 S2LatLngRect expanded(in S2LatLng margin) const { 475 R1Interval lat = _lat.expanded(margin.lat().radians()); 476 S1Interval lng = _lng.expanded(margin.lng().radians()); 477 if (lat.isEmpty() || lng.isEmpty()) return empty(); 478 return new S2LatLngRect(lat.intersection(fullLat()), lng); 479 } 480 481 /** 482 * If the rectangle does not include either pole, returns it unmodified. 483 * Otherwise expands the longitude range to Full() so that the rectangle 484 * contains all possible representations of the contained pole(s). 485 */ 486 S2LatLngRect polarClosure() const { 487 if (_lat.lo() == -M_PI_2 || _lat.hi() == M_PI_2) { 488 return new S2LatLngRect(_lat, S1Interval.full()); 489 } 490 return new S2LatLngRect(this); 491 } 492 493 /** 494 * Returns the smallest rectangle containing the union of this rectangle and 495 * the given rectangle. 496 */ 497 S2LatLngRect unite(in S2LatLngRect other) const { 498 return new S2LatLngRect(_lat.unite(other._lat), _lng.unite(other._lng)); 499 } 500 501 /** 502 * Returns the smallest rectangle containing the intersection of this 503 * rectangle and the given rectangle. Note that the region of intersection 504 * may consist of two disjoint rectangles, in which case a single rectangle 505 * spanning both of them is returned. 506 */ 507 S2LatLngRect intersection(in S2LatLngRect other) const { 508 R1Interval lat = _lat.intersection(other._lat); 509 S1Interval lng = _lng.intersection(other._lng); 510 if (lat.isEmpty() || lng.isEmpty()) { 511 // The lat/lng ranges must either be both empty or both non-empty. 512 return empty(); 513 } 514 return new S2LatLngRect(lat, lng); 515 } 516 517 /** 518 * Expands this rectangle so that it contains all points within the given 519 * distance of the boundary, and return the smallest such rectangle. If the 520 * distance is negative, then instead shrinks this rectangle so that it 521 * excludes all points within the given absolute distance of the boundary, 522 * and returns the largest such rectangle. 523 * 524 * Unlike Expanded(), this method treats the rectangle as a set of points on 525 * the sphere, and measures distances on the sphere. For example, you can 526 * use this method to find a rectangle that contains all points within 5km 527 * of a given rectangle. Because this method uses the topology of the 528 * sphere, note the following: 529 * 530 * - The full and empty rectangles have no boundary on the sphere. Any 531 * expansion (positive or negative) of these rectangles leaves them 532 * unchanged. 533 * 534 * - Any rectangle that covers the full longitude range does not have an 535 * east or west boundary, therefore no expansion (positive or negative) 536 * will occur in that direction. 537 * 538 * - Any rectangle that covers the full longitude range and also includes 539 * a pole will not be expanded or contracted at that pole, because it 540 * does not have a boundary there. 541 * 542 * - If a rectangle is within the given distance of a pole, the result will 543 * include the full longitude range (because all longitudes are present 544 * at the poles). 545 * 546 * Expansion and contraction are defined such that they are inverses whenver 547 * possible, i.e. 548 * 549 * rect.ExpandedByDistance(x).ExpandedByDistance(-x) == rect 550 * 551 * (approximately), so long as the first operation does not cause a 552 * rectangle boundary to disappear (i.e., the longitude range newly becomes 553 * full or empty, or the latitude range expands to include a pole). 554 */ 555 S2LatLngRect expandedByDistance(S1Angle distance) const { 556 if (distance >= S1Angle.zero()) { 557 // The most straightforward approach is to build a cap centered on each 558 // vertex and take the union of all the bounding rectangles (including the 559 // original rectangle; this is necessary for very large rectangles). 560 561 // TODO(ericv): Update this code to use an algorithm like the one below. 562 auto radius = S1ChordAngle(distance); 563 S2LatLngRect r = new S2LatLngRect(this); 564 for (int k = 0; k < 4; ++k) { 565 scope S2Cap cap = new S2Cap(getVertex(k).toS2Point(), radius); 566 r = r.unite(cap.getRectBound()); 567 } 568 return r; 569 } else { 570 // Shrink the latitude interval unless the latitude interval contains a pole 571 // and the longitude interval is full, in which case the rectangle has no 572 // boundary at that pole. 573 auto lat_result = R1Interval( 574 lat().lo() <= fullLat().lo() && lng().isFull() 575 ? fullLat().lo() : lat().lo() - distance.radians(), 576 lat().hi() >= fullLat().hi() && lng().isFull() 577 ? fullLat().hi() : lat().hi() + distance.radians()); 578 if (lat_result.isEmpty()) { 579 return S2LatLngRect.empty(); 580 } 581 582 // Maximum absolute value of a latitude in lat_result. At this latitude, 583 // the cap occupies the largest longitude interval. 584 double max_abs_lat = algorithm.max(-lat_result.lo(), lat_result.hi()); 585 586 // Compute the largest longitude interval that the cap occupies. We use the 587 // law of sines for spherical triangles. For the details, see the comment in 588 // S2Cap::GetRectBound(). 589 // 590 // When sin_a >= sin_c, the cap covers all the latitude. 591 double sin_a = math.sin(-distance.radians()); 592 double sin_c = math.cos(max_abs_lat); 593 double max_lng_margin = sin_a < sin_c ? math.asin(sin_a / sin_c) : M_PI_2; 594 595 S1Interval lng_result = lng().expanded(-max_lng_margin); 596 if (lng_result.isEmpty()) { 597 return S2LatLngRect.empty(); 598 } 599 return new S2LatLngRect(lat_result, lng_result); 600 } 601 } 602 603 // Returns the minimum distance (measured along the surface of the sphere) to 604 // the given S2LatLngRect. Both S2LatLngRects must be non-empty. 605 S1Angle getDistance(in S2LatLngRect other) const 606 in { 607 assert(!this.isEmpty()); 608 assert(!other.isEmpty()); 609 } do { 610 const S2LatLngRect a = this; 611 const S2LatLngRect b = other; 612 613 // First, handle the trivial cases where the longitude intervals overlap. 614 if (a.lng().intersects(b.lng())) { 615 if (a.lat().intersects(b.lat())) 616 return S1Angle.fromRadians(0); // Intersection between a and b. 617 618 // We found an overlap in the longitude interval, but not in the latitude 619 // interval. This means the shortest path travels along some line of 620 // longitude connecting the high-latitude of the lower rect with the 621 // low-latitude of the higher rect. 622 S1Angle lo, hi; 623 if (a.lat().lo() > b.lat().hi()) { 624 lo = b.latHi(); 625 hi = a.latLo(); 626 } else { 627 lo = a.latHi(); 628 hi = b.latLo(); 629 } 630 return hi - lo; 631 } 632 633 // The longitude intervals don't overlap. In this case, the closest points 634 // occur somewhere on the pair of longitudinal edges which are nearest in 635 // longitude-space. 636 S1Angle a_lng, b_lng; 637 S1Interval lo_hi = S1Interval.fromPointPair(a.lng().lo(), b.lng().hi()); 638 S1Interval hi_lo = S1Interval.fromPointPair(a.lng().hi(), b.lng().lo()); 639 if (lo_hi.getLength() < hi_lo.getLength()) { 640 a_lng = a.lngLo(); 641 b_lng = b.lngHi(); 642 } else { 643 a_lng = a.lngHi(); 644 b_lng = b.lngLo(); 645 } 646 647 // The shortest distance between the two longitudinal segments will include at 648 // least one segment endpoint. We could probably narrow this down further to a 649 // single point-edge distance by comparing the relative latitudes of the 650 // endpoints, but for the sake of clarity, we'll do all four point-edge 651 // distance tests. 652 S2Point a_lo = S2LatLng(a.latLo(), a_lng).toS2Point(); 653 S2Point a_hi = S2LatLng(a.latHi(), a_lng).toS2Point(); 654 S2Point b_lo = S2LatLng(b.latLo(), b_lng).toS2Point(); 655 S2Point b_hi = S2LatLng(b.latHi(), b_lng).toS2Point(); 656 return algorithm.min( 657 edgedistances.getDistance(a_lo, b_lo, b_hi), 658 algorithm.min( 659 edgedistances.getDistance(a_hi, b_lo, b_hi), 660 algorithm.min( 661 edgedistances.getDistance(b_lo, a_lo, a_hi), 662 edgedistances.getDistance(b_hi, a_lo, a_hi)))); 663 } 664 665 // Returns the minimum distance (measured along the surface of the sphere) 666 // from a given point to the rectangle (both its boundary and its interior). 667 // The latlng must be valid. 668 S1Angle getDistance(in S2LatLng p) const { 669 // The algorithm here is the same as in GetDistance(S2LatLngRect), only 670 // with simplified calculations. 671 const S2LatLngRect a = this; 672 if (a.isEmpty()) logger.logError("Empty S2LatLngRect in S2LatLngRect::GetDistance: ", a); 673 if (!p.isValid()) logger.logError("Invalid S2LatLng in S2LatLngRect::GetDistance: ", p); 674 675 if (a.lng().contains(p.lng().radians())) { 676 return S1Angle.fromRadians( 677 algorithm.max( 678 0.0, 679 algorithm.max( 680 p.lat().radians() - a.lat().hi(), 681 a.lat().lo() - p.lat().radians()))); 682 } 683 684 auto interval = S1Interval(a.lng().hi(), a.lng().getComplementCenter()); 685 double a_lng; 686 if (interval.contains(p.lng().radians())) { 687 a_lng = a.lng().hi(); 688 } else { 689 a_lng = a.lng().lo(); 690 } 691 S2Point lo = S2LatLng.fromRadians(a.lat().lo(), a_lng).toS2Point(); 692 S2Point hi = S2LatLng.fromRadians(a.lat().hi(), a_lng).toS2Point(); 693 return edgedistances.getDistance(p.toS2Point(), lo, hi); 694 } 695 696 // Returns the (directed or undirected) Hausdorff distance (measured along the 697 // surface of the sphere) to the given S2LatLngRect. The directed Hausdorff 698 // distance from rectangle A to rectangle B is given by 699 // h(A, B) = max_{p in A} min_{q in B} d(p, q). 700 // The Hausdorff distance between rectangle A and rectangle B is given by 701 // H(A, B) = max{h(A, B), h(B, A)}. 702 S1Angle getHausdorffDistance(in S2LatLngRect other) const { 703 return algorithm.max( 704 getDirectedHausdorffDistance(other), 705 other.getDirectedHausdorffDistance(this)); 706 } 707 708 S1Angle getDirectedHausdorffDistance(in S2LatLngRect other) const { 709 if (isEmpty()) { 710 return S1Angle.fromRadians(0); 711 } 712 if (other.isEmpty()) { 713 return S1Angle.fromRadians(M_PI); // maximum possible distance on S2 714 } 715 716 double lng_distance = lng().getDirectedHausdorffDistance(other.lng()); 717 assert(lng_distance >= 0); 718 return getDirectedHausdorffDistance(lng_distance, lat(), other.lat()); 719 } 720 721 // Returns true if two rectangles contains the same set of points. 722 override 723 bool opEquals(in Object o) const { 724 S2LatLngRect other = cast(S2LatLngRect) o; 725 if (other) { 726 return lat() == other.lat() && lng() == other.lng(); 727 } else { 728 return false; 729 } 730 } 731 732 // Returns true if the latitude and longitude intervals of the two rectangles 733 // are the same up to the given tolerance (see r1interval.h and s1interval.h 734 // for details). 735 bool approxEquals(in S2LatLngRect other, S1Angle max_error = S1Angle.fromRadians(1e-15)) const { 736 return _lat.approxEquals(other._lat, max_error.radians()) 737 && _lng.approxEquals(other._lng, max_error.radians()); 738 } 739 740 // ApproxEquals() with separate tolerances for latitude and longitude. 741 bool approxEquals(in S2LatLngRect other, in S2LatLng max_error) const { 742 return _lat.approxEquals(other._lat, max_error.lat().radians()) 743 && _lng.approxEquals(other._lng, max_error.lng().radians()); 744 } 745 746 //////////////////////////////////////////////////////////////////////// 747 // S2Region interface (see s2region.h for details): 748 749 override 750 S2LatLngRect clone() const { 751 return new S2LatLngRect(this); 752 } 753 754 override 755 S2Cap getCapBound() const { 756 // We consider two possible bounding caps, one whose axis passes 757 // through the center of the lat-long rectangle and one whose axis 758 // is the north or south pole. We return the smaller of the two caps. 759 760 if (isEmpty()) return S2Cap.empty(); 761 762 double pole_z, pole_angle; 763 if (_lat.lo() + _lat.hi() < 0) { 764 // South pole axis yields smaller cap. 765 pole_z = -1; 766 pole_angle = M_PI_2 + _lat.hi(); 767 } else { 768 pole_z = 1; 769 pole_angle = M_PI_2 - _lat.lo(); 770 } 771 S2Cap pole_cap = new S2Cap(S2Point(0, 0, pole_z), S1Angle.fromRadians(pole_angle)); 772 773 // For bounding rectangles that span 180 degrees or less in longitude, the 774 // maximum cap size is achieved at one of the rectangle vertices. For 775 // rectangles that are larger than 180 degrees, we punt and always return a 776 // bounding cap centered at one of the two poles. 777 double lng_span = _lng.hi() - _lng.lo(); 778 if (math.remainder(lng_span, 2 * M_PI) >= 0 && lng_span < 2 * M_PI) { 779 auto mid_cap = new S2Cap(getCenter().toS2Point(), S1Angle.fromRadians(0)); 780 for (int k = 0; k < 4; ++k) { 781 mid_cap.addPoint(getVertex(k).toS2Point()); 782 } 783 if (mid_cap.height() < pole_cap.height()) 784 return mid_cap; 785 } 786 return pole_cap; 787 } 788 789 override 790 S2LatLngRect getRectBound() const { 791 return new S2LatLngRect(this); 792 } 793 794 override 795 void getCellUnionBound(out S2CellId[] cellIds) { 796 return getCapBound().getCellUnionBound(cellIds); 797 } 798 799 override 800 bool contains(in S2Cell cell) const { 801 // A latitude-longitude rectangle contains a cell if and only if it contains 802 // the cell's bounding rectangle. This test is exact from a mathematical 803 // point of view, assuming that the bounds returned by S2Cell::GetRectBound() 804 // are tight. However, note that there can be a loss of precision when 805 // converting between representations -- for example, if an S2Cell is 806 // converted to a polygon, the polygon's bounding rectangle may not contain 807 // the cell's bounding rectangle. This has some slightly unexpected side 808 // effects; for instance, if one creates an S2Polygon from an S2Cell, the 809 // polygon will contain the cell, but the polygon's bounding box will not. 810 return contains(cell.getRectBound()); 811 } 812 813 // This test is cheap but is NOT exact. Use Intersects() if you want a more 814 // accurate and more expensive test. Note that when this method is used by 815 // an S2RegionCoverer, the accuracy isn't all that important since if a cell 816 // may intersect the region then it is subdivided, and the accuracy of this 817 // method goes up as the cells get smaller. 818 override 819 bool mayIntersect(in S2Cell cell) const { 820 // This test is cheap but is NOT exact (see s2latlng_rect.h). 821 return intersects(cell.getRectBound()); 822 } 823 824 // The point 'p' does not need to be normalized. 825 override 826 bool contains(in S2Point p) const { 827 return contains(S2LatLng(p)); 828 } 829 830 /** 831 * Appends a serialized representation of the S2LatLngRect to "encoder". 832 * 833 * REQUIRES: "encoder" uses the default constructor, so that its buffer 834 * can be enlarged as necessary by calling Ensure(int). 835 */ 836 void encode(ORangeT)(Encoder!ORangeT encoder) const 837 out(; encoder.avail() >= 0) { 838 encoder.ensure(40); // sufficient 839 840 encoder.put8(CURRENT_LOSSLESS_ENCODING_VERSION_NUMBER); 841 encoder.putDouble(_lat.lo()); 842 encoder.putDouble(_lat.hi()); 843 encoder.putDouble(_lng.lo()); 844 encoder.putDouble(_lng.hi()); 845 } 846 847 /// Decodes an S2LatLngRect encoded with Encode(). Returns true on success. 848 bool decode(IRangeT)(Decoder!IRangeT decoder) { 849 if (decoder.avail() < ubyte.sizeof + 4 * double.sizeof) 850 return false; 851 ubyte versionNum = decoder.get8(); 852 if (versionNum > CURRENT_LOSSLESS_ENCODING_VERSION_NUMBER) return false; 853 854 double lat_lo = decoder.getDouble(); 855 double lat_hi = decoder.getDouble(); 856 _lat = R1Interval(lat_lo, lat_hi); 857 double lng_lo = decoder.getDouble(); 858 double lng_hi = decoder.getDouble(); 859 _lng = S1Interval(lng_lo, lng_hi); 860 861 if (!isValid()) { 862 if (flagsS2Debug) logger.logError("Invalid result in S2LatLngRect.decode: ", this); 863 return false; 864 } 865 866 return true; 867 } 868 869 /// Returns true if the edge AB intersects the given edge of constant longitude. 870 static bool intersectsLngEdge( 871 in S2Point a, in S2Point b, in R1Interval lat, double lng) { 872 // Return true if the segment AB intersects the given edge of constant 873 // longitude. The nice thing about edges of constant longitude is that 874 // they are straight lines on the sphere (geodesics). 875 876 return crossingSign( 877 a, b, S2LatLng.fromRadians(lat.lo(), lng).toS2Point(), 878 S2LatLng.fromRadians(lat.hi(), lng).toS2Point()) > 0; 879 } 880 881 /** 882 * Returns true if the edge AB intersects the given edge of constant 883 * latitude. Requires the vectors to have unit length. 884 */ 885 static bool intersectsLatEdge( 886 in S2Point a, in S2Point b, double lat, in S1Interval lng) 887 in { 888 assert(isUnitLength(a)); 889 assert(isUnitLength(b)); 890 } do { 891 // Return true if the segment AB intersects the given edge of constant 892 // latitude. Unfortunately, lines of constant latitude are curves on 893 // the sphere. They can intersect a straight edge in 0, 1, or 2 points. 894 895 // First, compute the normal to the plane AB that points vaguely north. 896 Vector3_d z = robustCrossProd(a, b).normalize(); 897 if (z[2] < 0) z = -z; 898 899 // Extend this to an orthonormal frame (x,y,z) where x is the direction 900 // where the great circle through AB achieves its maximium latitude. 901 Vector3_d y = robustCrossProd(z, S2Point(0, 0, 1)).normalize(); 902 Vector3_d x = y.crossProd(z); 903 enforce(isUnitLength(x)); 904 enforce(x[2] >= 0); 905 906 // Compute the angle "theta" from the x-axis (in the x-y plane defined 907 // above) where the great circle intersects the given line of latitude. 908 double sin_lat = math.sin(lat); 909 if (math.fabs(sin_lat) >= x[2]) { 910 return false; // The great circle does not reach the given latitude. 911 } 912 enforce(x[2] > 0); 913 double cos_theta = sin_lat / x[2]; 914 double sin_theta = math.sqrt(1 - cos_theta * cos_theta); 915 double theta = math.atan2(sin_theta, cos_theta); 916 917 // The candidate intersection points are located +/- theta in the x-y 918 // plane. For an intersection to be valid, we need to check that the 919 // intersection point is contained in the interior of the edge AB and 920 // also that it is contained within the given longitude interval "lng". 921 922 // Compute the range of theta values spanned by the edge AB. 923 S1Interval ab_theta = S1Interval.fromPointPair( 924 math.atan2(a.dotProd(y), a.dotProd(x)), 925 math.atan2(b.dotProd(y), b.dotProd(x))); 926 927 if (ab_theta.contains(theta)) { 928 // Check if the intersection point is also in the given "lng" interval. 929 S2Point isect = x * cos_theta + y * sin_theta; 930 if (lng.contains(math.atan2(isect[1], isect[0]))) return true; 931 } 932 if (ab_theta.contains(-theta)) { 933 // Check if the intersection point is also in the given "lng" interval. 934 S2Point isect = x * cos_theta - y * sin_theta; 935 if (lng.contains(math.atan2(isect[1], isect[0]))) return true; 936 } 937 return false; 938 } 939 940 override 941 string toString() const { 942 return "[Lo" ~ to!string(lo()) ~ ", Hi" ~ to!string(hi()) ~ "]"; 943 } 944 945 private: 946 /** 947 * Return the directed Hausdorff distance from one longitudinal edge spanning 948 * latitude range 'a_lat' to the other longitudinal edge spanning latitude 949 * range 'b_lat', with their longitudinal difference given by 'lng_diff'. 950 */ 951 static S1Angle getDirectedHausdorffDistance(double lng_diff, in R1Interval a, in R1Interval b) 952 in { 953 assert(lng_diff >= 0); 954 assert(lng_diff <= M_PI); 955 } do { 956 // By symmetry, we can assume a's longtitude is 0 and b's longtitude is 957 // lng_diff. Call b's two endpoints b_lo and b_hi. Let H be the hemisphere 958 // containing a and delimited by the longitude line of b. The Voronoi diagram 959 // of b on H has three edges (portions of great circles) all orthogonal to b 960 // and meeting at b_lo cross b_hi. 961 // E1: (b_lo, b_lo cross b_hi) 962 // E2: (b_hi, b_lo cross b_hi) 963 // E3: (-b_mid, b_lo cross b_hi), where b_mid is the midpoint of b 964 // 965 // They subdivide H into three Voronoi regions. Depending on how longitude 0 966 // (which contains edge a) intersects these regions, we distinguish two cases: 967 // Case 1: it intersects three regions. This occurs when lng_diff <= M_PI_2. 968 // Case 2: it intersects only two regions. This occurs when lng_diff > M_PI_2. 969 // 970 // In the first case, the directed Hausdorff distance to edge b can only be 971 // realized by the following points on a: 972 // A1: two endpoints of a. 973 // A2: intersection of a with the equator, if b also intersects the equator. 974 // 975 // In the second case, the directed Hausdorff distance to edge b can only be 976 // realized by the following points on a: 977 // B1: two endpoints of a. 978 // B2: intersection of a with E3 979 // B3: farthest point from b_lo to the interior of D, and farthest point from 980 // b_hi to the interior of U, if any, where D (resp. U) is the portion 981 // of edge a below (resp. above) the intersection point from B2. 982 983 if (lng_diff == 0) { 984 return S1Angle.fromRadians(a.getDirectedHausdorffDistance(b)); 985 } 986 987 // Assumed longtitude of b. 988 double b_lng = lng_diff; 989 // Two endpoints of b. 990 S2Point b_lo = S2LatLng.fromRadians(b.lo(), b_lng).toS2Point(); 991 S2Point b_hi = S2LatLng.fromRadians(b.hi(), b_lng).toS2Point(); 992 993 // Handling of each case outlined at the top of the function starts here. 994 // This is initialized a few lines below. 995 S1Angle max_distance; 996 997 // Cases A1 and B1. 998 S2Point a_lo = S2LatLng.fromRadians(a.lo(), 0).toS2Point(); 999 S2Point a_hi = S2LatLng.fromRadians(a.hi(), 0).toS2Point(); 1000 max_distance = edgedistances.getDistance(a_lo, b_lo, b_hi); 1001 max_distance = algorithm.max(max_distance, edgedistances.getDistance(a_hi, b_lo, b_hi)); 1002 1003 if (lng_diff <= M_PI_2) { 1004 // Case A2. 1005 if (a.contains(0) && b.contains(0)) { 1006 max_distance = algorithm.max(max_distance, S1Angle.fromRadians(lng_diff)); 1007 } 1008 } else { 1009 // Case B2. 1010 const S2Point p = getBisectorIntersection(b, b_lng); 1011 double p_lat = S2LatLng.latitude(p).radians(); 1012 if (a.contains(p_lat)) { 1013 max_distance = algorithm.max(max_distance, S1Angle(p, b_lo)); 1014 } 1015 1016 // Case B3. 1017 if (p_lat > a.lo()) { 1018 max_distance = algorithm.max( 1019 max_distance, 1020 getInteriorMaxDistance(R1Interval(a.lo(), algorithm.min(p_lat, a.hi())), b_lo)); 1021 } 1022 if (p_lat < a.hi()) { 1023 max_distance = algorithm.max( 1024 max_distance, 1025 getInteriorMaxDistance(R1Interval(algorithm.max(p_lat, a.lo()), a.hi()), b_hi)); 1026 } 1027 } 1028 1029 return max_distance; 1030 } 1031 1032 /** 1033 * Return max distance from a point b to the segment spanning latitude range 1034 * a_lat on longitude 0, if the max occurs in the interior of a_lat. Otherwise 1035 * return -1. 1036 */ 1037 static S1Angle getInteriorMaxDistance(in R1Interval a_lat, in S2Point b) { 1038 // Longitude 0 is in the y=0 plane. b.x() >= 0 implies that the maximum 1039 // does not occur in the interior of a_lat. 1040 if (a_lat.isEmpty() || b.x() >= 0) return S1Angle.fromRadians(-1); 1041 1042 // Project b to the y=0 plane. The antipodal of the normalized projection is 1043 // the point at which the maxium distance from b occurs, if it is contained 1044 // in a_lat. 1045 S2Point intersection_point = S2Point(-b.x(), 0, -b.z()).normalize(); 1046 if (a_lat.interiorContains(S2LatLng.latitude(intersection_point).radians())) { 1047 return S1Angle(b, intersection_point); 1048 } else { 1049 return S1Angle.fromRadians(-1); 1050 } 1051 } 1052 1053 1054 /** 1055 * Return the intersection of longitude 0 with the bisector of an edge 1056 * on longitude 'lng' and spanning latitude range 'lat'. 1057 */ 1058 static S2Point getBisectorIntersection(in R1Interval lat, double lng) { 1059 lng = math.fabs(lng); 1060 double lat_center = lat.getCenter(); 1061 // A vector orthogonal to the bisector of the given longitudinal edge. 1062 S2LatLng ortho_bisector; 1063 if (lat_center >= 0) { 1064 ortho_bisector = S2LatLng.fromRadians(lat_center - M_PI_2, lng); 1065 } else { 1066 ortho_bisector = S2LatLng.fromRadians(-lat_center - M_PI_2, lng - M_PI); 1067 } 1068 // A vector orthogonal to longitude 0. 1069 static const S2Point ortho_lng = S2Point(0, -1, 0); 1070 return robustCrossProd(ortho_lng, ortho_bisector.toS2Point()); 1071 } 1072 1073 R1Interval _lat; 1074 S1Interval _lng; 1075 } 1076 1077 private enum ubyte CURRENT_LOSSLESS_ENCODING_VERSION_NUMBER = 1;