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.s1interval;
19 
20 import s2.util.math.vector;
21 import algorithm = std.algorithm;
22 import conv = std.conv;
23 import math = std.math;
24 import s2.util.math.s2const;
25 
26 // An S1Interval represents a closed interval on a unit circle (also known
27 // as a 1-dimensional sphere).  It is capable of representing the empty
28 // interval (containing no points), the full interval (containing all
29 // points), and zero-length intervals (containing a single point).
30 //
31 // Points are represented by the angle they make with the positive x-axis in
32 // the range [-Pi, Pi].  An interval is represented by its lower and upper
33 // bounds (both inclusive, since the interval is closed).  The lower bound may
34 // be greater than the upper bound, in which case the interval is "inverted"
35 // (i.e. it passes through the point (-1, 0)).
36 //
37 // Note that the point (-1, 0) has two valid representations, Pi and -Pi.
38 // The normalized representation of this point internally is Pi, so that
39 // endpoints of normal intervals are in the range (-Pi, Pi].  However, we
40 // take advantage of the point -Pi to construct two special intervals:
41 // the Full() interval is [-Pi, Pi], and the Empty() interval is [Pi, -Pi].
42 //
43 // This class is intended to be copied by value as desired.  It uses
44 // the default copy constructor and assignment operator.
45 struct S1Interval {
46 private:
47   enum ArgsChecked { ARGS_CHECKED }
48 
49   // Internal constructor that assumes that both arguments are in the
50   // correct range, i.e. normalization from -Pi to Pi is already done.
51   this(double lo, double hi, ArgsChecked dummy)
52   out {
53     assert(isValid());
54   } do {
55     _bounds = Vector2_d(lo, hi);
56   }
57 
58   // Return true if the interval (which is closed) contains the point 'p'.
59   // Skips the normalization of 'p' from -Pi to Pi.
60   bool fastContains(double p) const {
61     if (isInverted()) {
62       return (p >= lo() || p <= hi()) && !isEmpty();
63     } else {
64       return p >= lo() && p <= hi();
65     }
66   }
67 
68   static double positiveDistance(double a, double b) {
69     // Compute the distance from "a" to "b" in the range [0, 2*Pi).
70     // This is equivalent to (remainder(b - a - M_PI, 2 * M_PI) + M_PI),
71     // except that it is more numerically stable (it does not lose
72     // precision for very small positive distances).
73     double d = b - a;
74     if (d >= 0) {
75       return d;
76     }
77     // We want to ensure that if b == Pi and a == (-Pi + eps),
78     // the return result is approximately 2*Pi and not zero.
79     return (b + M_PI) - (a - M_PI);
80   }
81 
82   Vector2_d _bounds = Vector2_d(M_PI, -M_PI);
83 
84 public:
85   // Constructor.  Both endpoints must be in the range -Pi to Pi inclusive.
86   // The value -Pi is converted internally to Pi except for the Full()
87   // and Empty() intervals.
88   this(double lo, double hi)
89   out {
90     assert(isValid());
91   } do {
92     _bounds = Vector2_d(lo, hi);
93     if (lo == -M_PI && hi != M_PI) {
94       setLo(M_PI);
95     }
96     if (hi == -M_PI && lo != M_PI) {
97       setHi(M_PI);
98     }
99   }
100 
101   // Returns the empty interval.
102   static S1Interval empty() {
103     return S1Interval();
104   }
105 
106   // Returns the full interval.
107   static S1Interval full() {
108     return S1Interval(-M_PI, M_PI, ArgsChecked.ARGS_CHECKED);
109   }
110 
111   // Convenience method to construct an interval containing a single point.
112   static S1Interval fromPoint(double p) {
113     if (p == -M_PI) {
114       p = M_PI;
115     }
116     return S1Interval(p, p, ArgsChecked.ARGS_CHECKED);
117   }
118 
119   // Convenience method to construct the minimal interval containing
120   // the two given points.  This is equivalent to starting with an empty
121   // interval and calling AddPoint() twice, but it is more efficient.
122   static S1Interval fromPointPair(double p1, double p2)
123   in {
124     assert(math.fabs(p1) <= M_PI);
125     assert(math.fabs(p2) <= M_PI);
126   } do {
127     if (p1 == -M_PI) {
128       p1 = M_PI;
129     }
130     if (p2 == -M_PI) {
131       p2 = M_PI;
132     }
133     if (positiveDistance(p1, p2) <= M_PI) {
134       return S1Interval(p1, p2, ArgsChecked.ARGS_CHECKED);
135     } else {
136       return S1Interval(p2, p1, ArgsChecked.ARGS_CHECKED);
137     }
138   }
139 
140   // Accessors methods.
141   @property
142   double lo() const {
143     return _bounds[0];
144   }
145 
146   @property
147   double hi() const {
148     return _bounds[1];
149   }
150 
151   // Methods that allow the S1Interval to be accessed as a vector.  (The
152   // recommended style is to use lo() and hi() whenever possible, but these
153   // methods are useful when the endpoint to be selected is not constant.)
154   //
155   // Only const versions of these methods are provided, since S1Interval
156   // has invariants that must be maintained after each update.
157   double opIndex(size_t i) const {
158     return _bounds[i];
159   }
160 
161   @property
162   Vector2_d bounds() const {
163     return _bounds;
164   }
165 
166   // An interval is valid if neither bound exceeds Pi in absolute value,
167   // and the value -Pi appears only in the Empty() and Full() intervals.
168   bool isValid() const {
169     return (math.fabs(lo()) <= M_PI && math.fabs(hi()) <= M_PI
170         && !(lo() == -M_PI && hi() != M_PI)
171         && !(hi() == -M_PI && lo() != M_PI));
172   }
173 
174   // Return true if the interval contains all points on the unit circle.
175   bool isFull() const {
176     return lo() == -M_PI && hi() == M_PI;
177   }
178 
179   // Return true if the interval is empty, i.e. it contains no points.
180   bool isEmpty() const {
181     return lo() == M_PI && hi() == -M_PI;
182   }
183 
184   // Return true if lo() > hi().  (This is true for empty intervals.)
185   bool isInverted() const {
186     return lo() > hi();
187   }
188 
189   // Return the midpoint of the interval.  For full and empty intervals,
190   // the result is arbitrary.
191   double getCenter() const {
192     double center = 0.5 * (lo() + hi());
193     if (!isInverted()) {
194       return center;
195     }
196     // Return the center in the range (-Pi, Pi].
197     return (center <= 0) ? (center + M_PI) : (center - M_PI);
198   }
199 
200   // Return the length of the interval.  The length of an empty interval
201   // is negative.
202   double getLength() const {
203     double length = hi() - lo();
204     if (length >= 0) {
205       return length;
206     }
207     length += 2 * M_PI;
208     // Empty intervals have a negative length.
209     return (length > 0) ? length : -1;
210   }
211 
212   // Return the complement of the interior of the interval.  An interval and
213   // its complement have the same boundary but do not share any interior
214   // values.  The complement operator is not a bijection, since the complement
215   // of a singleton interval (containing a single value) is the same as the
216   // complement of an empty interval.
217   S1Interval complement() const {
218     if (lo() == hi()) {
219       return full();   // Singleton.
220     }
221     return S1Interval(hi(), lo(), ArgsChecked.ARGS_CHECKED);  // Handles empty and full.
222   }
223 
224   // Return the midpoint of the complement of the interval. For full and empty
225   // intervals, the result is arbitrary. For a singleton interval (containing a
226   // single point), the result is its antipodal point on S1.
227   double getComplementCenter() const {
228     if (lo() != hi()) {
229       return complement().getCenter();
230     } else {  // Singleton.
231       return (hi() <= 0) ? (hi() + M_PI) : (hi() - M_PI);
232     }
233   }
234 
235   // Return true if the interval (which is closed) contains the point 'p'.
236   bool contains(double p) const
237   in {
238     // Works for empty, full, and singleton intervals.
239     assert(math.fabs(p) <= M_PI);
240   } do {
241     if (p == -M_PI) {
242       p = M_PI;
243     }
244     return fastContains(p);
245   }
246 
247   // Return true if the interior of the interval contains the point 'p'.
248   bool interiorContains(double p) const
249   in {
250     // Works for empty, full, and singleton intervals.
251     assert(math.fabs(p) <= M_PI);
252   } do {
253     if (p == -M_PI) {
254       p = M_PI;
255     }
256 
257     if (isInverted()) {
258       return p > lo() || p < hi();
259     } else {
260       return (p > lo() && p < hi()) || isFull();
261     }
262   }
263 
264   // Return true if the interval contains the given interval 'y'.
265   // Works for empty, full, and singleton intervals.
266   bool contains(in S1Interval y) const {
267     // It might be helpful to compare the structure of these tests to
268     // the simpler Contains(double) method above.
269     if (isInverted()) {
270       if (y.isInverted()) {
271         return y.lo() >= lo() && y.hi() <= hi();
272       }
273       return (y.lo() >= lo() || y.hi() <= hi()) && !isEmpty();
274     } else {
275       if (y.isInverted()) {
276         return isFull() || y.isEmpty();
277       }
278       return y.lo() >= lo() && y.hi() <= hi();
279     }
280   }
281 
282   // Returns true if the interior of this interval contains the entire
283   // interval 'y'.  Note that x.InteriorContains(x) is true only when
284   // x is the empty or full interval, and x.InteriorContains(S1Interval(p,p))
285   // is equivalent to x.InteriorContains(p).
286   bool interiorContains(in S1Interval y) const {
287     if (isInverted()) {
288       if (!y.isInverted()) {
289         return y.lo() > lo() || y.hi() < hi();
290       }
291       return (y.lo() > lo() && y.hi() < hi()) || y.isEmpty();
292     } else {
293       if (y.isInverted()) {
294         return isFull() || y.isEmpty();
295       }
296       return (y.lo() > lo() && y.hi() < hi()) || isFull();
297     }
298   }
299 
300   // Return true if the two intervals contain any points in common.
301   // Note that the point +/-Pi has two representations, so the intervals
302   // [-Pi,-3] and [2,Pi] intersect, for example.
303   bool intersects(in S1Interval y) const {
304     if (isEmpty() || y.isEmpty()) {
305       return false;
306     }
307     if (isInverted()) {
308       // Every non-empty inverted interval contains Pi.
309       return y.isInverted() || y.lo() <= hi() || y.hi() >= lo();
310     } else {
311       if (y.isInverted()) {
312         return y.lo() <= hi() || y.hi() >= lo();
313       }
314       return y.lo() <= hi() && y.hi() >= lo();
315     }
316   }
317 
318   // Return true if the interior of this interval contains any point of the
319   // interval 'y' (including its boundary).  Works for empty, full, and
320   // singleton intervals.
321   bool interiorIntersects(in S1Interval y) const {
322     if (isEmpty() || y.isEmpty() || lo() == hi()) {
323       return false;
324     }
325     if (isInverted()) {
326       return y.isInverted() || y.lo() < hi() || y.hi() > lo();
327     } else {
328       if (y.isInverted()) {
329         return y.lo() < hi() || y.hi() > lo();
330       }
331       return (y.lo() < hi() && y.hi() > lo()) || isFull();
332     }
333   }
334 
335   // Return the Hausdorff distance to the given interval 'y'. For two
336   // S1Intervals x and y, this distance is defined by
337   //     h(x, y) = max_{p in x} min_{q in y} d(p, q),
338   // where d(.,.) is measured along S1.
339   double getDirectedHausdorffDistance(in S1Interval y) const
340   out (distance) {
341     assert(distance >= 0);
342   } do {
343     if (y.contains(this)) {
344       return 0.0;  // this includes the case this is empty
345     }
346     if (y.isEmpty()) {
347       return M_PI;  // maximum possible distance on S1
348     }
349 
350     double y_complement_center = y.getComplementCenter();
351     if (contains(y_complement_center)) {
352       return positiveDistance(y.hi(), y_complement_center);
353     } else {
354       // The Hausdorff distance is realized by either two hi() endpoints or two
355       // lo() endpoints, whichever is farther apart.
356       double hi_hi = S1Interval(y.hi(), y_complement_center).contains(hi()) ?
357           positiveDistance(y.hi(), hi()) : 0;
358       double lo_lo = S1Interval(y_complement_center, y.lo()).contains(lo()) ?
359           positiveDistance(lo(), y.lo()) : 0;
360       return algorithm.max(hi_hi, lo_lo);
361     }
362   }
363 
364   // Expand the interval by the minimum amount necessary so that it
365   // contains the given point "p" (an angle in the range [-Pi, Pi]).
366   void addPoint(double p)
367   in {
368     assert(math.fabs(p) <= M_PI);
369   } do {
370     if (p == -M_PI) {
371       p = M_PI;
372     }
373 
374     if (fastContains(p)) {
375       return;
376     }
377     if (isEmpty()) {
378       setHi(p);
379       setLo(p);
380     } else {
381       // Compute distance from p to each endpoint.
382       double dlo = positiveDistance(p, lo());
383       double dhi = positiveDistance(hi(), p);
384       if (dlo < dhi) {
385         setLo(p);
386       } else {
387         setHi(p);
388       }
389       // Adding a point can never turn a non-full interval into a full one.
390     }
391   }
392 
393   // Return the closest point in the interval to the given point "p".
394   // The interval must be non-empty.
395   double project(double p) const
396   in {
397     assert(!isEmpty());
398     assert(math.fabs(p) <= M_PI);
399   } do {
400     if (p == -M_PI) {
401       p = M_PI;
402     }
403     if (fastContains(p)) {
404       return p;
405     }
406     // Compute distance from p to each endpoint.
407     double dlo = positiveDistance(p, lo());
408     double dhi = positiveDistance(hi(), p);
409     return (dlo < dhi) ? lo() : hi();
410   }
411 
412   // Return an interval that has been expanded on each side by the given
413   // distance "margin".  If "margin" is negative, then shrink the interval on
414   // each side by "margin" instead.  The resulting interval may be empty or
415   // full.  Any expansion (positive or negative) of a full interval remains
416   // full, and any expansion of an empty interval remains empty.
417   S1Interval expanded(double margin) const {
418     if (margin >= 0) {
419       if (isEmpty()) {
420         return this;
421       }
422       // Check whether this interval will be full after expansion, allowing
423       // for a 1-bit rounding error when computing each endpoint.
424       if (getLength() + 2 * margin + 2 * double.epsilon >= 2 * M_PI) {
425         return full();
426       }
427     } else {
428       if (isFull()) {
429         return this;
430       }
431       // Check whether this interval will be empty after expansion, allowing
432       // for a 1-bit rounding error when computing each endpoint.
433       if (getLength() + 2 * margin - 2 * double.epsilon <= 0) {
434         return empty();
435       }
436     }
437     S1Interval result = S1Interval(math.remainder(lo() - margin, 2 * M_PI),
438         math.remainder(hi() + margin, 2 * M_PI));
439     if (result.lo() <= -M_PI) {
440       result.setLo(M_PI);
441     }
442     return result;
443   }
444 
445   // Return the smallest interval that contains this interval and the
446   // given interval "y".
447   S1Interval unite(in S1Interval y) const {
448     // The y.is_full() case is handled correctly in all cases by the code
449     // below, but can follow three separate code paths depending on whether
450     // this interval is inverted, is non-inverted but contains Pi, or neither.
451 
452     if (y.isEmpty()) {
453       return this;
454     }
455     if (fastContains(y.lo())) {
456       if (fastContains(y.hi())) {
457         // Either this interval contains y, or the union of the two
458         // intervals is the Full() interval.
459         if (contains(y)) {
460           return this;  // is_full() code path
461         }
462         return full();
463       }
464       return S1Interval(lo(), y.hi(), ArgsChecked.ARGS_CHECKED);
465     }
466     if (fastContains(y.hi())) {
467       return S1Interval(y.lo(), hi(), ArgsChecked.ARGS_CHECKED);
468     }
469 
470     // This interval contains neither endpoint of y.  This means that either y
471     // contains all of this interval, or the two intervals are disjoint.
472     if (isEmpty() || y.fastContains(lo())) {
473       return y;
474     }
475 
476     // Check which pair of endpoints are closer together.
477     double dlo = positiveDistance(y.hi(), lo());
478     double dhi = positiveDistance(hi(), y.lo());
479     if (dlo < dhi) {
480       return S1Interval(y.lo(), hi(), ArgsChecked.ARGS_CHECKED);
481     } else {
482       return S1Interval(lo(), y.hi(), ArgsChecked.ARGS_CHECKED);
483     }
484   }
485 
486   // Return the smallest interval that contains the intersection of this
487   // interval with "y".  Note that the region of intersection may
488   // consist of two disjoint intervals.
489   S1Interval intersection(in S1Interval y) const {
490     // The y.is_full() case is handled correctly in all cases by the code
491     // below, but can follow three separate code paths depending on whether
492     // this interval is inverted, is non-inverted but contains Pi, or neither.
493 
494     if (y.isEmpty()) {
495       return empty();
496     }
497     if (fastContains(y.lo())) {
498       if (fastContains(y.hi())) {
499         // Either this interval contains y, or the region of intersection
500         // consists of two disjoint subintervals.  In either case, we want
501         // to return the shorter of the two original intervals.
502         if (y.getLength() < getLength()) {
503           return y;  // is_full() code path
504         }
505         return this;
506       }
507       return S1Interval(y.lo(), hi(), ArgsChecked.ARGS_CHECKED);
508     }
509     if (fastContains(y.hi())) {
510       return S1Interval(lo(), y.hi(), ArgsChecked.ARGS_CHECKED);
511     }
512 
513     // This interval contains neither endpoint of y.  This means that either y
514     // contains all of this interval, or the two intervals are disjoint.
515 
516     if (y.fastContains(lo())) {
517       return this;  // is_empty() okay here
518     }
519     assert(!intersects(y));
520     return empty();
521 
522   }
523 
524   // Return true if two intervals contains the same set of points.
525   bool opEquals(in S1Interval y) const {
526     return lo() == y.lo() && hi() == y.hi();
527   }
528 
529 
530   // Return true if this interval can be transformed into the given interval by
531   // moving each endpoint by at most "max_error" (and without the endpoints
532   // crossing, which would invert the interval).  Empty and full intervals are
533   // considered to start at an arbitrary point on the unit circle, thus any
534   // interval with (length <= 2*max_error) matches the empty interval, and any
535   // interval with (length >= 2*Pi - 2*max_error) matches the full interval.
536   bool approxEquals(in S1Interval y, double max_error = 1e-15) const {
537     // Full and empty intervals require special cases because the "endpoints"
538     // are considered to be positioned arbitrarily.
539     if (isEmpty()) {
540       return y.getLength() <= 2 * max_error;
541     }
542     if (y.isEmpty()) {
543       return getLength() <= 2 * max_error;
544     }
545     if (isFull()) {
546       return y.getLength() >= 2 * (M_PI - max_error);
547     }
548     if (y.isFull()) {
549       return getLength() >= 2 * (M_PI - max_error);
550     }
551 
552     // The purpose of the last test below is to verify that moving the endpoints
553     // does not invert the interval, e.g. [-1e20, 1e20] vs. [1e20, -1e20].
554     return (math.fabs(math.remainder(y.lo() - lo(), 2 * M_PI)) <= max_error
555         && math.fabs(math.remainder(y.hi() - hi(), 2 * M_PI)) <= max_error
556         && math.fabs(getLength() - y.getLength()) <= 2 * max_error);
557   }
558 
559   // Low-level methods to modify one endpoint of an existing S1Interval.
560   // These methods should really be private because setting just one endpoint
561   // can violate the invariants maintained by S1Interval.  In particular:
562   //
563   //  - It is not valid to call these methods on an Empty() or Full()
564   //    interval, since these intervals do not have any endpoints.
565   //
566   //  - It is not allowed to set an endpoint to -Pi.  (When these methods are
567   //    used internally, values of -Pi have already been normalized to Pi.)
568   //
569   // The preferred way to modify both endpoints of an interval is to use a
570   // constructor, e.g. lng = S1Interval(lng_lo, lng_hi).
571   void setLo(double p)
572   out {
573     assert(isValid());
574   } do {
575     _bounds[0] = p;
576   }
577 
578   void setHi(double p)
579   out {
580     assert(isValid());
581   } do {
582     _bounds[1] = p;
583   }
584 
585   string toString() const {
586     return "[" ~ conv.to!string(lo()) ~ ", " ~ conv.to!string(hi()) ~ "]";
587   }
588 }