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;