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 }