├── phpunit.xml ├── bootstrap.php ├── S2AreaCentroid.php ├── S2Region.php ├── S2Edge.php ├── LICENSE ├── R2Vector.php ├── S1Angle.php ├── S2Point.php ├── tests └── SmokeTest.php ├── R1Interval.php ├── S2Polyline.php ├── S2LatLng.php ├── S2Projections.php ├── S1Interval.php ├── S2Cap.php ├── S2Cell.php ├── S2EdgeIndex.php ├── S2RegionCoverer.php ├── S2CellUnion.php └── S2PolygonBuilder.php /phpunit.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | -------------------------------------------------------------------------------- /bootstrap.php: -------------------------------------------------------------------------------- 1 | area = $area; 11 | $this->centroid = $centroid; 12 | } 13 | 14 | public function getArea() { 15 | return $this->area; 16 | } 17 | 18 | public function getCentroid() { 19 | return $this->centroid; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /S2Region.php: -------------------------------------------------------------------------------- 1 | start = $start; 19 | $this->end = $end; 20 | } 21 | 22 | /** 23 | * @return \S2Point 24 | */ 25 | public function getStart() { 26 | return $this->start; 27 | } 28 | 29 | public function getEnd() { 30 | return $this->end; 31 | } 32 | 33 | public function toString() { 34 | return sprintf( 35 | "Edge: (%s -> %s)\n or [%s -> %s]", 36 | $this->start->toDegreesString(), 37 | $this->end->toDegreesString(), 38 | $this->start, 39 | $this->end 40 | ); 41 | } 42 | 43 | public function hashCode() { 44 | return $this->getStart()->hashCode() - $this->getEnd()->hashCode(); 45 | } 46 | 47 | public function equals($o) { 48 | if ($o == null || !($o instanceof S2Edge)) { 49 | return false; 50 | } 51 | $other = $o; 52 | return $this->getStart()->equals($other->getStart()) && $this->getEnd()->equals($other->getEnd()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Evgeniy Makhrov 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /R2Vector.php: -------------------------------------------------------------------------------- 1 | x = $x; 10 | $this->y = $y; 11 | } else if ($x != null) { 12 | if (!is_array($x) || count($x) != 2) throw new \Exception("Points must have exactly 2 coordinates"); 13 | $this->x = $x[0]; 14 | $this->y = $x[1]; 15 | } else { 16 | $this->x = 0; 17 | $this->y = 0; 18 | } 19 | } 20 | 21 | public function x() { 22 | return $this->x; 23 | } 24 | 25 | public function y() { 26 | return $this->y; 27 | } 28 | 29 | public function get($index) { 30 | if ($index > 1) { 31 | throw new \Exception($index); 32 | } 33 | return $index == 0 ? $this->x : $this->y; 34 | } 35 | 36 | public static function add(R2Vector $p1, R2Vector $p2) { 37 | return new R2Vector($p1->x + $p2->x, $p1->y + $p2->y); 38 | } 39 | 40 | public static function mul(R2Vector $p, $m) { 41 | return new R2Vector($m * $p->x, $m * $p->y); 42 | } 43 | 44 | public function norm2() { 45 | return ($this->x * $this->x) + ($this->y * $this->y); 46 | } 47 | 48 | public static function sdotProd(R2Vector $p1, R2Vector $p2) { 49 | return ($p1->x * $p2->x) + ($p1->y * $p2->y); 50 | } 51 | 52 | public function dotProd(R2Vector $that) { 53 | return self::sdotProd($this, $that); 54 | } 55 | 56 | public function crossProd(R2Vector $that) { 57 | return $this->x * $that->y - $this->y * $that->x; 58 | } 59 | 60 | public function lessThan(R2Vector $vb) { 61 | if ($this->x < $vb->x) { 62 | return true; 63 | } 64 | if ($vb->x < $this->x) { 65 | return false; 66 | } 67 | if ($this->y < $vb->y) { 68 | return true; 69 | } 70 | return false; 71 | } 72 | 73 | public function equals($that) { 74 | if (!($that instanceof R2Vector)) { 75 | return false; 76 | } 77 | $thatPoint = $that; 78 | return $this->x == $thatPoint->x && $this->y == $thatPoint->y; 79 | } 80 | 81 | /** 82 | * Calculates hashcode based on stored coordinates. Since we want +0.0 and 83 | * -0.0 to be treated the same, we ignore the sign of the coordinates. 84 | */ 85 | public function hashCode() { 86 | $value = 17; 87 | //$value += 37 * $value + Double.doubleToLongBits(abs($this->x)); 88 | //$value += 37 * $value + Double.doubleToLongBits(abs($this->y)); 89 | //return (int) ($value ^ ($value >>> 32)); 90 | } 91 | 92 | public function toString() { 93 | return "(" . $this->x . ", " . $this->y . ")"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /S1Angle.php: -------------------------------------------------------------------------------- 1 | radians; 12 | } 13 | 14 | public static function sradians($radians) { 15 | return new S1Angle($radians); 16 | } 17 | 18 | /** 19 | * @return double 20 | */ 21 | public function degrees() { 22 | return $this->radians * (180 / M_PI); 23 | } 24 | 25 | /** 26 | * @param double $degrees 27 | * @return S1Angle 28 | */ 29 | public static function sdegrees($degrees) { 30 | return new S1Angle($degrees * (M_PI / 180)); 31 | } 32 | 33 | public function e5() { 34 | return round($this->degrees() * 1e5); 35 | } 36 | 37 | public function e6() { 38 | return round($this->degrees() * 1e6); 39 | } 40 | 41 | public function e7() { 42 | return round($this->degrees() * 1e7); 43 | } 44 | 45 | /** 46 | * @param double|S2Point $radians_or_x 47 | * @param S2Point $y 48 | * Return the angle between two points, which is also equal to the distance 49 | * between these points on the unit sphere. The points do not need to be 50 | * normalized. 51 | */ 52 | public function __construct($radians_or_x = null, $y = null) { 53 | if ($radians_or_x instanceof S2Point && $y instanceof S2Point) { 54 | $this->radians = $radians_or_x->angle($y); 55 | } else { 56 | $this->radians = $radians_or_x === null ? 0 : $radians_or_x; 57 | } 58 | } 59 | 60 | public function equals($that) { 61 | if ($that instanceof S1Angle) { 62 | return $this->radians() == $that->radians(); 63 | } 64 | return false; 65 | } 66 | 67 | public function hashCode() { 68 | //$value = Double.doubleToLongBits(radians); 69 | //return (int) (value ^ (value >>> 32)); 70 | } 71 | 72 | public function lessThan(S1Angle $that) { 73 | return $this->radians() < $that->radians(); 74 | } 75 | 76 | public function greaterThan(S1Angle $that) { 77 | return $this->radians() > $that->radians(); 78 | } 79 | 80 | public function lessOrEquals(S1Angle $that) { 81 | return $this->radians() <= $that->radians(); 82 | } 83 | 84 | public function greaterOrEquals(S1Angle $that) { 85 | return $this->radians() >= $that->radians(); 86 | } 87 | 88 | public static function max(S1Angle $left, S1Angle $right) { 89 | return $right->greaterThan($left) ? $right : $left; 90 | } 91 | 92 | public static function min(S1Angle $left, S1Angle $right) { 93 | return $right->greaterThan($left) ? $left : $right; 94 | } 95 | 96 | public static function se5($e5) { 97 | return self::sdegrees($e5 * 1e-5); 98 | } 99 | 100 | public static function se6($e6) { 101 | // Multiplying by 1e-6 isn't quite as accurate as dividing by 1e6, 102 | // but it's about 10 times faster and more than accurate enough. 103 | return self::sdegrees($e6 * 1e-6); 104 | } 105 | 106 | public static function se7($e7) { 107 | return self::sdegrees($e7 * 1e-7); 108 | } 109 | 110 | /** 111 | * Writes the angle in degrees with a "d" suffix, e.g. "17.3745d". By default 112 | * 6 digits are printed; this can be changed using setprecision(). Up to 17 113 | * digits are required to distinguish one angle from another. 114 | */ 115 | public function toString() { 116 | return $this->degrees() . "d"; 117 | } 118 | 119 | public function compareTo(S1Angle $that) { 120 | return $this->radians < $that->radians ? -1 : $this->radians > $that->radians ? 1 : 0; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /S2Point.php: -------------------------------------------------------------------------------- 1 | x = $this->y = $this->z = 0; 11 | else { 12 | $this->x = $x; 13 | $this->y = $y; 14 | $this->z = $z; 15 | } 16 | } 17 | 18 | public static function minus(S2Point $p1, S2Point $p2) { 19 | return self::sub($p1, $p2); 20 | } 21 | 22 | public static function neg(S2Point $p) { 23 | return new S2Point(-$p->x, -$p->y, -$p->z); 24 | } 25 | 26 | public function norm2() { 27 | return $this->x * $this->x + $this->y * $this->y + $this->z * $this->z; 28 | } 29 | 30 | public function norm() { 31 | return sqrt($this->norm2()); 32 | } 33 | 34 | public static function crossProd(S2Point $p1, S2Point $p2) { 35 | return new S2Point( 36 | $p1->y * $p2->z - $p1->z * $p2->y, 37 | $p1->z * $p2->x - $p1->x * $p2->z, 38 | $p1->x * $p2->y - $p1->y * $p2->x 39 | ); 40 | } 41 | 42 | public static function add(S2Point $p1, S2Point $p2) { 43 | return new S2Point($p1->x + $p2->x, $p1->y + $p2->y, $p1->z + $p2->z); 44 | } 45 | 46 | public static function sub(S2Point $p1, S2Point $p2) { 47 | return new S2Point($p1->x - $p2->x, $p1->y - $p2->y, $p1->z - $p2->z); 48 | } 49 | 50 | public function dotProd(S2Point $that) { 51 | return $this->x * $that->x + $this->y * $that->y + $this->z * $that->z; 52 | } 53 | 54 | public static function mul(S2Point $p, $m) { 55 | return new S2Point($m * $p->x, $m * $p->y, $m * $p->z); 56 | } 57 | 58 | public static function div(S2Point $p, $m) { 59 | return new S2Point($p->x / $m, $p->y / $m, $p->z / $m); 60 | } 61 | 62 | /** return a vector orthogonal to this one */ 63 | public function ortho() { 64 | $k = $this->largestAbsComponent(); 65 | if ($k == 1) { 66 | $temp = new S2Point(1, 0, 0); 67 | } else if ($k == 2) { 68 | $temp = new S2Point(0, 1, 0); 69 | } else { 70 | $temp = new S2Point(0, 0, 1); 71 | } 72 | return S2Point::normalize($this->crossProd($this, $temp)); 73 | } 74 | 75 | /** Return the index of the largest component fabs */ 76 | public function largestAbsComponent() { 77 | $temp = $this->fabs($this); 78 | if ($temp->x > $temp->y) { 79 | if ($temp->x > $temp->z) { 80 | return 0; 81 | } else { 82 | return 2; 83 | } 84 | } else { 85 | if ($temp->y > $temp->z) { 86 | return 1; 87 | } else { 88 | return 2; 89 | } 90 | } 91 | } 92 | 93 | public static function fabs(S2Point $p) { 94 | return new S2Point(abs($p->x), abs($p->y), abs($p->z)); 95 | } 96 | 97 | public static function normalize(S2Point $p) { 98 | $norm = $p->norm(); 99 | if ($norm != 0) { 100 | $norm = 1.0 / $norm; 101 | } 102 | return S2Point::mul($p, $norm); 103 | } 104 | 105 | public function get($axis) { 106 | return ($axis == 0) ? $this->x : (($axis == 1) ? $this->y : $this->z); 107 | } 108 | 109 | /** Return the angle between two vectors in radians */ 110 | public function angle(S2Point $va) { 111 | return atan2($this->crossProd($this, $va)->norm(), $this->dotProd($va)); 112 | } 113 | 114 | /** 115 | * Compare two vectors, return true if all their components are within a 116 | * difference of margin. 117 | */ 118 | private function aequal(S2Point $that, $margin) { 119 | return (abs($this->x - $that->x) < $margin) && (abs($this->y - $that->y) < $margin) 120 | && (abs($this->z - $that->z) < $margin); 121 | } 122 | 123 | public function equals($that) { 124 | if (!($that instanceof S2Point)) { 125 | return false; 126 | } 127 | $thatPoint = $that; 128 | return $this->x == $thatPoint->x && $this->y == $thatPoint->y && $this->z == $thatPoint->z; 129 | } 130 | 131 | public function lessThan(S2Point $vb) { 132 | if ($this->x < $vb->x) { 133 | return true; 134 | } 135 | if ($vb->x < $this->x) { 136 | return false; 137 | } 138 | if ($this->y < $vb->y) { 139 | return true; 140 | } 141 | if ($vb->y < $this->y) { 142 | return false; 143 | } 144 | if ($this->z < $vb->z) { 145 | return true; 146 | } 147 | return false; 148 | } 149 | 150 | public function compareTo(S2Point $other) { 151 | return ($this->lessThan($other) ? -1 : ($this->equals($other) ? 0 : 1)); 152 | } 153 | 154 | public function toString() { 155 | return "(" . $this->x . ", " . $this->y . ", " . $this->z . ")"; 156 | } 157 | 158 | public function toDegreesString() { 159 | $s2LatLng = new S2LatLng($this); 160 | return "(" . $s2LatLng->latDegrees() . ", " 161 | . $s2LatLng->lngDegrees() . ")"; 162 | } 163 | 164 | /** 165 | * Calcualates hashcode based on stored coordinates. Since we want +0.0 and 166 | * -0.0 to be treated the same, we ignore the sign of the coordinates. 167 | */ 168 | public function hashCode() { 169 | $value = 17; 170 | // $value += 37 * $value + Double.doubleToLongBits(Math.abs(x)); 171 | // $value += 37 * $value + Double.doubleToLongBits(Math.abs(y)); 172 | // $value += 37 * $value + Double.doubleToLongBits(Math.abs(z)); 173 | // return (int) ($value ^ ($value >>> 32)); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/SmokeTest.php: -------------------------------------------------------------------------------- 1 | latRadians() - $b->latRadians()) 14 | + cos($a->latRadians()) * cos($b->latRadians()) * self::Haversin($a->lngRadians() - $b->lngRadians()); 15 | $ret = 2 * self::EARTH_RADIUS * asin(sqrt($angle)); 16 | return $ret; 17 | } 18 | 19 | public static function Haversin($a) { 20 | return (1 - cos($a)) / 2; 21 | } 22 | 23 | public static function greatCircleBearing(S2LatLng $a, S2LatLng $b) { 24 | $cos_latb = cos($b->latRadians()); 25 | $dlon = $b->lngRadians() - $a->lngRadians(); 26 | $y = sin($dlon) * $cos_latb; 27 | $x = cos($a->latRadians()) * sin($b->latRadians()) - sin($a->latRadians()) * $cos_latb * cos($dlon); 28 | $brng = atan2($y, $x); 29 | return $brng; 30 | } 31 | 32 | public static function greatCircleDestination(S2LatLng $a, $bearing, $distance) { 33 | $cos_dist_earth = cos($distance / self::EARTH_RADIUS); 34 | $sin_dist_earth = sin($distance / self::EARTH_RADIUS); 35 | $sin_lat = sin($a->latRadians()); 36 | $cos_lat = cos($a->latRadians()); 37 | $lat = asin( 38 | $sin_lat * $cos_dist_earth + 39 | $cos_lat * $sin_dist_earth * cos($bearing) 40 | ); 41 | $lng = $a->lngRadians() 42 | + atan2( 43 | sin($bearing) * $sin_dist_earth * $cos_lat, 44 | $cos_dist_earth - $sin_lat * sin($lat) 45 | ); 46 | return new S2LatLng($lat, $lng); 47 | } 48 | 49 | public static function encodeLocation(S2LatLng $s2ll){ 50 | $lat = (int) ($s2ll->latDegrees() * 1000000); 51 | $lng = (int) ($s2ll->lngDegrees() * 1000000); 52 | $ret = sprintf("%08x,%08x", $lat, $lng); 53 | return $ret; 54 | } 55 | 56 | public static function decodeLocation($loc) { 57 | if (strpos($loc, ',') === false) return false; 58 | list ($lat, $lng) = explode(',', $loc); 59 | return S2LatLng::fromDegrees(hexdec($lat) / 1000000, hexdec($lng) / 1000000); 60 | } 61 | 62 | function find_some_point_with_distance($lat, $lng) { 63 | // find some point with distance in 30..40 from specified point 64 | $lat_p = array($lat, $lat, 0, $lat + 0.01, 1e10); 65 | $lng_p = array($lng, $lng, 0, $lng + 0.01, 1e10); 66 | $loc_port = S2LatLng::fromDegrees($lat,$lng); 67 | for ($i = 0; $i < 10; $i++) { 68 | $loc_1 = S2LatLng::fromDegrees($lat_p[0],$lng_p[0]); 69 | $dist = self::GreatEarthDistance($loc_1, $loc_port); 70 | if ($dist < 30) { 71 | if ($dist > $lat_p[2]) { 72 | $lat_p[2] = $dist; 73 | $lat_p[1] = $lat_p[0]; 74 | } 75 | if ($dist > $lng_p[2]) { 76 | $lng_p[2] = $dist; 77 | $lng_p[1] = $lng_p[0]; 78 | } 79 | } else if ($dist > 39.9) { 80 | if ($dist < $lat_p[4]) { 81 | $lat_p[4] = $dist; 82 | $lat_p[3] = $lat_p[0]; 83 | } 84 | if ($dist < $lng_p[4]) { 85 | $lng_p[4] = $dist; 86 | $lng_p[3] = $lng_p[0]; 87 | } 88 | } else { 89 | echo "found $lat_p[0] $lng_p[0]\n"; 90 | break; 91 | } 92 | $lat_p[0] = $lat_p[1] + ($lat_p[3] - $lat_p[1]) / 2; 93 | $lng_p[0] = $lng_p[1] + ($lng_p[3] - $lng_p[1]) / 2; 94 | } 95 | die; 96 | } 97 | 98 | private static function guidToToken($guid) { 99 | return substr($guid, 0, 16); 100 | } 101 | 102 | public function testA() { 103 | $hex_loc = '0351272d,0242b406'; 104 | $this->assertEquals($hex_loc, self::encodeLocation(self::decodeLocation($hex_loc))); 105 | 106 | $from = S2LatLng::fromDegrees(55.578201,37.912176); 107 | $to = S2LatLng::fromDegrees(55.578324,37.9109); 108 | 109 | $dist = self::GreatEarthDistance($from, $to); 110 | 111 | $bearing = self::greatCircleBearing($from, $to); 112 | $to2 = self::greatCircleDestination($from, $bearing, 40); 113 | $bearing2 = self::greatCircleBearing($to2, $to); 114 | $to3 = self::greatCircleDestination($to2, $bearing2, $dist - 40); 115 | 116 | $dist3 = self::GreatEarthDistance($from, $to3); 117 | 118 | $this->assertEquals(0.9700225997852, $from->latRadians()); 119 | $this->assertEquals(0.66169229779557, $from->lngRadians()); 120 | 121 | $this->assertEquals(0.97002474654019, $to->latRadians()); 122 | $this->assertEquals(0.66167002739432, $to->lngRadians()); 123 | 124 | $this->assertEquals(0.97002365521829, $to2->latRadians()); 125 | $this->assertEquals(0.66168134906715, $to2->lngRadians()); 126 | 127 | $this->assertEquals(0.97002474654019, $to3->latRadians()); 128 | $this->assertEquals(0.66167002739432, $to3->lngRadians()); 129 | 130 | $this->assertEquals(-1.4018857232359, $bearing); 131 | $this->assertEquals(-1.4018947548004, $bearing2); 132 | $this->assertEquals(81.362381188294, $dist); 133 | $this->assertEquals(81.362381188297, $dist3); 134 | } 135 | 136 | public function testB() { 137 | $lat = 55.613855; 138 | $lng = 37.978578; 139 | 140 | $loc = S2LatLng::fromDegrees(55.6141284375, 37.9788514375); 141 | 142 | $s2ll = S2CellId::fromToken(self::guidToToken('700c7c5346a246ee88eee70b200d0b33.16'))->toLatLng(); 143 | $this->assertEquals('(-0.023004811178492, -3.06557268979)', (string)$s2ll); 144 | 145 | $s2ll = S2CellId::fromToken(self::guidToToken('414ab9b68fd0000000082a7300000025.6'))->toLatLng(); 146 | $this->assertEquals('(0.97053474915648, 0.66268771618818)', (string)$s2ll); 147 | 148 | $loc_1 = S2LatLng::fromDegrees(55.605873,37.970864); 149 | $loc_ezhio = S2LatLng::fromE6(55608152, 37972176); 150 | $loc_oktz = S2LatLng::fromE6(55607195, 37971367); 151 | $loc_art = S2LatLng::fromE6(55605726, 37970664); 152 | $dist = self::GreatEarthDistance($loc_1, $s2ll); 153 | $this->assertEquals(212.99711509717, $dist); 154 | } 155 | 156 | public function testPolygon() { 157 | $S2Polygon = new S2Polygon(); 158 | 159 | $S2Polygon = new S2Polygon(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /R1Interval.php: -------------------------------------------------------------------------------- 1 | hi, the interval is empty. */ 8 | public function __construct($lo, $hi) { 9 | $this->lo = $lo; 10 | $this->hi = $hi; 11 | } 12 | 13 | /** 14 | * Returns an empty interval. (Any interval where lo > hi is considered 15 | * empty.) 16 | */ 17 | public static function emptya() { 18 | return new R1Interval(1, 0); 19 | } 20 | 21 | /** 22 | * Convenience method to construct an interval containing a single point. 23 | */ 24 | public static function fromPoint($p) { 25 | return new R1Interval($p, $p); 26 | } 27 | 28 | /** 29 | * Convenience method to construct the minimal interval containing the two 30 | * given points. This is equivalent to starting with an empty interval and 31 | * calling AddPoint() twice, but it is more efficient. 32 | */ 33 | public static function fromPointPair($p1, $p2) { 34 | if ($p1 <= $p2) { 35 | return new R1Interval($p1, $p2); 36 | } else { 37 | return new R1Interval($p2, $p1); 38 | } 39 | } 40 | 41 | public function lo() { 42 | return $this->lo; 43 | } 44 | 45 | public function hi() { 46 | return $this->hi; 47 | } 48 | 49 | /** 50 | * Return true if the interval is empty, i.e. it contains no points. 51 | */ 52 | public function isEmpty() { 53 | return $this->lo() > $this->hi(); 54 | } 55 | 56 | /** 57 | * Return the center of the interval. For empty intervals, the result is 58 | * arbitrary. 59 | */ 60 | public function getCenter() { 61 | return 0.5 * ($this->lo() + $this->hi()); 62 | } 63 | 64 | /** 65 | * Return the length of the interval. The length of an empty interval is 66 | * negative. 67 | */ 68 | public function getLength() { 69 | return $this->hi() - $this->lo(); 70 | } 71 | 72 | /** Return true if this interval contains the interval 'y'. */ 73 | public function contains($p) { 74 | if ($p instanceof R1Interval) { 75 | $y = $p; 76 | if ($y->isEmpty()) { 77 | return true; 78 | } 79 | return $y->lo() >= $this->lo() && $y->hi() <= $this->hi(); 80 | } 81 | return $p >= $this->lo() && $p <= $this->hi(); 82 | } 83 | 84 | /** 85 | * Return true if the interior of this interval contains the entire interval 86 | * 'y' (including its boundary). 87 | */ 88 | public function interiorContains($p) { 89 | if ($p instanceof R1Interval) { 90 | $y = $p; 91 | if ($y->isEmpty()) { 92 | return true; 93 | } 94 | return $y->lo() > $this->lo() && $y->hi() < $this->hi(); 95 | } 96 | return $p > $this->lo() && $p < $this->hi(); 97 | } 98 | 99 | /** 100 | * Return true if this interval intersects the given interval, i.e. if they 101 | * have any points in common. 102 | */ 103 | public function intersects(R1Interval $y) { 104 | if ($this->lo() <= $y->lo()) { 105 | return $y->lo() <= $this->hi() && $y->lo() <= $y->hi(); 106 | } else { 107 | return $this->lo() <= $y->hi() && $this->lo() <= $this->hi(); 108 | } 109 | } 110 | 111 | /** 112 | * Return true if the interior of this interval intersects any point of the 113 | * given interval (including its boundary). 114 | */ 115 | public function interiorIntersects(R1Interval $y) { 116 | return $y->lo() < $this->hi() && $this->lo() < $y->hi() && $this->lo() < $this->hi() && $y->lo() <= $y->hi(); 117 | } 118 | 119 | /** Expand the interval so that it contains the given point "p". */ 120 | public function addPoint($p) { 121 | if (isEmpty()) { 122 | return R1Interval::fromPoint($p); 123 | } else if ($p < $this->lo()) { 124 | return new R1Interval($p, $this->hi()); 125 | } else if ($p > $this->hi()) { 126 | return new R1Interval($this->lo(), $p); 127 | } else { 128 | return new R1Interval($this->lo(), $this->hi()); 129 | } 130 | } 131 | 132 | /** 133 | * Return an interval that contains all points with a distance "radius" of a 134 | * point in this interval. Note that the expansion of an empty interval is 135 | * always empty. 136 | */ 137 | public function expanded($radius) { 138 | // assert (radius >= 0); 139 | if ($this->isEmpty()) { 140 | return $this; 141 | } 142 | return new R1Interval($this->lo() - $radius, $this->hi() + $radius); 143 | } 144 | 145 | /** 146 | * Return the smallest interval that contains this interval and the given 147 | * interval "y". 148 | */ 149 | public function union(R1Interval $y) { 150 | if ($this->isEmpty()) { 151 | return $y; 152 | } 153 | if ($y->isEmpty()) { 154 | return $this; 155 | } 156 | return new R1Interval(min($this->lo(), $y->lo()), max($this->hi(), $y->hi())); 157 | } 158 | 159 | /** 160 | * Return the intersection of this interval with the given interval. Empty 161 | * intervals do not need to be special-cased. 162 | */ 163 | public function intersection(R1Interval $y) { 164 | return new R1Interval(max($this->lo(), $y->lo()), min($this->hi(), $y->hi())); 165 | } 166 | 167 | public function equals($that) { 168 | if ($that instanceof R1Interval) { 169 | $y = $that; 170 | // Return true if two intervals contain the same set of points. 171 | return ($this->lo() == $y->lo() && $this->hi() == $y->hi()) || ($this->isEmpty() && $y->isEmpty()); 172 | } 173 | return false; 174 | } 175 | 176 | public function hashCode() { 177 | if (isEmpty()) { 178 | return 17; 179 | } 180 | 181 | $value = 17; 182 | // $value = 37 * $value + Double.doubleToLongBits($this->lo); 183 | // $value = 37 * $value + Double.doubleToLongBits($this->hi); 184 | // return (int)($value ^ ($value >>> 32)); 185 | } 186 | 187 | /** 188 | * Return true if length of the symmetric difference between the two intervals 189 | * is at most the given tolerance. 190 | * 191 | */ 192 | public function approxEquals(R1Interval $y, $maxError = null) { 193 | if ($maxError === null) { 194 | return $this->approxEquals($y, 1e-15); 195 | } 196 | if ($this->isEmpty()) { 197 | return $y->getLength() <= $maxError; 198 | } 199 | if ($y->isEmpty()) { 200 | return $this->getLength() <= $maxError; 201 | } 202 | return abs($y->lo() - $this->lo()) + abs($y->hi() - $this->hi()) <= $maxError; 203 | } 204 | 205 | public function toString() { 206 | return "[" . $this->lo() . ", " . $this->hi() . "]"; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /S2Polyline.php: -------------------------------------------------------------------------------- 1 | Note: Polylines do not have a Contains(S2Point) method, because 10 | * "containment" is not numerically well-defined except at the polyline 11 | * vertices. 12 | * 13 | */ 14 | 15 | class S2Polyline implements S2Region { 16 | //private static final Logger log = Logger.getLogger(S2Polyline.class.getCanonicalName()); 17 | 18 | //private final int numVertices; 19 | private $numVertices; 20 | //private final S2Point[] vertices; 21 | private $vertices; 22 | 23 | /** 24 | * Create a polyline that connects the given vertices. Empty polylines are 25 | * allowed. Adjacent vertices should not be identical or antipodal. All 26 | * vertices should be unit length. 27 | *#/ 28 | public S2Polyline(List 29 | vertices) { 30 | // assert isValid(vertices); 31 | this.numVertices = vertices.size(); 32 | this.vertices = vertices.toArray(new S2Point[numVertices]); 33 | } 34 | 35 | /** 36 | * Copy constructor. 37 | * 38 | * TODO(dbeaumont): Now that S2Polyline is immutable, remove this. 39 | *#/ 40 | public S2Polyline(S2Polyline src) { 41 | this.numVertices = src.numVertices(); 42 | this.vertices = src.vertices.clone(); 43 | } 44 | 45 | /** 46 | * Return true if the given vertices form a valid polyline. 47 | *#/ 48 | public boolean isValid(List 49 | vertices) { 50 | // All vertices must be unit length. 51 | int n = vertices.size(); 52 | for (int i = 0; i < n; ++i) { 53 | if (!S2.isUnitLength(vertices.get(i))) { 54 | log.info("Vertex " + i + " is not unit length"); 55 | return false; 56 | } 57 | } 58 | 59 | // Adjacent vertices must not be identical or antipodal. 60 | for (int i = 1; i < n; ++i) { 61 | if (vertices.get(i - 1).equals(vertices.get(i)) 62 | || vertices.get(i - 1).equals(S2Point.neg(vertices.get(i)))) { 63 | log.info("Vertices " + (i - 1) + " and " + i + " are identical or antipodal"); 64 | return false; 65 | } 66 | } 67 | 68 | return true; 69 | } 70 | 71 | public int numVertices() { 72 | return numVertices; 73 | } 74 | 75 | public S2Point vertex(int k) { 76 | // assert (k >= 0 && k < numVertices); 77 | return vertices[k]; 78 | } 79 | 80 | /** 81 | * Return the angle corresponding to the total arclength of the polyline on a 82 | * unit sphere. 83 | *#/ 84 | public S1Angle getArclengthAngle() { 85 | double lengthSum = 0; 86 | for (int i = 1; i < numVertices(); ++i) { 87 | lengthSum += vertex(i - 1).angle(vertex(i)); 88 | } 89 | return S1Angle.radians(lengthSum); 90 | } 91 | 92 | /** 93 | * Return the point whose distance from vertex 0 along the polyline is the 94 | * given fraction of the polyline's total length. Fractions less than zero or 95 | * greater than one are clamped. The return value is unit length. This cost of 96 | * this function is currently linear in the number of vertices. 97 | *#/ 98 | public S2Point interpolate(double fraction) { 99 | // We intentionally let the (fraction >= 1) case fall through, since 100 | // we need to handle it in the loop below in any case because of 101 | // possible roundoff errors. 102 | if (fraction <= 0) { 103 | return vertex(0); 104 | } 105 | 106 | double lengthSum = 0; 107 | for (int i = 1; i < numVertices(); ++i) { 108 | lengthSum += vertex(i - 1).angle(vertex(i)); 109 | } 110 | double target = fraction * lengthSum; 111 | for (int i = 1; i < numVertices(); ++i) { 112 | double length = vertex(i - 1).angle(vertex(i)); 113 | if (target < length) { 114 | // This code interpolates with respect to arc length rather than 115 | // straight-line distance, and produces a unit-length result. 116 | double f = Math.sin(target) / Math.sin(length); 117 | return S2Point.add(S2Point.mul(vertex(i - 1), (Math.cos(target) - f * Math.cos(length))), 118 | S2Point.mul(vertex(i), f)); 119 | } 120 | target -= length; 121 | } 122 | return vertex(numVertices() - 1); 123 | } 124 | 125 | // S2Region interface (see {@code S2Region} for details): 126 | 127 | /** Return a bounding spherical cap. *#/ 128 | @Override 129 | public S2Cap getCapBound() { 130 | return getRectBound().getCapBound(); 131 | } 132 | 133 | 134 | /** Return a bounding latitude-longitude rectangle. *#/ 135 | @Override 136 | public S2LatLngRect getRectBound() { 137 | S2EdgeUtil.RectBounder bounder = new S2EdgeUtil.RectBounder(); 138 | for (int i = 0; i < numVertices(); ++i) { 139 | bounder.addPoint(vertex(i)); 140 | } 141 | return bounder.getBound(); 142 | } 143 | 144 | /** 145 | * If this method returns true, the region completely contains the given cell. 146 | * Otherwise, either the region does not contain the cell or the containment 147 | * relationship could not be determined. 148 | *#/ 149 | @Override 150 | public boolean contains(S2Cell cell) { 151 | throw new UnsupportedOperationException( 152 | "'containment' is not numerically well-defined " + "except at the polyline vertices"); 153 | } 154 | 155 | /** 156 | * If this method returns false, the region does not intersect the given cell. 157 | * Otherwise, either region intersects the cell, or the intersection 158 | * relationship could not be determined. 159 | *#/ 160 | @Override 161 | public boolean mayIntersect(S2Cell cell) { 162 | if (numVertices() == 0) { 163 | return false; 164 | } 165 | 166 | // We only need to check whether the cell contains vertex 0 for correctness, 167 | // but these tests are cheap compared to edge crossings so we might as well 168 | // check all the vertices. 169 | for (int i = 0; i < numVertices(); ++i) { 170 | if (cell.contains(vertex(i))) { 171 | return true; 172 | } 173 | } 174 | S2Point[] cellVertices = new S2Point[4]; 175 | for (int i = 0; i < 4; ++i) { 176 | cellVertices[i] = cell.getVertex(i); 177 | } 178 | for (int j = 0; j < 4; ++j) { 179 | S2EdgeUtil.EdgeCrosser crosser = 180 | new S2EdgeUtil.EdgeCrosser(cellVertices[j], cellVertices[(j + 1) & 3], vertex(0)); 181 | for (int i = 1; i < numVertices(); ++i) { 182 | if (crosser.robustCrossing(vertex(i)) >= 0) { 183 | // There is a proper crossing, or two vertices were the same. 184 | return true; 185 | } 186 | } 187 | } 188 | return false; 189 | } 190 | 191 | /** 192 | * Given a point, returns the index of the start point of the (first) edge on 193 | * the polyline that is closest to the given point. The polyline must have at 194 | * least one vertex. Throws IllegalStateException if this is not the case. 195 | *#/ 196 | public int getNearestEdgeIndex(S2Point point) { 197 | Preconditions.checkState(numVertices() > 0, "Empty polyline"); 198 | 199 | if (numVertices() == 1) { 200 | // If there is only one vertex, the "edge" is trivial, and it's the only one 201 | return 0; 202 | } 203 | 204 | // Initial value larger than any possible distance on the unit sphere. 205 | S1Angle minDistance = S1Angle.radians(10); 206 | int minIndex = -1; 207 | 208 | // Find the line segment in the polyline that is closest to the point given. 209 | for (int i = 0; i < numVertices() - 1; ++i) { 210 | S1Angle distanceToSegment = S2EdgeUtil.getDistance(point, vertex(i), vertex(i + 1)); 211 | if (distanceToSegment.lessThan(minDistance)) { 212 | minDistance = distanceToSegment; 213 | minIndex = i; 214 | } 215 | } 216 | return minIndex; 217 | } 218 | 219 | /** 220 | * Given a point p and the index of the start point of an edge of this polyline, 221 | * returns the point on that edge that is closest to p. 222 | *#/ 223 | public S2Point projectToEdge(S2Point point, int index) { 224 | Preconditions.checkState(numVertices() > 0, "Empty polyline"); 225 | Preconditions.checkState(numVertices() == 1 || index < numVertices() - 1, "Invalid edge index"); 226 | if (numVertices() == 1) { 227 | // If there is only one vertex, it is always closest to any given point. 228 | return vertex(0); 229 | } 230 | return S2EdgeUtil.getClosestPoint(point, vertex(index), vertex(index + 1)); 231 | } 232 | 233 | @Override 234 | public boolean equals(Object that) { 235 | if (!(that instanceof S2Polyline)) { 236 | return false; 237 | } 238 | 239 | S2Polyline thatPolygon = (S2Polyline) that; 240 | if (numVertices != thatPolygon.numVertices) { 241 | return false; 242 | } 243 | 244 | for (int i = 0; i < vertices.length; i++) { 245 | if (!vertices[i].equals(thatPolygon.vertices[i])) { 246 | return false; 247 | } 248 | } 249 | return true; 250 | } 251 | 252 | @Override 253 | public int hashCode() { 254 | return Objects.hashCode(numVertices, Arrays.deepHashCode(vertices)); 255 | } 256 | */ 257 | } 258 | -------------------------------------------------------------------------------- /S2LatLng.php: -------------------------------------------------------------------------------- 1 | get(2), 47 | sqrt($p->get(0) * $p->get(0) + $p->get(1) * $p->get(1)) 48 | ) 49 | ); 50 | } 51 | 52 | public static function longitude(S2Point $p) { 53 | // Note that atan2(0, 0) is defined to be zero. 54 | return S1Angle::sradians(atan2($p->get(1), $p->get(0))); 55 | } 56 | 57 | /** 58 | * This is internal to avoid ambiguity about which units are expected. 59 | * @param double|S1Angle $latRadians 60 | * @param double|S1Angle $lngRadians 61 | */ 62 | public function __construct($latRadians = null, $lngRadians = null) { 63 | if ($latRadians instanceof S1Angle && $lngRadians instanceof S1Angle) { 64 | $this->latRadians = $latRadians->radians(); 65 | $this->lngRadians = $lngRadians->radians(); 66 | } else if ($lngRadians === null && $latRadians instanceof S2Point) { 67 | $this->latRadians = atan2($latRadians->z, sqrt($latRadians->x * $latRadians->x + $latRadians->y * $latRadians->y)); 68 | $this->lngRadians = atan2($latRadians->y, $latRadians->x); 69 | } else if ($latRadians === null && $lngRadians === null) { 70 | $this->latRadians = 0; 71 | $this->lngRadians = 0; 72 | } else { 73 | $this->latRadians = $latRadians; 74 | $this->lngRadians = $lngRadians; 75 | } 76 | } 77 | 78 | /** Returns the latitude of this point as a new S1Angle. */ 79 | public function lat() { 80 | return S1Angle::sradians($this->latRadians); 81 | } 82 | 83 | /** Returns the latitude of this point as radians. */ 84 | public function latRadians() { 85 | return $this->latRadians; 86 | } 87 | 88 | /** Returns the latitude of this point as degrees. */ 89 | public function latDegrees() { 90 | return 180.0 / M_PI * $this->latRadians; 91 | } 92 | 93 | /** Returns the longitude of this point as a new S1Angle. */ 94 | public function lng() { 95 | return S1Angle::sradians($this->lngRadians); 96 | } 97 | 98 | /** Returns the longitude of this point as radians. */ 99 | public function lngRadians() { 100 | return $this->lngRadians; 101 | } 102 | 103 | /** Returns the longitude of this point as degrees. */ 104 | public function lngDegrees() { 105 | return 180.0 / M_PI * $this->lngRadians; 106 | } 107 | 108 | /** 109 | * Return true if the latitude is between -90 and 90 degrees inclusive and the 110 | * longitude is between -180 and 180 degrees inclusive. 111 | *#/ 112 | * public boolean isValid() { 113 | * return Math.abs(lat().radians()) <= S2.M_PI_2 && Math.abs(lng().radians()) <= S2.M_PI; 114 | * } 115 | * 116 | * /** 117 | * Returns a new S2LatLng based on this instance for which {@link #isValid()} 118 | * will be {@code true}. 119 | * 123 | *

If the current point is valid then the returned point will have the same 124 | * coordinates. 125 | *#/ 126 | * public S2LatLng normalized() { 127 | * // drem(x, 2 * S2.M_PI) reduces its argument to the range 128 | * // [-S2.M_PI, S2.M_PI] inclusive, which is what we want here. 129 | * return new S2LatLng(Math.max(-S2.M_PI_2, Math.min(S2.M_PI_2, lat().radians())), 130 | * Math.IEEEremainder(lng().radians(), 2 * S2.M_PI)); 131 | * } 132 | * 133 | * // Clamps the latitude to the range [-90, 90] degrees, and adds or subtracts 134 | * // a multiple of 360 degrees to the longitude if necessary to reduce it to 135 | * // the range [-180, 180]. 136 | * 137 | * /** Convert an S2LatLng to the equivalent unit-length vector (S2Point). */ 138 | public function toPoint() { 139 | $phi = $this->lat()->radians(); 140 | $theta = $this->lng()->radians(); 141 | $cosphi = cos($phi); 142 | return new S2Point(cos($theta) * $cosphi, sin($theta) * $cosphi, sin($phi)); 143 | } 144 | 145 | /** 146 | * Return the distance (measured along the surface of the sphere) to the given 147 | * point. 148 | *#/ 149 | * public S1Angle getDistance(final S2LatLng o) { 150 | * // This implements the Haversine formula, which is numerically stable for 151 | * // small distances but only gets about 8 digits of precision for very large 152 | * // distances (e.g. antipodal points). Note that 8 digits is still accurate 153 | * // to within about 10cm for a sphere the size of the Earth. 154 | * // 155 | * // This could be fixed with another sin() and cos() below, but at that point 156 | * // you might as well just convert both arguments to S2Points and compute the 157 | * // distance that way (which gives about 15 digits of accuracy for all 158 | * // distances). 159 | * 160 | * double lat1 = lat().radians(); 161 | * double lat2 = o.lat().radians(); 162 | * double lng1 = lng().radians(); 163 | * double lng2 = o.lng().radians(); 164 | * double dlat = Math.sin(0.5 * (lat2 - lat1)); 165 | * double dlng = Math.sin(0.5 * (lng2 - lng1)); 166 | * double x = dlat * dlat + dlng * dlng * Math.cos(lat1) * Math.cos(lat2); 167 | * return S1Angle.radians(2 * Math.atan2(Math.sqrt(x), Math.sqrt(Math.max(0.0, 1.0 - x)))); 168 | * // Return the distance (measured along the surface of the sphere) to the 169 | * // given S2LatLng. This is mathematically equivalent to: 170 | * // 171 | * // S1Angle::FromRadians(ToPoint().Angle(o.ToPoint()) 172 | * // 173 | * // but this implementation is slightly more efficient. 174 | * } 175 | * 176 | * /** 177 | * Returns the surface distance to the given point assuming a constant radius. 178 | *#/ 179 | * public double getDistance(final S2LatLng o, double radius) { 180 | * // TODO(dbeaumont): Maybe check that radius >= 0 ? 181 | * return getDistance(o).radians() * radius; 182 | * } 183 | * 184 | * /** 185 | * Returns the surface distance to the given point assuming the default Earth 186 | * radius of {@link #EARTH_RADIUS_METERS}. 187 | *#/ 188 | * public double getEarthDistance(final S2LatLng o) { 189 | * return getDistance(o, EARTH_RADIUS_METERS); 190 | * } 191 | * 192 | * /** 193 | * Adds the given point to this point. 194 | * Note that there is no guarantee that the new point will be valid. 195 | *#/ 196 | * public S2LatLng add(final S2LatLng o) { 197 | * return new S2LatLng(latRadians + o.latRadians, lngRadians + o.lngRadians); 198 | * } 199 | * 200 | * /** 201 | * Subtracts the given point from this point. 202 | * Note that there is no guarantee that the new point will be valid. 203 | *#/ 204 | * public S2LatLng sub(final S2LatLng o) { 205 | * return new S2LatLng(latRadians - o.latRadians, lngRadians - o.lngRadians); 206 | * } 207 | * 208 | * /** 209 | * Scales this point by the given scaling factor. 210 | * Note that there is no guarantee that the new point will be valid. 211 | */ 212 | public function mul($m) { 213 | // TODO(dbeaumont): Maybe check that m >= 0 ? 214 | return new S2LatLng($this->latRadians * $m, $this->lngRadians * $m); 215 | } 216 | 217 | /* 218 | @Override 219 | public boolean equals(Object that) { 220 | if (that instanceof S2LatLng) { 221 | S2LatLng o = (S2LatLng) that; 222 | return (latRadians == o.latRadians) && (lngRadians == o.lngRadians); 223 | } 224 | return false; 225 | } 226 | 227 | @Override 228 | public int hashCode() { 229 | long value = 17; 230 | value += 37 * value + Double.doubleToLongBits(latRadians); 231 | value += 37 * value + Double.doubleToLongBits(lngRadians); 232 | return (int) (value ^ (value >>> 32)); 233 | } 234 | 235 | /** 236 | * Returns true if both the latitude and longitude of the given point are 237 | * within {@code maxError} radians of this point. 238 | *#/ 239 | public boolean approxEquals(S2LatLng o, double maxError) { 240 | return (Math.abs(latRadians - o.latRadians) < maxError) 241 | && (Math.abs(lngRadians - o.lngRadians) < maxError); 242 | } 243 | 244 | /#** 245 | * Returns true if the given point is within {@code 1e-9} radians of this 246 | * point. This corresponds to a distance of less than {@code 1cm} at the 247 | * surface of the Earth. 248 | *#/ 249 | public boolean approxEquals(S2LatLng o) { 250 | return approxEquals(o, 1e-9); 251 | } 252 | */ 253 | public function __toString() { 254 | return "(" . $this->latRadians . ", " . $this->lngRadians . ")"; 255 | } 256 | 257 | public function toStringDegrees() { 258 | return "(" . $this->latDegrees() . ", " . $this->lngDegrees() . ")"; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /S2Projections.php: -------------------------------------------------------------------------------- 1 | = 0) { 175 | return (1 / 3.) * ((1 + $s) * (1 + $s) - 1); 176 | } else { 177 | return (1 / 3.) * (1 - (1 - $s) * (1 - $s)); 178 | } 179 | default: 180 | throw new IllegalStateException("Invalid value for S2_PROJECTION"); 181 | } 182 | } 183 | 184 | public static function uvToST($u) { 185 | switch (self::S2_PROJECTION) { 186 | case self::S2_LINEAR_PROJECTION: 187 | return $u; 188 | 189 | case self::S2_TAN_PROJECTION: 190 | return (4 * S2::M_1_PI) * atan($u); 191 | 192 | case self::S2_QUADRATIC_PROJECTION: 193 | if ($u >= 0) { 194 | return sqrt(1 + 3 * $u) - 1; 195 | } else { 196 | return 1 - sqrt(1 - 3 * $u); 197 | } 198 | default: 199 | throw new IllegalStateException("Invalid value for S2_PROJECTION"); 200 | } 201 | } 202 | 203 | /** 204 | * Convert (face, u, v) coordinates to a direction vector (not necessarily 205 | * unit length). 206 | */ 207 | public static function faceUvToXyz($face, $u, $v) { 208 | switch ($face) { 209 | case 0: 210 | return new S2Point(1, $u, $v); 211 | 212 | case 1: 213 | return new S2Point(-$u, 1, $v); 214 | 215 | case 2: 216 | return new S2Point(-$u, -$v, 1); 217 | 218 | case 3: 219 | return new S2Point(-1, -$v, -$u); 220 | 221 | case 4: 222 | return new S2Point($v, -1, -$u); 223 | 224 | default: 225 | return new S2Point($v, $u, -1); 226 | } 227 | } 228 | 229 | public static function validFaceXyzToUv($face, S2Point $p) { 230 | // assert (p.dotProd(faceUvToXyz(face, 0, 0)) > 0); 231 | switch ($face) { 232 | case 0: 233 | $pu = $p->y / $p->x; 234 | $pv = $p->z / $p->x; 235 | break; 236 | 237 | case 1: 238 | $pu = -$p->x / $p->y; 239 | $pv = $p->z / $p->y; 240 | break; 241 | 242 | case 2: 243 | $pu = -$p->x / $p->z; 244 | $pv = -$p->y / $p->z; 245 | break; 246 | 247 | case 3: 248 | $pu = $p->z / $p->x; 249 | $pv = $p->y / $p->x; 250 | break; 251 | 252 | case 4: 253 | $pu = $p->z / $p->y; 254 | $pv = -$p->x / $p->y; 255 | break; 256 | 257 | default: 258 | $pu = -$p->y / $p->z; 259 | $pv = -$p->x / $p->z; 260 | break; 261 | } 262 | return new R2Vector($pu, $pv); 263 | } 264 | 265 | public static function xyzToFace(S2Point $p) { 266 | $face = $p->largestAbsComponent(); 267 | if ($p->get($face) < 0) { 268 | $face += 3; 269 | } 270 | return $face; 271 | } 272 | 273 | /* 274 | public static R2Vector faceXyzToUv(int face, S2Point p) { 275 | if (face < 3) { 276 | if (p.get(face) <= 0) { 277 | return null; 278 | } 279 | } else { 280 | if (p.get(face - 3) >= 0) { 281 | return null; 282 | } 283 | } 284 | return validFaceXyzToUv(face, p); 285 | } 286 | 287 | public static S2Point getUNorm(int face, double u) { 288 | switch (face) { 289 | case 0: 290 | return new S2Point(u, -1, 0); 291 | case 1: 292 | return new S2Point(1, u, 0); 293 | case 2: 294 | return new S2Point(1, 0, u); 295 | case 3: 296 | return new S2Point(-u, 0, 1); 297 | case 4: 298 | return new S2Point(0, -u, 1); 299 | default: 300 | return new S2Point(0, -1, -u); 301 | } 302 | } 303 | 304 | public static S2Point getVNorm(int face, double v) { 305 | switch (face) { 306 | case 0: 307 | return new S2Point(-v, 0, 1); 308 | case 1: 309 | return new S2Point(0, -v, 1); 310 | case 2: 311 | return new S2Point(0, -1, -v); 312 | case 3: 313 | return new S2Point(v, -1, 0); 314 | case 4: 315 | return new S2Point(1, v, 0); 316 | default: 317 | return new S2Point(1, 0, v); 318 | } 319 | } 320 | 321 | public static S2Point getNorm(int face) { 322 | return faceUvToXyz(face, 0, 0); 323 | } 324 | */ 325 | public static function getUAxis($face) { 326 | switch ($face) { 327 | case 0: 328 | return new S2Point(0, 1, 0); 329 | 330 | case 1: 331 | return new S2Point(-1, 0, 0); 332 | 333 | case 2: 334 | return new S2Point(-1, 0, 0); 335 | 336 | case 3: 337 | return new S2Point(0, 0, -1); 338 | 339 | case 4: 340 | return new S2Point(0, 0, -1); 341 | 342 | default: 343 | return new S2Point(0, 1, 0); 344 | } 345 | } 346 | 347 | public static function getVAxis($face) { 348 | switch ($face) { 349 | case 0: 350 | return new S2Point(0, 0, 1); 351 | 352 | case 1: 353 | return new S2Point(0, 0, 1); 354 | 355 | case 2: 356 | return new S2Point(0, -1, 0); 357 | 358 | case 3: 359 | return new S2Point(0, -1, 0); 360 | 361 | case 4: 362 | return new S2Point(1, 0, 0); 363 | 364 | default: 365 | return new S2Point(1, 0, 0); 366 | } 367 | } 368 | } 369 | -------------------------------------------------------------------------------- /S1Interval.php: -------------------------------------------------------------------------------- 1 | lo = $lo->lo; 14 | $this->hi = $lo->hi; 15 | } else { 16 | $newLo = $lo; 17 | $newHi = $hi; 18 | if (!$checked) { 19 | if ($lo == -S2::M_PI && $hi != S2::M_PI) { 20 | $newLo = S2::M_PI; 21 | } 22 | if ($hi == -S2::M_PI && $lo != S2::M_PI) { 23 | $newHi = S2::M_PI; 24 | } 25 | } 26 | $this->lo = $newLo; 27 | $this->hi = $newHi; 28 | } 29 | } 30 | 31 | public static function emptya() { 32 | return new S1Interval(S2::M_PI, -S2::M_PI, true); 33 | } 34 | 35 | public static function full() { 36 | return new S1Interval(-S2::M_PI, S2::M_PI, true); 37 | } 38 | 39 | /** Convenience method to construct an interval containing a single point. *#/ 40 | * public static S1Interval fromPoint(double p) { 41 | * if (p == -S2.M_PI) { 42 | * p = S2.M_PI; 43 | * } 44 | * return new S1Interval(p, p, true); 45 | * } 46 | * 47 | * /** 48 | * Convenience method to construct the minimal interval containing the two 49 | * given points. This is equivalent to starting with an empty interval and 50 | * calling AddPoint() twice, but it is more efficient. 51 | */ 52 | public static function fromPointPair($p1, $p2) { 53 | // assert (Math.abs(p1) <= S2.M_PI && Math.abs(p2) <= S2.M_PI); 54 | if ($p1 == -S2::M_PI) { 55 | $p1 = S2::M_PI; 56 | } 57 | if ($p2 == -S2::M_PI) { 58 | $p2 = S2::M_PI; 59 | } 60 | if (self::positiveDistance($p1, $p2) <= S2::M_PI) { 61 | return new S1Interval($p1, $p2, true); 62 | } else { 63 | return new S1Interval($p2, $p1, true); 64 | } 65 | } 66 | 67 | public function lo() { 68 | return $this->lo; 69 | } 70 | 71 | public function hi() { 72 | return $this->hi; 73 | } 74 | 75 | /** 76 | * An interval is valid if neither bound exceeds Pi in absolute value, and the 77 | * value -Pi appears only in the Empty() and Full() intervals. 78 | *#/ 79 | * public boolean isValid() { 80 | * return (Math.abs(lo()) <= S2.M_PI && Math.abs(hi()) <= S2.M_PI 81 | * && !(lo() == -S2.M_PI && hi() != S2.M_PI) && !(hi() == -S2.M_PI && lo() != S2.M_PI)); 82 | * } 83 | * 84 | * /** Return true if the interval contains all points on the unit circle. *#/ 85 | * public boolean isFull() { 86 | * return hi() - lo() == 2 * S2.M_PI; 87 | * } 88 | * 89 | * 90 | * /** Return true if the interval is empty, i.e. it contains no points. */ 91 | public function isEmpty() { 92 | return $this->lo() - $this->hi() == 2 * S2::M_PI; 93 | } 94 | 95 | /* Return true if lo() > hi(). (This is true for empty intervals.) */ 96 | public function isInverted() { 97 | return $this->lo() > $this->hi(); 98 | } 99 | 100 | /** 101 | * Return the midpoint of the interval. For full and empty intervals, the 102 | * result is arbitrary. 103 | */ 104 | public function getCenter() { 105 | $center = 0.5 * ($this->lo() + $this->hi()); 106 | if (!$this->isInverted()) { 107 | return $center; 108 | } 109 | // Return the center in the range (-Pi, Pi]. 110 | return ($center <= 0) ? ($center + S2::M_PI) : ($center - S2::M_PI); 111 | } 112 | 113 | /** 114 | * Return the length of the interval. The length of an empty interval is 115 | * negative. 116 | */ 117 | public function getLength() { 118 | $length = $this->hi() - $this->lo(); 119 | if ($length >= 0) { 120 | return $length; 121 | } 122 | $length += 2 * S2::M_PI; 123 | // Empty intervals have a negative length. 124 | return ($length > 0) ? $length : -1; 125 | } 126 | 127 | /** 128 | * Return the complement of the interior of the interval. An interval and its 129 | * complement have the same boundary but do not share any interior values. The 130 | * complement operator is not a bijection, since the complement of a singleton 131 | * interval (containing a single value) is the same as the complement of an 132 | * empty interval. 133 | *#/ 134 | * public S1Interval complement() { 135 | * if (lo() == hi()) { 136 | * return full(); // Singleton. 137 | * } 138 | * return new S1Interval(hi(), lo(), true); // Handles 139 | * // empty and 140 | * // full. 141 | * } 142 | * 143 | * /** Return true if the interval (which is closed) contains the point 'p'. *#/ 144 | * public boolean contains(double p) { 145 | * // Works for empty, full, and singleton intervals. 146 | * // assert (Math.abs(p) <= S2.M_PI); 147 | * if (p == -S2.M_PI) { 148 | * p = S2.M_PI; 149 | * } 150 | * return fastContains(p); 151 | * } 152 | * 153 | * /** 154 | * Return true if the interval (which is closed) contains the point 'p'. Skips 155 | * the normalization of 'p' from -Pi to Pi. 156 | * 157 | *#/ 158 | * public boolean fastContains(double p) { 159 | * if (isInverted()) { 160 | * return (p >= lo() || p <= hi()) && !isEmpty(); 161 | * } else { 162 | * return p >= lo() && p <= hi(); 163 | * } 164 | * } 165 | * 166 | * /** Return true if the interior of the interval contains the point 'p'. *#/ 167 | * public boolean interiorContains(double p) { 168 | * // Works for empty, full, and singleton intervals. 169 | * // assert (Math.abs(p) <= S2.M_PI); 170 | * if (p == -S2.M_PI) { 171 | * p = S2.M_PI; 172 | * } 173 | * 174 | * if (isInverted()) { 175 | * return p > lo() || p < hi(); 176 | * } else { 177 | * return (p > lo() && p < hi()) || isFull(); 178 | * } 179 | * } 180 | * 181 | * /** 182 | * Return true if the interval contains the given interval 'y'. Works for 183 | * empty, full, and singleton intervals. 184 | *#/ 185 | * public boolean contains(final S1Interval y) { 186 | * // It might be helpful to compare the structure of these tests to 187 | * // the simpler Contains(double) method above. 188 | * 189 | * if (isInverted()) { 190 | * if (y.isInverted()) { 191 | * return y.lo() >= lo() && y.hi() <= hi(); 192 | * } 193 | * return (y.lo() >= lo() || y.hi() <= hi()) && !isEmpty(); 194 | * } else { 195 | * if (y.isInverted()) { 196 | * return isFull() || y.isEmpty(); 197 | * } 198 | * return y.lo() >= lo() && y.hi() <= hi(); 199 | * } 200 | * } 201 | * 202 | * /** 203 | * Returns true if the interior of this interval contains the entire interval 204 | * 'y'. Note that x.InteriorContains(x) is true only when x is the empty or 205 | * full interval, and x.InteriorContains(S1Interval(p,p)) is equivalent to 206 | * x.InteriorContains(p). 207 | *#/ 208 | * public boolean interiorContains(final S1Interval y) { 209 | * if (isInverted()) { 210 | * if (!y.isInverted()) { 211 | * return y.lo() > lo() || y.hi() < hi(); 212 | * } 213 | * return (y.lo() > lo() && y.hi() < hi()) || y.isEmpty(); 214 | * } else { 215 | * if (y.isInverted()) { 216 | * return isFull() || y.isEmpty(); 217 | * } 218 | * return (y.lo() > lo() && y.hi() < hi()) || isFull(); 219 | * } 220 | * } 221 | * 222 | * /** 223 | * Return true if the two intervals contain any points in common. Note that 224 | * the point +/-Pi has two representations, so the intervals [-Pi,-3] and 225 | * [2,Pi] intersect, for example. 226 | */ 227 | public function intersects(S1Interval $y) { 228 | if ($this->isEmpty() || $y->isEmpty()) { 229 | return false; 230 | } 231 | if ($this->isInverted()) { 232 | // Every non-empty inverted interval contains Pi. 233 | return $y->isInverted() || $y->lo() <= $this->hi() || $y->hi() >= $this->lo(); 234 | } else { 235 | if ($y->isInverted()) { 236 | return $y->lo() <= $this->hi() || $y->hi() >= $this->lo(); 237 | } 238 | return $y->lo() <= $this->hi() && $y->hi() >= $this->lo(); 239 | } 240 | } 241 | 242 | /** 243 | * Return true if the interior of this interval contains any point of the 244 | * interval 'y' (including its boundary). Works for empty, full, and singleton 245 | * intervals. 246 | *#/ 247 | * public boolean interiorIntersects(final S1Interval y) { 248 | * if (isEmpty() || y.isEmpty() || lo() == hi()) { 249 | * return false; 250 | * } 251 | * if (isInverted()) { 252 | * return y.isInverted() || y.lo() < hi() || y.hi() > lo(); 253 | * } else { 254 | * if (y.isInverted()) { 255 | * return y.lo() < hi() || y.hi() > lo(); 256 | * } 257 | * return (y.lo() < hi() && y.hi() > lo()) || isFull(); 258 | * } 259 | * } 260 | * 261 | * /** 262 | * Expand the interval by the minimum amount necessary so that it contains the 263 | * given point "p" (an angle in the range [-Pi, Pi]). 264 | *#/ 265 | * public S1Interval addPoint(double p) { 266 | * // assert (Math.abs(p) <= S2.M_PI); 267 | * if (p == -S2.M_PI) { 268 | * p = S2.M_PI; 269 | * } 270 | * 271 | * if (fastContains(p)) { 272 | * return new S1Interval(this); 273 | * } 274 | * 275 | * if (isEmpty()) { 276 | * return S1Interval.fromPoint(p); 277 | * } else { 278 | * // Compute distance from p to each endpoint. 279 | * double dlo = positiveDistance(p, lo()); 280 | * double dhi = positiveDistance(hi(), p); 281 | * if (dlo < dhi) { 282 | * return new S1Interval(p, hi()); 283 | * } else { 284 | * return new S1Interval(lo(), p); 285 | * } 286 | * // Adding a point can never turn a non-full interval into a full one. 287 | * } 288 | * } 289 | * 290 | * /** 291 | * Return an interval that contains all points within a distance "radius" of 292 | * a point in this interval. Note that the expansion of an empty interval is 293 | * always empty. The radius must be non-negative. 294 | */ 295 | public function expanded($radius) { 296 | // assert (radius >= 0); 297 | if ($this->isEmpty()) { 298 | return this; 299 | } 300 | 301 | // Check whether this interval will be full after expansion, allowing 302 | // for a 1-bit rounding error when computing each endpoint. 303 | if ($this->getLength() + 2 * $radius >= 2 * S2::M_PI - 1e-15) { 304 | return self::full(); 305 | } 306 | 307 | // NOTE(dbeaumont): Should this remainder be 2 * M_PI or just M_PI ?? 308 | // double lo = Math.IEEEremainder(lo() - radius, 2 * S2.M_PI); 309 | $lo = S2::IEEEremainder($this->lo() - $radius, 2 * S2::M_PI); 310 | $hi = S2::IEEEremainder($this->hi() + $radius, 2 * S2::M_PI); 311 | if ($lo == -S2::M_PI) { 312 | $lo = S2::M_PI; 313 | } 314 | return new S1Interval($lo, $hi); 315 | } 316 | 317 | /** 318 | * Return the smallest interval that contains this interval and the given 319 | * interval "y". 320 | *#/ 321 | * public S1Interval union(final S1Interval y) { 322 | * // The y.is_full() case is handled correctly in all cases by the code 323 | * // below, but can follow three separate code paths depending on whether 324 | * // this interval is inverted, is non-inverted but contains Pi, or neither. 325 | * 326 | * if (y.isEmpty()) { 327 | * return this; 328 | * } 329 | * if (fastContains(y.lo())) { 330 | * if (fastContains(y.hi())) { 331 | * // Either this interval contains y, or the union of the two 332 | * // intervals is the Full() interval. 333 | * if (contains(y)) { 334 | * return this; // is_full() code path 335 | * } 336 | * return full(); 337 | * } 338 | * return new S1Interval(lo(), y.hi(), true); 339 | * } 340 | * if (fastContains(y.hi())) { 341 | * return new S1Interval(y.lo(), hi(), true); 342 | * } 343 | * 344 | * // This interval contains neither endpoint of y. This means that either y 345 | * // contains all of this interval, or the two intervals are disjoint. 346 | * if (isEmpty() || y.fastContains(lo())) { 347 | * return y; 348 | * } 349 | * 350 | * // Check which pair of endpoints are closer together. 351 | * double dlo = positiveDistance(y.hi(), lo()); 352 | * double dhi = positiveDistance(hi(), y.lo()); 353 | * if (dlo < dhi) { 354 | * return new S1Interval(y.lo(), hi(), true); 355 | * } else { 356 | * return new S1Interval(lo(), y.hi(), true); 357 | * } 358 | * } 359 | * 360 | * /** 361 | * Return the smallest interval that contains the intersection of this 362 | * interval with "y". Note that the region of intersection may consist of two 363 | * disjoint intervals. 364 | *#/ 365 | * public S1Interval intersection(final S1Interval y) { 366 | * // The y.is_full() case is handled correctly in all cases by the code 367 | * // below, but can follow three separate code paths depending on whether 368 | * // this interval is inverted, is non-inverted but contains Pi, or neither. 369 | * 370 | * if (y.isEmpty()) { 371 | * return empty(); 372 | * } 373 | * if (fastContains(y.lo())) { 374 | * if (fastContains(y.hi())) { 375 | * // Either this interval contains y, or the region of intersection 376 | * // consists of two disjoint subintervals. In either case, we want 377 | * // to return the shorter of the two original intervals. 378 | * if (y.getLength() < getLength()) { 379 | * return y; // is_full() code path 380 | * } 381 | * return this; 382 | * } 383 | * return new S1Interval(y.lo(), hi(), true); 384 | * } 385 | * if (fastContains(y.hi())) { 386 | * return new S1Interval(lo(), y.hi(), true); 387 | * } 388 | * 389 | * // This interval contains neither endpoint of y. This means that either y 390 | * // contains all of this interval, or the two intervals are disjoint. 391 | * 392 | * if (y.fastContains(lo())) { 393 | * return this; // is_empty() okay here 394 | * } 395 | * // assert (!intersects(y)); 396 | * return empty(); 397 | * } 398 | * 399 | * /** 400 | * Return true if the length of the symmetric difference between the two 401 | * intervals is at most the given tolerance. 402 | *#/ 403 | * public boolean approxEquals(final S1Interval y, double maxError) { 404 | * if (isEmpty()) { 405 | * return y.getLength() <= maxError; 406 | * } 407 | * if (y.isEmpty()) { 408 | * return getLength() <= maxError; 409 | * } 410 | * return (Math.abs(Math.IEEEremainder(y.lo() - lo(), 2 * S2.M_PI)) 411 | * + Math.abs(Math.IEEEremainder(y.hi() - hi(), 2 * S2.M_PI))) <= maxError; 412 | * } 413 | * 414 | * public boolean approxEquals(final S1Interval y) { 415 | * return approxEquals(y, 1e-9); 416 | * } 417 | * 418 | * /** 419 | * Return true if two intervals contains the same set of points. 420 | *#/ 421 | * @Override 422 | * public boolean equals(Object that) { 423 | * if (that instanceof S1Interval) { 424 | * S1Interval thatInterval = (S1Interval) that; 425 | * return lo() == thatInterval.lo() && hi() == thatInterval.hi(); 426 | * } 427 | * return false; 428 | * } 429 | * 430 | * @Override 431 | * public int hashCode() { 432 | * long value = 17; 433 | * value = 37 * value + Double.doubleToLongBits(lo()); 434 | * value = 37 * value + Double.doubleToLongBits(hi()); 435 | * return (int) ((value >>> 32) ^ value); 436 | * } 437 | * 438 | * @Override 439 | * public String toString() { 440 | * return "[" + this.lo() + ", " + this.hi() + "]"; 441 | * } 442 | * 443 | * /** 444 | * Compute the distance from "a" to "b" in the range [0, 2*Pi). This is 445 | * equivalent to (drem(b - a - S2.M_PI, 2 * S2.M_PI) + S2.M_PI), except that 446 | * it is more numerically stable (it does not lose precision for very small 447 | * positive distances). 448 | */ 449 | public static function positiveDistance($a, $b) { 450 | $d = $b - $a; 451 | if ($d >= 0) { 452 | return $d; 453 | } 454 | // We want to ensure that if b == Pi and a == (-Pi + eps), 455 | // the return result is approximately 2*Pi and not zero. 456 | return ($b + S2::M_PI) - ($a - S2::M_PI); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /S2Cap.php: -------------------------------------------------------------------------------- 1 | axis = new S2Point(); 21 | $this->height = 0; 22 | } else { 23 | $this->axis = $axis; 24 | $this->height = $height; 25 | } 26 | } 27 | 28 | /** 29 | * Create a cap given its axis and the cap height, i.e. the maximum projected 30 | * distance along the cap axis from the cap center. 'axis' should be a 31 | * unit-length vector. 32 | */ 33 | // public static S2Cap fromAxisHeight(S2Point axis, double height) { 34 | // assert (S2.isUnitLength(axis)); 35 | // return new S2Cap(axis, height); 36 | // } 37 | 38 | /** 39 | * Create a cap given its axis and the cap opening angle, i.e. maximum angle 40 | * between the axis and a point on the cap. 'axis' should be a unit-length 41 | * vector, and 'angle' should be between 0 and 180 degrees. 42 | */ 43 | public static function fromAxisAngle(S2Point $axis, S1Angle $angle) { 44 | // The height of the cap can be computed as 1-cos(angle), but this isn't 45 | // very accurate for angles close to zero (where cos(angle) is almost 1). 46 | // Computing it as 2*(sin(angle/2)**2) gives much better precision. 47 | 48 | // assert (S2.isUnitLength(axis)); 49 | $d = sin(0.5 * $angle->radians()); 50 | return new S2Cap($axis, 2 * $d * $d); 51 | } 52 | 53 | /** 54 | * Create a cap given its axis and its area in steradians. 'axis' should be a 55 | * unit-length vector, and 'area' should be between 0 and 4 * M_PI. 56 | */ 57 | // public static S2Cap fromAxisArea(S2Point axis, double area) { 58 | // assert (S2.isUnitLength(axis)); 59 | // return new S2Cap(axis, area / (2 * S2.M_PI)); 60 | // } 61 | 62 | /** Return an empty cap, i.e. a cap that contains no points. */ 63 | public static function sempty() { 64 | return new S2Cap(new S2Point(1, 0, 0), -1); 65 | } 66 | 67 | /** Return a full cap, i.e. a cap that contains all points. *#/ 68 | * public static S2Cap full() { 69 | * return new S2Cap(new S2Point(1, 0, 0), 2); 70 | * } 71 | 72 | */ 73 | // Accessor methods. 74 | public function axis() { 75 | return $this->axis; 76 | } 77 | 78 | public function height() { 79 | return $this->height; 80 | } 81 | 82 | public function area() { 83 | return 2 * S2::M_PI * max(0.0, $this->height); 84 | } 85 | 86 | /** 87 | * Return the cap opening angle in radians, or a negative number for empty 88 | * caps. 89 | */ 90 | public function angle() { 91 | // This could also be computed as acos(1 - height_), but the following 92 | // formula is much more accurate when the cap height is small. It 93 | // follows from the relationship h = 1 - cos(theta) = 2 sin^2(theta/2). 94 | if ($this->isEmpty()) { 95 | return S1Angle::sradians(-1); 96 | } 97 | return S1Angle::sradians(2 * asin(sqrt(0.5 * $this->height))); 98 | } 99 | 100 | /** 101 | * We allow negative heights (to represent empty caps) but not heights greater 102 | * than 2. 103 | *#/ 104 | * public boolean isValid() { 105 | * return S2.isUnitLength(axis) && height <= 2; 106 | * } 107 | * 108 | * /** Return true if the cap is empty, i.e. it contains no points. */ 109 | public function isEmpty() { 110 | return $this->height < 0; 111 | } 112 | 113 | /** Return true if the cap is full, i.e. it contains all points. *#/ 114 | * public boolean isFull() { 115 | * return height >= 2; 116 | * } 117 | * 118 | * /** 119 | * Return the complement of the interior of the cap. A cap and its complement 120 | * have the same boundary but do not share any interior points. The complement 121 | * operator is not a bijection, since the complement of a singleton cap 122 | * (containing a single point) is the same as the complement of an empty cap. 123 | *#/ 124 | * public S2Cap complement() { 125 | * // The complement of a full cap is an empty cap, not a singleton. 126 | * // Also make sure that the complement of an empty cap has height 2. 127 | * double cHeight = isFull() ? -1 : 2 - Math.max(height, 0.0); 128 | * return S2Cap.fromAxisHeight(S2Point.neg(axis), cHeight); 129 | * } 130 | * 131 | * /** 132 | * Return true if and only if this cap contains the given other cap (in a set 133 | * containment sense, e.g. every cap contains the empty cap). 134 | *#/ 135 | * public boolean contains(S2Cap other) { 136 | * if (isFull() || other.isEmpty()) { 137 | * return true; 138 | * } 139 | * return angle().radians() >= axis.angle(other.axis) 140 | * + other.angle().radians(); 141 | * } 142 | * 143 | * /** 144 | * Return true if and only if the interior of this cap intersects the given 145 | * other cap. (This relationship is not symmetric, since only the interior of 146 | * this cap is used.) 147 | *#/ 148 | * public boolean interiorIntersects(S2Cap other) { 149 | * // Interior(X) intersects Y if and only if Complement(Interior(X)) 150 | * // does not contain Y. 151 | * return !complement().contains(other); 152 | * } 153 | * 154 | * /** 155 | * Return true if and only if the given point is contained in the interior of 156 | * the region (i.e. the region excluding its boundary). 'p' should be a 157 | * unit-length vector. 158 | *#/ 159 | * public boolean interiorContains(S2Point p) { 160 | * // assert (S2.isUnitLength(p)); 161 | * return isFull() || S2Point.sub(axis, p).norm2() < 2 * height; 162 | * } 163 | * 164 | * /** 165 | * Increase the cap height if necessary to include the given point. If the cap 166 | * is empty the axis is set to the given point, but otherwise it is left 167 | * unchanged. 'p' should be a unit-length vector. 168 | */ 169 | public function addPoint(S2Point $p) { 170 | // Compute the squared chord length, then convert it into a height. 171 | // assert (S2.isUnitLength(p)); 172 | if ($this->isEmpty()) { 173 | return new S2Cap($p, 0); 174 | } else { 175 | // To make sure that the resulting cap actually includes this point, 176 | // we need to round up the distance calculation. That is, after 177 | // calling cap.AddPoint(p), cap.Contains(p) should be true. 178 | $dist2 = S2Point::sub($this->axis, $p)->norm2(); 179 | $newHeight = max($this->height, self::ROUND_UP * 0.5 * $dist2); 180 | return new S2Cap($this->axis, $newHeight); 181 | } 182 | } 183 | 184 | /* 185 | // Increase the cap height if necessary to include "other". If the current 186 | // cap is empty it is set to the given other cap. 187 | public S2Cap addCap(S2Cap other) { 188 | if (isEmpty()) { 189 | return new S2Cap(other.axis, other.height); 190 | } else { 191 | // See comments for FromAxisAngle() and AddPoint(). This could be 192 | // optimized by doing the calculation in terms of cap heights rather 193 | // than cap opening angles. 194 | double angle = axis.angle(other.axis) + other.angle().radians(); 195 | if (angle >= S2.M_PI) { 196 | return new S2Cap(axis, 2); //Full cap 197 | } else { 198 | double d = Math.sin(0.5 * angle); 199 | double newHeight = Math.max(height, ROUND_UP * 2 * d * d); 200 | return new S2Cap(axis, newHeight); 201 | } 202 | } 203 | } 204 | 205 | // ////////////////////////////////////////////////////////////////////// 206 | // S2Region interface (see {@code S2Region} for details): 207 | * */ 208 | public function getCapBound() { 209 | return $this; 210 | } 211 | 212 | public function getRectBound() { 213 | if ($this->isEmpty()) { 214 | return S2LatLngRect::emptya(); 215 | } 216 | 217 | // Convert the axis to a (lat,lng) pair, and compute the cap angle. 218 | $axisLatLng = new S2LatLng($this->axis); 219 | $capAngle = $this->angle()->radians(); 220 | 221 | $allLongitudes = false; 222 | $lat = array(); 223 | $lng = array(); 224 | $lng[0] = -S2::M_PI; 225 | $lng[1] = S2::M_PI; 226 | 227 | // Check whether cap includes the south pole. 228 | $lat[0] = $axisLatLng->lat()->radians() - $capAngle; 229 | if ($lat[0] <= -S2::M_PI_2) { 230 | $lat[0] = -S2::M_PI_2; 231 | $allLongitudes = true; 232 | } 233 | // Check whether cap includes the north pole. 234 | $lat[1] = $axisLatLng->lat()->radians() + $capAngle; 235 | if ($lat[1] >= S2::M_PI_2) { 236 | $lat[1] = S2::M_PI_2; 237 | $allLongitudes = true; 238 | } 239 | if (!$allLongitudes) { 240 | // Compute the range of longitudes covered by the cap. We use the law 241 | // of sines for spherical triangles. Consider the triangle ABC where 242 | // A is the north pole, B is the center of the cap, and C is the point 243 | // of tangency between the cap boundary and a line of longitude. Then 244 | // C is a right angle, and letting a,b,c denote the sides opposite A,B,C, 245 | // we have sin(a)/sin(A) = sin(c)/sin(C), or sin(A) = sin(a)/sin(c). 246 | // Here "a" is the cap angle, and "c" is the colatitude (90 degrees 247 | // minus the latitude). This formula also works for negative latitudes. 248 | // 249 | // The formula for sin(a) follows from the relationship h = 1 - cos(a). 250 | 251 | $sinA = sqrt($this->height * (2 - $this->height)); 252 | $sinC = cos($axisLatLng->lat()->radians()); 253 | if ($sinA <= $sinC) { 254 | $angleA = asin($sinA / $sinC); 255 | $lng[0] = S2::IEEEremainder($axisLatLng->lng()->radians() - $angleA, 2 * S2::M_PI); 256 | $lng[1] = S2::IEEEremainder($axisLatLng->lng()->radians() + $angleA, 2 * S2::M_PI); 257 | } 258 | } 259 | return new S2LatLngRect( 260 | new R1Interval($lat[0], $lat[1]), 261 | new S1Interval( 262 | $lng[0], 263 | $lng[1] 264 | ) 265 | ); 266 | } 267 | 268 | public function contains($cell) { 269 | // If the cap does not contain all cell vertices, return false. 270 | // We check the vertices before taking the Complement() because we can't 271 | // accurately represent the complement of a very small cap (a height 272 | // of 2-epsilon is rounded off to 2). 273 | $vertices = array(); 274 | for ($k = 0; $k < 4; ++$k) { 275 | $vertices[$k] = $cell->getVertex($k); 276 | if (!$this->contains($vertices[$k])) { 277 | return false; 278 | } 279 | } 280 | // Otherwise, return true if the complement of the cap does not intersect 281 | // the cell. (This test is slightly conservative, because technically we 282 | // want Complement().InteriorIntersects() here.) 283 | return !$this->complement()->intersects($cell, $vertices); 284 | } 285 | 286 | public function mayIntersect(S2Cell $cell) { 287 | // If the cap contains any cell vertex, return true. 288 | $vertices = array(); 289 | for ($k = 0; $k < 4; ++$k) { 290 | $vertices[$k] = $cell->getVertex($k); 291 | if ($this->contains($vertices[$k])) { 292 | return true; 293 | } 294 | } 295 | return $this->intersects($cell, $vertices); 296 | } 297 | 298 | /** 299 | * Return true if the cap intersects 'cell', given that the cap vertices have 300 | * alrady been checked. 301 | *#/ 302 | * public boolean intersects(S2Cell cell, S2Point[] vertices) { 303 | * // Return true if this cap intersects any point of 'cell' excluding its 304 | * // vertices (which are assumed to already have been checked). 305 | * 306 | * // If the cap is a hemisphere or larger, the cell and the complement of the 307 | * // cap are both convex. Therefore since no vertex of the cell is contained, 308 | * // no other interior point of the cell is contained either. 309 | * if (height >= 1) { 310 | * return false; 311 | * } 312 | * 313 | * // We need to check for empty caps due to the axis check just below. 314 | * if (isEmpty()) { 315 | * return false; 316 | * } 317 | * 318 | * // Optimization: return true if the cell contains the cap axis. (This 319 | * // allows half of the edge checks below to be skipped.) 320 | * if (cell.contains(axis)) { 321 | * return true; 322 | * } 323 | * 324 | * // At this point we know that the cell does not contain the cap axis, 325 | * // and the cap does not contain any cell vertex. The only way that they 326 | * // can intersect is if the cap intersects the interior of some edge. 327 | * 328 | * double sin2Angle = height * (2 - height); // sin^2(capAngle) 329 | * for (int k = 0; k < 4; ++k) { 330 | * S2Point edge = cell.getEdgeRaw(k); 331 | * double dot = axis.dotProd(edge); 332 | * if (dot > 0) { 333 | * // The axis is in the interior half-space defined by the edge. We don't 334 | * // need to consider these edges, since if the cap intersects this edge 335 | * // then it also intersects the edge on the opposite side of the cell 336 | * // (because we know the axis is not contained with the cell). 337 | * continue; 338 | * } 339 | * // The Norm2() factor is necessary because "edge" is not normalized. 340 | * if (dot * dot > sin2Angle * edge.norm2()) { 341 | * return false; // Entire cap is on the exterior side of this edge. 342 | * } 343 | * // Otherwise, the great circle containing this edge intersects 344 | * // the interior of the cap. We just need to check whether the point 345 | * // of closest approach occurs between the two edge endpoints. 346 | * S2Point dir = S2Point.crossProd(edge, axis); 347 | * if (dir.dotProd(vertices[k]) < 0 348 | * && dir.dotProd(vertices[(k + 1) & 3]) > 0) { 349 | * return true; 350 | * } 351 | * } 352 | * return false; 353 | * } 354 | * 355 | * public boolean contains(S2Point p) { 356 | * // The point 'p' should be a unit-length vector. 357 | * // assert (S2.isUnitLength(p)); 358 | * return S2Point.sub(axis, p).norm2() <= 2 * height; 359 | * 360 | * } 361 | * 362 | * 363 | * /** Return true if two caps are identical. *#/ 364 | * @Override 365 | * public boolean equals(Object that) { 366 | * 367 | * if (!(that instanceof S2Cap)) { 368 | * return false; 369 | * } 370 | * 371 | * S2Cap other = (S2Cap) that; 372 | * return (axis.equals(other.axis) && height == other.height) 373 | * || (isEmpty() && other.isEmpty()) || (isFull() && other.isFull()); 374 | * 375 | * } 376 | * 377 | * @Override 378 | * public int hashCode() { 379 | * if (isFull()) { 380 | * return 17; 381 | * } else if (isEmpty()) { 382 | * return 37; 383 | * } 384 | * int result = 17; 385 | * result = 37 * result + axis.hashCode(); 386 | * long heightBits = Double.doubleToLongBits(height); 387 | * result = 37 * result + (int) ((heightBits >>> 32) ^ heightBits); 388 | * return result; 389 | * } 390 | * 391 | * // ///////////////////////////////////////////////////////////////////// 392 | * // The following static methods are convenience functions for assertions 393 | * // and testing purposes only. 394 | * 395 | * /** 396 | * Return true if the cap axis and height differ by at most "max_error" from 397 | * the given cap "other". 398 | *#/ 399 | * boolean approxEquals(S2Cap other, double maxError) { 400 | * return (axis.aequal(other.axis, maxError) && Math.abs(height - other.height) <= maxError) 401 | * || (isEmpty() && other.height <= maxError) 402 | * || (other.isEmpty() && height <= maxError) 403 | * || (isFull() && other.height >= 2 - maxError) 404 | * || (other.isFull() && height >= 2 - maxError); 405 | * } 406 | * 407 | * boolean approxEquals(S2Cap other) { 408 | * return approxEquals(other, 1e-14); 409 | * } 410 | * 411 | * @Override 412 | * public String toString() { 413 | * return "[Point = " + axis.toString() + " Height = " + height + "]"; 414 | * } 415 | */ 416 | } 417 | -------------------------------------------------------------------------------- /S2Cell.php: -------------------------------------------------------------------------------- 1 | init(S2CellId::fromPoint($p)); 22 | } else if ($p instanceof S2LatLng) { 23 | $this->init(S2CellId::fromLatLng($p)); 24 | } else if ($p instanceof S2CellId) { 25 | $this->init($p); 26 | } 27 | } 28 | 29 | // This is a static method in order to provide named parameters.*/ 30 | public static function fromFacePosLevel($face, $pos, $level) { 31 | return new S2Cell(S2CellId::fromFacePosLevel($face, $pos, $level)); 32 | } 33 | 34 | public function id() { 35 | return $this->cellId; 36 | } 37 | 38 | public function face() { 39 | return $this->face; 40 | } 41 | 42 | public function level() { 43 | return $this->level; 44 | } 45 | /* 46 | public byte orientation() { 47 | return orientation; 48 | } 49 | 50 | public boolean isLeaf() { 51 | return level == S2CellId.MAX_LEVEL; 52 | } 53 | 54 | public S2Point getVertex(int k) { 55 | return S2Point.normalize(getVertexRaw(k)); 56 | } 57 | 58 | /** 59 | * Return the k-th vertex of the cell (k = 0,1,2,3). Vertices are returned in 60 | * CCW order. The points returned by GetVertexRaw are not necessarily unit 61 | * length. 62 | *#/ 63 | public S2Point getVertexRaw(int k) { 64 | // Vertices are returned in the order SW, SE, NE, NW. 65 | return S2Projections.faceUvToXyz(face, uv[0][(k >> 1) ^ (k & 1)], uv[1][k >> 1]); 66 | } 67 | 68 | public S2Point getEdge(int k) { 69 | return S2Point.normalize(getEdgeRaw(k)); 70 | } 71 | 72 | public S2Point getEdgeRaw(int k) { 73 | switch (k) { 74 | case 0: 75 | return S2Projections.getVNorm(face, uv[1][0]); // South 76 | case 1: 77 | return S2Projections.getUNorm(face, uv[0][1]); // East 78 | case 2: 79 | return S2Point.neg(S2Projections.getVNorm(face, uv[1][1])); // North 80 | default: 81 | return S2Point.neg(S2Projections.getUNorm(face, uv[0][0])); // West 82 | } 83 | } 84 | */ 85 | /** 86 | * Return the inward-facing normal of the great circle passing through the 87 | * edge from vertex k to vertex k+1 (mod 4). The normals returned by 88 | * GetEdgeRaw are not necessarily unit length. 89 | * 90 | * If this is not a leaf cell, set children[0..3] to the four children of 91 | * this cell (in traversal order) and return true. Otherwise returns false. 92 | * This method is equivalent to the following: 93 | * 94 | * for (pos=0, id=child_begin(); id != child_end(); id = id.next(), ++pos) 95 | * children[i] = S2Cell(id); 96 | * 97 | * except that it is more than two times faster. 98 | * @param S2Cell[] $children 99 | */ 100 | public function subdivide(&$children) { 101 | // This function is equivalent to just iterating over the child cell ids 102 | // and calling the S2Cell constructor, but it is about 2.5 times faster. 103 | 104 | if ($this->cellId->isLeaf()) { 105 | return false; 106 | } 107 | 108 | // Compute the cell midpoint in uv-space. 109 | $uvMid = $this->getCenterUV(); 110 | 111 | // Create four children with the appropriate bounds. 112 | /** @var S2CellId $id */ 113 | $id = $this->cellId->childBegin(); 114 | for ($pos = 0; $pos < 4; ++$pos, $id = $id->next()) { 115 | $child = &$children[$pos]; 116 | $child->face = $this->face; 117 | $child->level = $this->level + 1; 118 | $new_o = S2::posToOrientation($pos); 119 | $child->orientation = $this->orientation ^ $new_o; 120 | // echo "this-ori:" . $this->orientation . " new_o:" . $new_o . " res:" . $child->orientation . "\n"; 121 | $child->cellId = $id; 122 | $ij = S2::posToIJ($this->orientation, $pos); 123 | for ($d = 0; $d < 2; ++$d) { 124 | // The dimension 0 index (i/u) is in bit 1 of ij. 125 | $m = 1 - (($ij >> (1 - $d)) & 1); 126 | $child->uv[$d][$m] = $uvMid->get($d); 127 | $child->uv[$d][1 - $m] = $this->uv[$d][1 - $m]; 128 | } 129 | } 130 | return true; 131 | } 132 | 133 | /** 134 | * Return the direction vector corresponding to the center in (s,t)-space of 135 | * the given cell. This is the point at which the cell is divided into four 136 | * subcells; it is not necessarily the centroid of the cell in (u,v)-space or 137 | * (x,y,z)-space. The point returned by GetCenterRaw is not necessarily unit 138 | * length. 139 | *#/ 140 | * public S2Point getCenter() { 141 | * return S2Point.normalize(getCenterRaw()); 142 | * } 143 | * 144 | * public S2Point getCenterRaw() { 145 | * return cellId.toPointRaw(); 146 | * } 147 | * 148 | * /** 149 | * Return the center of the cell in (u,v) coordinates (see {@code 150 | * S2Projections}). Note that the center of the cell is defined as the point 151 | * at which it is recursively subdivided into four children; in general, it is 152 | * not at the midpoint of the (u,v) rectangle covered by the cell 153 | */ 154 | public function getCenterUV() { 155 | $i = 0; 156 | $j = 0; 157 | $null = null; 158 | $this->cellId->toFaceIJOrientation($i, $j, $null); 159 | $cellSize = 1 << (S2CellId::MAX_LEVEL - $this->level); 160 | 161 | // TODO(dbeaumont): Figure out a better naming of the variables here (and elsewhere). 162 | $si = ($i & -$cellSize) * 2 + $cellSize - self::MAX_CELL_SIZE; 163 | $x = S2Projections::stToUV((1.0 / self::MAX_CELL_SIZE) * $si); 164 | 165 | $sj = ($j & -$cellSize) * 2 + $cellSize - self::MAX_CELL_SIZE; 166 | $y = S2Projections::stToUV((1.0 / self::MAX_CELL_SIZE) * $sj); 167 | 168 | return new R2Vector($x, $y); 169 | } 170 | 171 | /** 172 | * Return the average area for cells at the given level. 173 | *#/ 174 | * public static double averageArea(int level) { 175 | * return S2Projections.AVG_AREA.getValue(level); 176 | * } 177 | * 178 | * /** 179 | * Return the average area of cells at this level. This is accurate to within 180 | * a factor of 1.7 (for S2_QUADRATIC_PROJECTION) and is extremely cheap to 181 | * compute. 182 | *#/ 183 | * public double averageArea() { 184 | * return averageArea(level); 185 | * } 186 | * 187 | * /** 188 | * Return the approximate area of this cell. This method is accurate to within 189 | * 3% percent for all cell sizes and accurate to within 0.1% for cells at 190 | * level 5 or higher (i.e. 300km square or smaller). It is moderately cheap to 191 | * compute. 192 | *#/ 193 | * public double approxArea() { 194 | * 195 | * // All cells at the first two levels have the same area. 196 | * if (level < 2) { 197 | * return averageArea(level); 198 | * } 199 | * 200 | * // First, compute the approximate area of the cell when projected 201 | * // perpendicular to its normal. The cross product of its diagonals gives 202 | * // the normal, and the length of the normal is twice the projected area. 203 | * double flatArea = 0.5 * S2Point.crossProd( 204 | * S2Point.sub(getVertex(2), getVertex(0)), S2Point.sub(getVertex(3), getVertex(1))).norm(); 205 | * 206 | * // Now, compensate for the curvature of the cell surface by pretending 207 | * // that the cell is shaped like a spherical cap. The ratio of the 208 | * // area of a spherical cap to the area of its projected disc turns out 209 | * // to be 2 / (1 + sqrt(1 - r*r)) where "r" is the radius of the disc. 210 | * // For example, when r=0 the ratio is 1, and when r=1 the ratio is 2. 211 | * // Here we set Pi*r*r == flat_area to find the equivalent disc. 212 | * return flatArea * 2 / (1 + Math.sqrt(1 - Math.min(S2.M_1_PI * flatArea, 1.0))); 213 | * } 214 | * 215 | * /** 216 | * Return the area of this cell as accurately as possible. This method is more 217 | * expensive but it is accurate to 6 digits of precision even for leaf cells 218 | * (whose area is approximately 1e-18). 219 | *#/ 220 | * public double exactArea() { 221 | * S2Point v0 = getVertex(0); 222 | * S2Point v1 = getVertex(1); 223 | * S2Point v2 = getVertex(2); 224 | * S2Point v3 = getVertex(3); 225 | * return S2.area(v0, v1, v2) + S2.area(v0, v2, v3); 226 | * } 227 | * 228 | * // ////////////////////////////////////////////////////////////////////// 229 | * // S2Region interface (see {@code S2Region} for details): 230 | * 231 | * @Override 232 | * public S2Region clone() { 233 | * S2Cell clone = new S2Cell(); 234 | * clone.face = this.face; 235 | * clone.level = this.level; 236 | * clone.orientation = this.orientation; 237 | * clone.uv = this.uv.clone(); 238 | * 239 | * return clone; 240 | * } 241 | 242 | */ 243 | public function getCapBound() { 244 | // Use the cell center in (u,v)-space as the cap axis. This vector is 245 | // very close to GetCenter() and faster to compute. Neither one of these 246 | // vectors yields the bounding cap with minimal surface area, but they 247 | // are both pretty close. 248 | // 249 | // It's possible to show that the two vertices that are furthest from 250 | // the (u,v)-origin never determine the maximum cap size (this is a 251 | // possible future optimization). 252 | 253 | $u = 0.5 * ($this->uv[0][0] + $this->uv[0][1]); 254 | $v = 0.5 * ($this->uv[1][0] + $this->uv[1][1]); 255 | $cap = S2Cap::fromAxisHeight(S2Point::normalize(S2Projections::faceUvToXyz($this->face, $u, $v)), 0); 256 | for ($k = 0; $k < 4; ++$k) { 257 | $cap = $cap->addPoint($this->getVertex($k)); 258 | } 259 | return $cap; 260 | } 261 | // We grow the bounds slightly to make sure that the bounding rectangle 262 | // also contains the normalized versions of the vertices. Note that the 263 | // maximum result magnitude is Pi, with a floating-point exponent of 1. 264 | // Therefore adding or subtracting 2**-51 will always change the result. 265 | const MAX_ERROR = MAX_ERROR; 266 | 267 | // The 4 cells around the equator extend to +/-45 degrees latitude at the 268 | // midpoints of their top and bottom edges. The two cells covering the 269 | // poles extend down to +/-35.26 degrees at their vertices. 270 | // adding kMaxError (as opposed to the C version) because of asin and atan2 271 | // roundoff errors 272 | const POLE_MIN_LAT = POLE_MIN_LAT; 273 | 274 | // 35.26 degrees 275 | 276 | public function getRectBound() { 277 | if ($this->level > 0) { 278 | // Except for cells at level 0, the latitude and longitude extremes are 279 | // attained at the vertices. Furthermore, the latitude range is 280 | // determined by one pair of diagonally opposite vertices and the 281 | // longitude range is determined by the other pair. 282 | // 283 | // We first determine which corner (i,j) of the cell has the largest 284 | // absolute latitude. To maximize latitude, we want to find the point in 285 | // the cell that has the largest absolute z-coordinate and the smallest 286 | // absolute x- and y-coordinates. To do this we look at each coordinate 287 | // (u and v), and determine whether we want to minimize or maximize that 288 | // coordinate based on the axis direction and the cell's (u,v) quadrant. 289 | $u = $this->uv[0][0] + $this->uv[0][1]; 290 | $v = $this->uv[1][0] + $this->uv[1][1]; 291 | $i = S2Projections::getUAxis($this->face)->z == 0 ? ($u < 0 ? 1 : 0) : ($u > 0 ? 1 : 0); 292 | $j = S2Projections::getVAxis($this->face)->z == 0 ? ($v < 0 ? 1 : 0) : ($v > 0 ? 1 : 0); 293 | 294 | $lat = R1Interval::fromPointPair($this->getLatitude($i, $j), $this->getLatitude(1 - $i, 1 - $j)); 295 | $lat = $lat->expanded(self::MAX_ERROR)->intersection(S2LatLngRect::fullLat()); 296 | if ($lat->lo() == -S2::M_PI_2 || $lat->hi() == S2::M_PI_2) { 297 | return new S2LatLngRect($lat, S1Interval::full()); 298 | } 299 | $lng = S1Interval::fromPointPair($this->getLongitude($i, 1 - $j), $this->getLongitude(1 - $i, $j)); 300 | return new S2LatLngRect($lat, $lng->expanded(self::MAX_ERROR)); 301 | } 302 | 303 | 304 | // The face centers are the +X, +Y, +Z, -X, -Y, -Z axes in that order. 305 | // assert (S2Projections.getNorm(face).get(face % 3) == ((face < 3) ? 1 : -1)); 306 | switch ($this->face) { 307 | case 0: 308 | return new S2LatLngRect( 309 | new R1Interval(-S2::M_PI_4, S2::M_PI_4), 310 | new S1Interval(-S2::M_PI_4, S2::M_PI_4) 311 | ); 312 | 313 | case 1: 314 | return new S2LatLngRect( 315 | new R1Interval(-S2::M_PI_4, S2::M_PI_4), 316 | new S1Interval(S2::M_PI_4, 3 * S2::M_PI_4) 317 | ); 318 | 319 | case 2: 320 | return new S2LatLngRect( 321 | new R1Interval(POLE_MIN_LAT, S2::M_PI_2), 322 | new S1Interval(-S2::M_PI, S2::M_PI) 323 | ); 324 | 325 | case 3: 326 | return new S2LatLngRect( 327 | new R1Interval(-S2::M_PI_4, S2::M_PI_4), 328 | new S1Interval(3 * S2::M_PI_4, -3 * S2::M_PI_4) 329 | ); 330 | 331 | case 4: 332 | return new S2LatLngRect( 333 | new R1Interval(-S2::M_PI_4, S2::M_PI_4), 334 | new S1Interval(-3 * S2::M_PI_4, -S2::M_PI_4) 335 | ); 336 | 337 | default: 338 | return new S2LatLngRect( 339 | new R1Interval(-S2::M_PI_2, -POLE_MIN_LAT), 340 | new S1Interval(-S2::M_PI, S2::M_PI) 341 | ); 342 | } 343 | } 344 | 345 | public function mayIntersect(S2Cell $cell) { 346 | return $this->cellId->intersects($cell->cellId); 347 | } 348 | 349 | public function contains($p) { 350 | // We can't just call XYZtoFaceUV, because for points that lie on the 351 | // boundary between two faces (i.e. u or v is +1/-1) we need to return 352 | // true for both adjacent cells. 353 | if ($p instanceof S2Point) { 354 | $uvPoint = S2Projections::faceXyzToUv($this->face, $p); 355 | if ($uvPoint == null) { 356 | return false; 357 | } 358 | return ($uvPoint->x() >= $uv[0][0] && $uvPoint->x() <= $uv[0][1] 359 | && $uvPoint->y() >= $uv[1][0] && $uvPoint->y() <= $uv[1][1]); 360 | } else if ($p instanceof S2Cell) { 361 | return $this->cellId . contains($p->cellId); 362 | } 363 | } 364 | 365 | private function init(S2CellId $id) { 366 | $this->cellId = $id; 367 | $ij = array(0, 0); 368 | $mOrientation = 0; 369 | 370 | // echo " $mOrientation\n"; 371 | $this->face = $id->toFaceIJOrientation($ij[0], $ij[1], $mOrientation); 372 | // echo ">> $mOrientation\n"; 373 | $this->orientation = $mOrientation; 374 | $this->level = $id->level(); 375 | $cellSize = 1 << (S2CellId::MAX_LEVEL - $this->level); 376 | for ($d = 0; $d < 2; ++$d) { 377 | // Compute the cell bounds in scaled (i,j) coordinates. 378 | $sijLo = ($ij[$d] & -$cellSize) * 2 - self::MAX_CELL_SIZE; 379 | $sijHi = $sijLo + $cellSize * 2; 380 | $this->uv[$d][0] = S2Projections::stToUV((1.0 / self::MAX_CELL_SIZE) * $sijLo); 381 | $this->uv[$d][1] = S2Projections::stToUV((1.0 / self::MAX_CELL_SIZE) * $sijHi); 382 | } 383 | } 384 | 385 | // Internal method that does the actual work in the constructors. 386 | 387 | private function getLatitude($i, $j) { 388 | $p = S2Projections::faceUvToXyz($this->face, $this->uv[0][$i], $this->uv[1][$j]); 389 | return atan2($p->z, sqrt($p->x * $p->x + $p->y * $p->y)); 390 | } 391 | 392 | private function getLongitude($i, $j) { 393 | $p = S2Projections::faceUvToXyz($this->face, $this->uv[0][$i], $this->uv[1][$j]); 394 | return atan2($p->y, $p->x); 395 | } 396 | 397 | /* 398 | // Return the latitude or longitude of the cell vertex given by (i,j), 399 | // where "i" and "j" are either 0 or 1. 400 | 401 | */ 402 | public function __toString() { 403 | return sprintf("[%d, %d, %d, %s]", $this->face, $this->level, $this->orientation, $this->cellId); 404 | } 405 | /* 406 | @Override 407 | public int hashCode() { 408 | int value = 17; 409 | value = 37 * (37 * (37 * value + face) + orientation) + level; 410 | return 37 * value + id().hashCode(); 411 | } 412 | 413 | @Override 414 | public boolean equals(Object that) { 415 | if (that instanceof S2Cell) { 416 | S2Cell thatCell = (S2Cell) that; 417 | return this.face == thatCell.face && this.level == thatCell.level 418 | && this.orientation == thatCell.orientation && this.cellId.equals(thatCell.cellId); 419 | } 420 | return false; 421 | } 422 | */ 423 | } 424 | -------------------------------------------------------------------------------- /S2EdgeIndex.php: -------------------------------------------------------------------------------- 1 | edges. 19 | */ 20 | private $cells; 21 | 22 | /** 23 | * The edge contained by each cell, as given in the parallel array 24 | * cells. 25 | */ 26 | private $edges; 27 | 28 | /** 29 | * No cell strictly below this level appears in mapping. Initially leaf level, 30 | * that's the minimum level at which we will ever look for test edges. 31 | */ 32 | private $minimumS2LevelUsed; 33 | 34 | /** 35 | * Has the index been computed already? 36 | */ 37 | private $indexComputed; 38 | 39 | /** 40 | * Number of queries so far 41 | */ 42 | private $queryCount; 43 | 44 | /** 45 | * Empties the index in case it already contained something. 46 | */ 47 | public function reset() { 48 | $this->minimumS2LevelUsed = S2CellId::MAX_LEVEL; 49 | $this->indexComputed = false; 50 | $this->queryCount = 0; 51 | $this->cells = null; 52 | $this->edges = null; 53 | } 54 | 55 | /** 56 | * Compares [cell1, edge1] to [cell2, edge2], by cell first and edge second. 57 | * 58 | * @param $cell1 59 | * @param $edge1 60 | * @param $cell2 61 | * @param $edge2 62 | * @return int -1 if [cell1, edge1] is less than [cell2, edge2], 1 if [cell1, 63 | */ 64 | private static function compare($cell1, $edge1, $cell2, $edge2) { 65 | if ($cell1 < $cell2) { 66 | return -1; 67 | } else if ($cell1 > $cell2) { 68 | return 1; 69 | } else if ($edge1 < $edge2) { 70 | return -1; 71 | } else if ($edge1 > $edge2) { 72 | return 1; 73 | } else { 74 | return 0; 75 | } 76 | } 77 | 78 | /** Computes the index (if it has not been previously done). *#/ 79 | public final function computeIndex() { 80 | if ($this->indexComputed) { 81 | return; 82 | } 83 | List 84 | cellList = Lists.newArrayList(); 85 | List 86 | edgeList = Lists.newArrayList(); 87 | for (int i = 0; i < getNumEdges(); ++i) { 88 | S2Point from = edgeFrom(i); 89 | S2Point to = edgeTo(i); 90 | ArrayList 91 | cover = Lists.newArrayList(); 92 | int level = getCovering(from, to, true, cover); 93 | minimumS2LevelUsed = Math.min(minimumS2LevelUsed, level); 94 | for (S2CellId cellId : cover) { 95 | cellList.add(cellId.id()); 96 | edgeList.add(i); 97 | } 98 | } 99 | cells = new long[cellList.size()]; 100 | edges = new int[edgeList.size()]; 101 | for (int i = 0; i < cells.length; i++) { 102 | cells[i] = cellList.get(i); 103 | edges[i] = edgeList.get(i); 104 | } 105 | sortIndex(); 106 | indexComputed = true; 107 | } 108 | 109 | /** Sorts the parallel cells and edges arrays. *#/ 110 | private function sortIndex() { 111 | // create an array of indices and sort based on the values in the parallel 112 | // arrays at each index 113 | Integer[] indices = new Integer[cells.length]; 114 | for (int i = 0; i < indices.length; i++) { 115 | indices[i] = i; 116 | } 117 | Arrays.sort(indices, new Comparator 118 | () { 119 | @Override 120 | public int compare(Integer index1, Integer index2) { 121 | return S2EdgeIndex.compare(cells[index1], edges[index1], cells[index2], edges[index2]); 122 | } 123 | }); 124 | // copy the cells and edges in the order given by the sorted list of indices 125 | long[] newCells = new long[cells.length]; 126 | int[] newEdges = new int[edges.length]; 127 | for (int i = 0; i < indices.length; i++) { 128 | newCells[i] = cells[indices[i]]; 129 | newEdges[i] = edges[indices[i]]; 130 | } 131 | // replace the cells and edges with the sorted arrays 132 | cells = newCells; 133 | edges = newEdges; 134 | } 135 | 136 | public final function isIndexComputed() { 137 | return indexComputed; 138 | } 139 | 140 | /** 141 | * Tell the index that we just received a new request for candidates. Useful 142 | * to compute when to switch to quad tree. 143 | *#/ 144 | protected final function incrementQueryCount() { 145 | ++queryCount; 146 | } 147 | 148 | /** 149 | * If the index hasn't been computed yet, looks at how much work has gone into 150 | * iterating using the brute force method, and how much more work is planned 151 | * as defined by 'cost'. If it were to have been cheaper to use a quad tree 152 | * from the beginning, then compute it now. This guarantees that we will never 153 | * use more than twice the time we would have used had we known in advance 154 | * exactly how many edges we would have wanted to test. It is the theoretical 155 | * best. 156 | * 157 | * The value 'n' is the number of iterators we expect to request from this 158 | * edge index. 159 | * 160 | * If we have m data edges and n query edges, then the brute force cost is m 161 | * * n * testCost where testCost is taken to be the cost of 162 | * EdgeCrosser.robustCrossing, measured to be about 30ns at the time of this 163 | * writing. 164 | * 165 | * If we compute the index, the cost becomes: m * costInsert + n * 166 | * costFind(m) 167 | * 168 | * - costInsert can be expected to be reasonably stable, and was measured at 169 | * 1200ns with the BM_QuadEdgeInsertionCost benchmark. 170 | * 171 | * - costFind depends on the length of the edge . For m=1000 edges, we got 172 | * timings ranging from 1ms (edge the length of the polygon) to 40ms. The 173 | * latter is for very long query edges, and needs to be optimized. We will 174 | * assume for the rest of the discussion that costFind is roughly 3ms. 175 | * 176 | * When doing one additional query, the differential cost is m * testCost - 177 | * costFind(m) With the numbers above, it is better to use the quad tree (if 178 | * we have it) if m >= 100. 179 | * 180 | * If m = 100, 30 queries will give m*n*testCost = m * costInsert = 100ms, 181 | * while the marginal cost to find is 3ms. Thus, this is a reasonable thing to 182 | * do. 183 | *#/ 184 | public final function predictAdditionalCalls(int n) { 185 | if (indexComputed) { 186 | return; 187 | } 188 | if (getNumEdges() > 100 && (queryCount + n) > 30) { 189 | computeIndex(); 190 | } 191 | } 192 | 193 | /** 194 | * Overwrite these functions to give access to the underlying data. The 195 | * function getNumEdges() returns the number of edges in the index, while 196 | * edgeFrom(index) and edgeTo(index) return the "from" and "to" endpoints of 197 | * the edge at the given index. 198 | *#/ 199 | protected abstract function getNumEdges(); 200 | 201 | protected abstract function edgeFrom(int index); 202 | 203 | protected abstract function edgeTo(int index); 204 | 205 | /** 206 | * Appends to "candidateCrossings" all edge references which may cross the 207 | * given edge. This is done by covering the edge and then finding all 208 | * references of edges whose coverings overlap this covering. Parent cells are 209 | * checked level by level. Child cells are checked all at once by taking 210 | * advantage of the natural ordering of S2CellIds. 211 | *#/ 212 | protected function findCandidateCrossings(S2Point a, S2Point b, List 213 | candidateCrossings) { 214 | Preconditions.checkState(indexComputed); 215 | ArrayList 216 | cover = Lists.newArrayList(); 217 | getCovering(a, b, false, cover); 218 | 219 | // Edge references are inserted into the map once for each covering cell, so 220 | // absorb duplicates here 221 | Set 222 | uniqueSet = new HashSet 223 | (); 224 | getEdgesInParentCells(cover, uniqueSet); 225 | 226 | // TODO(user): An important optimization for long query 227 | // edges (Contains queries): keep a bounding cap and clip the query 228 | // edge to the cap before starting the descent. 229 | getEdgesInChildrenCells(a, b, cover, uniqueSet); 230 | 231 | candidateCrossings.clear(); 232 | candidateCrossings.addAll(uniqueSet); 233 | } 234 | 235 | /** 236 | * Returns the smallest cell containing all four points, or 237 | * {@link S2CellId#sentinel()} if they are not all on the same face. The 238 | * points don't need to be normalized. 239 | *#/ 240 | private static function containingCell(S2Point pa, S2Point pb, S2Point pc, S2Point pd) { 241 | S2CellId a = S2CellId.fromPoint(pa); 242 | S2CellId b = S2CellId.fromPoint(pb); 243 | S2CellId c = S2CellId.fromPoint(pc); 244 | S2CellId d = S2CellId.fromPoint(pd); 245 | 246 | if (a.face() != b.face() || a.face() != c.face() || a.face() != d.face()) { 247 | return S2CellId.sentinel(); 248 | } 249 | 250 | while (!a.equals(b) || !a.equals(c) || !a.equals(d)) { 251 | a = a.parent(); 252 | b = b.parent(); 253 | c = c.parent(); 254 | d = d.parent(); 255 | } 256 | return a; 257 | } 258 | 259 | /** 260 | * Returns the smallest cell containing both points, or Sentinel if they are 261 | * not all on the same face. The points don't need to be normalized. 262 | *#/ 263 | private static function containingCell(S2Point pa, S2Point pb) { 264 | S2CellId a = S2CellId.fromPoint(pa); 265 | S2CellId b = S2CellId.fromPoint(pb); 266 | 267 | if (a.face() != b.face()) { 268 | return S2CellId.sentinel(); 269 | } 270 | 271 | while (!a.equals(b)) { 272 | a = a.parent(); 273 | b = b.parent(); 274 | } 275 | return a; 276 | } 277 | 278 | /** 279 | * Computes a cell covering of an edge. Clears edgeCovering and returns the 280 | * level of the s2 cells used in the covering (only one level is ever used for 281 | * each call). 282 | * 283 | * If thickenEdge is true, the edge is thickened and extended by 1% of its 284 | * length. 285 | * 286 | * It is guaranteed that no child of a covering cell will fully contain the 287 | * covered edge. 288 | *#/ 289 | private function getCovering( 290 | S2Point a, S2Point b, boolean thickenEdge, ArrayList 291 | edgeCovering) { 292 | edgeCovering.clear(); 293 | 294 | // Selects the ideal s2 level at which to cover the edge, this will be the 295 | // level whose S2 cells have a width roughly commensurate to the length of 296 | // the edge. We multiply the edge length by 2*THICKENING to guarantee the 297 | // thickening is honored (it's not a big deal if we honor it when we don't 298 | // request it) when doing the covering-by-cap trick. 299 | double edgeLength = a.angle(b); 300 | int idealLevel = S2Projections.MIN_WIDTH.getMaxLevel(edgeLength * (1 + 2 * THICKENING)); 301 | 302 | S2CellId containingCellId; 303 | if (!thickenEdge) { 304 | containingCellId = containingCell(a, b); 305 | } else { 306 | if (idealLevel == S2CellId.MAX_LEVEL) { 307 | // If the edge is tiny, instabilities are more likely, so we 308 | // want to limit the number of operations. 309 | // We pretend we are in a cell much larger so as to trigger the 310 | // 'needs covering' case, so we won't try to thicken the edge. 311 | containingCellId = (new S2CellId(0xFFF0)).parent(3); 312 | } else { 313 | S2Point pq = S2Point.mul(S2Point.minus(b, a), THICKENING); 314 | S2Point ortho = 315 | S2Point.mul(S2Point.normalize(S2Point.crossProd(pq, a)), edgeLength * THICKENING); 316 | S2Point p = S2Point.minus(a, pq); 317 | S2Point q = S2Point.add(b, pq); 318 | // If p and q were antipodal, the edge wouldn't be lengthened, 319 | // and it could even flip! This is not a problem because 320 | // idealLevel != 0 here. The farther p and q can be is roughly 321 | // a quarter Earth away from each other, so we remain 322 | // Theta(THICKENING). 323 | containingCellId = 324 | containingCell(S2Point.minus(p, ortho), S2Point.add(p, ortho), S2Point.minus(q, ortho), 325 | S2Point.add(q, ortho)); 326 | } 327 | } 328 | 329 | // Best case: edge is fully contained in a cell that's not too big. 330 | if (!containingCellId.equals(S2CellId.sentinel()) 331 | && containingCellId.level() >= idealLevel - 2) { 332 | edgeCovering.add(containingCellId); 333 | return containingCellId.level(); 334 | } 335 | 336 | if (idealLevel == 0) { 337 | // Edge is very long, maybe even longer than a face width, so the 338 | // trick below doesn't work. For now, we will add the whole S2 sphere. 339 | // TODO(user): Do something a tad smarter (and beware of the 340 | // antipodal case). 341 | for (S2CellId cellid = S2CellId.begin(0); !cellid.equals(S2CellId.end(0)); 342 | cellid = cellid.next()) { 343 | edgeCovering.add(cellid); 344 | } 345 | return 0; 346 | } 347 | // TODO(user): Check trick below works even when vertex is at 348 | // interface 349 | // between three faces. 350 | 351 | // Use trick as in S2PolygonBuilder.PointIndex.findNearbyPoint: 352 | // Cover the edge by a cap centered at the edge midpoint, then cover 353 | // the cap by four big-enough cells around the cell vertex closest to the 354 | // cap center. 355 | S2Point middle = S2Point.normalize(S2Point.div(S2Point.add(a, b), 2)); 356 | int actualLevel = Math.min(idealLevel, S2CellId.MAX_LEVEL - 1); 357 | S2CellId.fromPoint(middle).getVertexNeighbors(actualLevel, edgeCovering); 358 | return actualLevel; 359 | } 360 | 361 | /** 362 | * Filters a list of entries down to the inclusive range defined by the given 363 | * cells, in O(log N) time. 364 | * 365 | * @param cell1 One side of the inclusive query range. 366 | * @param cell2 The other side of the inclusive query range. 367 | * @return An array of length 2, containing the start/end indices. 368 | *#/ 369 | private function getEdges(long cell1, long cell2) { 370 | // ensure cell1 <= cell2 371 | if (cell1 > cell2) { 372 | long temp = cell1; 373 | cell1 = cell2; 374 | cell2 = temp; 375 | } 376 | // The binary search returns -N-1 to indicate an insertion point at index N, 377 | // if an exact match cannot be found. Since the edge indices queried for are 378 | // not valid edge indices, we will always get -N-1, so we immediately 379 | // convert to N. 380 | return new int[]{ 381 | -1 - binarySearch(cell1, Integer.MIN_VALUE), 382 | -1 - binarySearch(cell2, Integer.MAX_VALUE)}; 383 | } 384 | 385 | private function binarySearch(long cell, int edge) { 386 | int low = 0; 387 | int high = cells.length - 1; 388 | while (low <= high) { 389 | int mid = (low + high) >>> 1; 390 | int cmp = compare(cells[mid], edges[mid], cell, edge); 391 | if (cmp < 0) { 392 | low = mid + 1; 393 | } else if (cmp > 0) { 394 | high = mid - 1; 395 | } else { 396 | return mid; 397 | } 398 | } 399 | return -(low + 1); 400 | } 401 | 402 | /** 403 | * Adds to candidateCrossings all the edges present in any ancestor of any 404 | * cell of cover, down to minimumS2LevelUsed. The cell->edge map is in the 405 | * variable mapping. 406 | *#/ 407 | private function getEdgesInParentCells(List 408 | cover, Set 409 | candidateCrossings) { 410 | // Find all parent cells of covering cells. 411 | Set 412 | parentCells = Sets.newHashSet(); 413 | for (S2CellId coverCell : cover) { 414 | for (int parentLevel = coverCell.level() - 1; parentLevel >= minimumS2LevelUsed; 415 | --parentLevel) { 416 | if (!parentCells.add(coverCell.parent(parentLevel))) { 417 | break; // cell is already in => parents are too. 418 | } 419 | } 420 | } 421 | 422 | // Put parent cell edge references into result. 423 | for (S2CellId parentCell : parentCells) { 424 | int[] bounds = getEdges(parentCell.id(), parentCell.id()); 425 | for (int i = bounds[0]; i < bounds[1]; i++) { 426 | candidateCrossings.add(edges[i]); 427 | } 428 | } 429 | } 430 | 431 | /** 432 | * Returns true if ab possibly crosses cd, by clipping tiny angles to zero. 433 | *#/ 434 | private static function lenientCrossing(S2Point a, S2Point b, S2Point c, S2Point d) { 435 | // assert (S2.isUnitLength(a)); 436 | // assert (S2.isUnitLength(b)); 437 | // assert (S2.isUnitLength(c)); 438 | 439 | double acb = S2Point.crossProd(a, c).dotProd(b); 440 | double bda = S2Point.crossProd(b, d).dotProd(a); 441 | if (Math.abs(acb) < MAX_DET_ERROR || Math.abs(bda) < MAX_DET_ERROR) { 442 | return true; 443 | } 444 | if (acb * bda < 0) { 445 | return false; 446 | } 447 | double cbd = S2Point.crossProd(c, b).dotProd(d); 448 | double dac = S2Point.crossProd(c, a).dotProd(c); 449 | if (Math.abs(cbd) < MAX_DET_ERROR || Math.abs(dac) < MAX_DET_ERROR) { 450 | return true; 451 | } 452 | return (acb * cbd >= 0) && (acb * dac >= 0); 453 | } 454 | 455 | /** 456 | * Returns true if the edge and the cell (including boundary) intersect. 457 | *#/ 458 | private static function edgeIntersectsCellBoundary(S2Point a, S2Point b, S2Cell cell) { 459 | S2Point[] vertices = new S2Point[4]; 460 | for (int i = 0; i < 4; ++i) { 461 | vertices[i] = cell.getVertex(i); 462 | } 463 | for (int i = 0; i < 4; ++i) { 464 | S2Point fromPoint = vertices[i]; 465 | S2Point toPoint = vertices[(i + 1) % 4]; 466 | if (lenientCrossing(a, b, fromPoint, toPoint)) { 467 | return true; 468 | } 469 | } 470 | return false; 471 | } 472 | 473 | /** 474 | * Appends to candidateCrossings the edges that are fully contained in an S2 475 | * covering of edge. The covering of edge used is initially cover, but is 476 | * refined to eliminate quickly subcells that contain many edges but do not 477 | * intersect with edge. 478 | *#/ 479 | private function getEdgesInChildrenCells(S2Point a, S2Point b, List 480 | cover, 481 | Set 482 | candidateCrossings) { 483 | // Put all edge references of (covering cells + descendant cells) into 484 | // result. 485 | // This relies on the natural ordering of S2CellIds. 486 | S2Cell[] children = null; 487 | while (!cover.isEmpty()) { 488 | S2CellId cell = cover.remove(cover.size() - 1); 489 | int[] bounds = getEdges(cell.rangeMin().id(), cell.rangeMax().id()); 490 | if (bounds[1] - bounds[0] <= 16) { 491 | for (int i = bounds[0]; i < bounds[1]; i++) { 492 | candidateCrossings.add(edges[i]); 493 | } 494 | } else { 495 | // Add cells at this level 496 | bounds = getEdges(cell.id(), cell.id()); 497 | for (int i = bounds[0]; i < bounds[1]; i++) { 498 | candidateCrossings.add(edges[i]); 499 | } 500 | // Recurse on the children -- hopefully some will be empty. 501 | if (children == null) { 502 | children = new S2Cell[4]; 503 | for (int i = 0; i < 4; ++i) { 504 | children[i] = new S2Cell(); 505 | } 506 | } 507 | new S2Cell(cell).subdivide(children); 508 | for (S2Cell child : children) { 509 | // TODO(user): Do the check for the four cells at once, 510 | // as it is enough to check the four edges between the cells. At 511 | // this time, we are checking 16 edges, 4 times too many. 512 | // 513 | // Note that given the guarantee of AppendCovering, it is enough 514 | // to check that the edge intersect with the cell boundary as it 515 | // cannot be fully contained in a cell. 516 | if (edgeIntersectsCellBoundary(a, b, child)) { 517 | cover.add(child.id()); 518 | } 519 | } 520 | } 521 | } 522 | } 523 | */ 524 | } 525 | 526 | /* 527 | * An iterator on data edges that may cross a query edge (a,b). Create the 528 | * iterator, call getCandidates(), then hasNext()/next() repeatedly. 529 | * 530 | * The current edge in the iteration has index index(), goes between from() 531 | * and to(). 532 | */ 533 | 534 | class DataEdgeIterator { 535 | /** 536 | * The structure containing the data edges. 537 | */ 538 | private $edgeIndex; 539 | 540 | /** 541 | * Tells whether getCandidates() obtained the candidates through brute force 542 | * iteration or using the quad tree structure. 543 | */ 544 | private $isBruteForce; 545 | 546 | /** 547 | * Index of the current edge and of the edge before the last next() call. 548 | */ 549 | private $currentIndex; 550 | 551 | /** 552 | * Cache of edgeIndex.getNumEdges() so that hasNext() doesn't make an extra 553 | * call 554 | */ 555 | private $numEdges; 556 | 557 | /** 558 | * All the candidates obtained by getCandidates() when we are using a 559 | * quad-tree (i.e. isBruteForce = false). 560 | *#/ 561 | ArrayList 562 | candidates; 563 | 564 | /** 565 | * Index within array above. We have: currentIndex = 566 | * candidates.get(currentIndexInCandidates). 567 | *#/ 568 | private int currentIndexInCandidates; 569 | 570 | public DataEdgeIterator(S2EdgeIndex edgeIndex) { 571 | this.edgeIndex = edgeIndex; 572 | candidates = Lists.newArrayList(); 573 | } 574 | 575 | /** 576 | * Initializes the iterator to iterate over a set of candidates that may 577 | * cross the edge (a,b). 578 | *#/ 579 | public void getCandidates(S2Point a, S2Point b) { 580 | edgeIndex.predictAdditionalCalls(1); 581 | isBruteForce = !edgeIndex.isIndexComputed(); 582 | if (isBruteForce) { 583 | edgeIndex.incrementQueryCount(); 584 | currentIndex = 0; 585 | numEdges = edgeIndex.getNumEdges(); 586 | } else { 587 | candidates.clear(); 588 | edgeIndex.findCandidateCrossings(a, b, candidates); 589 | currentIndexInCandidates = 0; 590 | if (!candidates.isEmpty()) { 591 | currentIndex = candidates.get(0); 592 | } 593 | } 594 | } 595 | 596 | /** 597 | * Index of the current edge in the iteration. 598 | *#/ 599 | public int index() { 600 | Preconditions.checkState(hasNext()); 601 | return currentIndex; 602 | } 603 | 604 | /** 605 | * False if there are no more candidates; true otherwise. 606 | *#/ 607 | public boolean hasNext() { 608 | if (isBruteForce) { 609 | return (currentIndex < numEdges); 610 | } else { 611 | return currentIndexInCandidates < candidates.size(); 612 | } 613 | } 614 | 615 | /** 616 | * Iterate to the next available candidate. 617 | *#/ 618 | public void next() { 619 | Preconditions.checkState(hasNext()); 620 | if (isBruteForce) { 621 | ++currentIndex; 622 | } else { 623 | ++currentIndexInCandidates; 624 | if (currentIndexInCandidates < candidates.size()) { 625 | currentIndex = candidates.get(currentIndexInCandidates); 626 | } 627 | } 628 | } 629 | */ 630 | } 631 | -------------------------------------------------------------------------------- /S2RegionCoverer.php: -------------------------------------------------------------------------------- 1 | , entries of equal 57 | * priority would be sorted according to the memory address of the candidate. 58 | */ 59 | /* static class QueueEntriesComparator implements Comparator { 60 | @Override 61 | public int compare(S2RegionCoverer.QueueEntry x, S2RegionCoverer.QueueEntry y) { 62 | return x.id < y.id ? 1 : (x.id > y.id ? -1 : 0); 63 | } 64 | }*/ 65 | 66 | 67 | /** 68 | * We keep the candidates in a priority queue. We specify a vector to hold the 69 | * queue entries since for some reason priority_queue<> uses a deque by 70 | * default. 71 | */ 72 | private $candidateQueue; 73 | 74 | /** 75 | * Default constructor, sets all fields to default values. 76 | */ 77 | public function __construct() { 78 | $this->minLevel = 0; 79 | $this->maxLevel = S2CellId::MAX_LEVEL; 80 | $this->levelMod = 1; 81 | $this->maxCells = self::DEFAULT_MAX_CELLS; 82 | $this->region = null; 83 | $this->result = array(); 84 | // TODO(kirilll?): 10 is a completely random number, work out a better 85 | // estimate 86 | // $this->candidateQueue = array();//new PriorityQueue(10, new QueueEntriesComparator()); 87 | $this->candidateQueue = new SplPriorityQueue(); //new PriorityQueue(10, new QueueEntriesComparator()); 88 | } 89 | 90 | // Set the minimum and maximum cell level to be used. The default is to use 91 | // all cell levels. Requires: max_level() >= min_level(). 92 | // 93 | // To find the cell level corresponding to a given physical distance, use 94 | // the S2Cell metrics defined in s2.h. For example, to find the cell 95 | // level that corresponds to an average edge length of 10km, use: 96 | // 97 | // int level = S2::kAvgEdge.GetClosestLevel( 98 | // geostore::S2Earth::KmToRadians(length_km)); 99 | // 100 | // Note: min_level() takes priority over max_cells(), i.e. cells below the 101 | // given level will never be used even if this causes a large number of 102 | // cells to be returned. 103 | 104 | /** 105 | * Sets the minimum level to be used. 106 | */ 107 | public function setMinLevel($minLevel) { 108 | // assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL); 109 | $this->minLevel = max(0, min(S2CellId::MAX_LEVEL, $minLevel)); 110 | } 111 | 112 | /** 113 | * Sets the maximum level to be used. 114 | */ 115 | public function setMaxLevel($maxLevel) { 116 | // assert (maxLevel >= 0 && maxLevel <= S2CellId.MAX_LEVEL); 117 | $this->maxLevel = max(0, min(S2CellId::MAX_LEVEL, $maxLevel)); 118 | } 119 | 120 | public function minLevel() { 121 | return $this->minLevel; 122 | } 123 | 124 | public function maxLevel() { 125 | return $this->maxLevel; 126 | } 127 | 128 | // public int maxCells() { 129 | // return maxCells; 130 | // } 131 | 132 | /** 133 | * If specified, then only cells where (level - min_level) is a multiple of 134 | * "level_mod" will be used (default 1). This effectively allows the branching 135 | * factor of the S2CellId hierarchy to be increased. Currently the only 136 | * parameter values allowed are 1, 2, or 3, corresponding to branching factors 137 | * of 4, 16, and 64 respectively. 138 | */ 139 | // public void setLevelMod(int levelMod) { 140 | // assert (levelMod >= 1 && levelMod <= 3); 141 | // this.levelMod = Math.max(1, Math.min(3, levelMod)); 142 | // } 143 | 144 | public function levelMod() { 145 | return $this->levelMod; 146 | } 147 | 148 | /** 149 | * Sets the maximum desired number of cells in the approximation (defaults to 150 | * kDefaultMaxCells). Note the following: 151 | * 152 | *

166 | * 167 | * Accuracy is measured by dividing the area of the covering by the area of 168 | * the original region. The following table shows the median and worst case 169 | * values for this area ratio on a test case consisting of 100,000 spherical 170 | * caps of random size (generated using s2regioncoverer_unittest): 171 | * 172 | *
173 |      * max_cells: 3 4 5 6 8 12 20 100 1000
174 |      * median ratio: 5.33 3.32 2.73 2.34 1.98 1.66 1.42 1.11 1.01
175 |      * worst case: 215518 14.41 9.72 5.26 3.91 2.75 1.92 1.20 1.02
176 |      * 
177 | */ 178 | // public void setMaxCells(int maxCells) { 179 | // this.maxCells = maxCells; 180 | // } 181 | 182 | /** 183 | * Computes a list of cell ids that covers the given region and satisfies the 184 | * various restrictions specified above. 185 | * 186 | * @param region The region to cover 187 | * @param S2CellId[] covering The list filled in by this method 188 | */ 189 | public function getCovering(S2Region $region, &$covering) { 190 | // Rather than just returning the raw list of cell ids generated by 191 | // GetCoveringInternal(), we construct a cell union and then denormalize it. 192 | // This has the effect of replacing four child cells with their parent 193 | // whenever this does not violate the covering parameters specified 194 | // (min_level, level_mod, etc). This strategy significantly reduces the 195 | // number of cells returned in many cases, and it is cheap compared to 196 | // computing the covering in the first place. 197 | 198 | $tmp = new S2CellUnion(); 199 | 200 | $this->interiorCovering = false; 201 | $this->getCoveringInternal($region); 202 | $tmp->initSwap($this->result); 203 | 204 | $tmp->denormalize($this->minLevel(), $this->levelMod(), $covering); 205 | } 206 | 207 | /** 208 | * Computes a list of cell ids that is contained within the given region and 209 | * satisfies the various restrictions specified above. 210 | * 211 | * @param region The region to fill 212 | * @param interior The list filled in by this method 213 | */ 214 | // public void getInteriorCovering(S2Region region, ArrayList interior) { 215 | // S2CellUnion tmp = getInteriorCovering(region); 216 | // tmp.denormalize(minLevel(), levelMod(), interior); 217 | // } 218 | 219 | /** 220 | * Return a normalized cell union that is contained within the given region 221 | * and satisfies the restrictions *EXCEPT* for min_level() and level_mod(). 222 | */ 223 | // public S2CellUnion getInteriorCovering(S2Region region) { 224 | // S2CellUnion covering = new S2CellUnion(); 225 | // getInteriorCovering(region, covering); 226 | // return covering; 227 | // } 228 | 229 | // public void getInteriorCovering(S2Region region, S2CellUnion covering) { 230 | // interiorCovering = true; 231 | // getCoveringInternal(region); 232 | // covering.initSwap(result); 233 | // } 234 | 235 | /** 236 | * Given a connected region and a starting point, return a set of cells at the 237 | * given level that cover the region. 238 | */ 239 | // public static void getSimpleCovering( 240 | // S2Region region, S2Point start, int level, ArrayList output) { 241 | // floodFill(region, S2CellId.fromPoint(start).parent(level), output); 242 | // } 243 | 244 | /** 245 | * If the cell intersects the given region, return a new candidate with no 246 | * children, otherwise return null. Also marks the candidate as "terminal" if 247 | * it should not be expanded further. 248 | */ 249 | private function newCandidate(S2Cell $cell) { 250 | if (!$this->region->mayIntersect($cell)) { 251 | // echo "null\n"; 252 | return null; 253 | } 254 | 255 | $isTerminal = false; 256 | if ($cell->level() >= $this->minLevel) { 257 | if ($this->interiorCovering) { 258 | if ($this->region->contains($cell)) { 259 | $isTerminal = true; 260 | } else if ($cell->level() + $this->levelMod > $this->maxLevel) { 261 | return null; 262 | } 263 | } else { 264 | if ($cell->level() + $this->levelMod > $this->maxLevel || $this->region->contains($cell)) { 265 | $isTerminal = true; 266 | } 267 | } 268 | } 269 | $candidate = new Candidate(); 270 | $candidate->cell = $cell; 271 | $candidate->isTerminal = $isTerminal; 272 | if (!$isTerminal) { 273 | $candidate->children = array_pad(array(), 1 << $this->maxChildrenShift(), new Candidate); 274 | } 275 | $this->candidatesCreatedCounter++; 276 | return $candidate; 277 | } 278 | 279 | /** Return the log base 2 of the maximum number of children of a candidate. */ 280 | private function maxChildrenShift() { 281 | return 2 * $this->levelMod; 282 | } 283 | 284 | /** 285 | * Process a candidate by either adding it to the result list or expanding its 286 | * children and inserting it into the priority queue. Passing an argument of 287 | * NULL does nothing. 288 | */ 289 | private function addCandidate(Candidate $candidate = null) { 290 | if ($candidate == null) { 291 | // echo "\t addCandidate null\n"; 292 | return; 293 | } 294 | 295 | if ($candidate->isTerminal) { 296 | // echo "\taddCandidato terminal: " . $candidate->cell->id() . "\n"; 297 | $this->result[] = $candidate->cell->id(); 298 | return; 299 | } 300 | // assert (candidate.numChildren == 0); 301 | 302 | // Expand one level at a time until we hit min_level_ to ensure that 303 | // we don't skip over it. 304 | $numLevels = ($candidate->cell->level() < $this->minLevel) ? 1 : $this->levelMod; 305 | $numTerminals = $this->expandChildren($candidate, $candidate->cell, $numLevels); 306 | 307 | // var_dump($candidate->numChildren); 308 | 309 | if ($candidate->numChildren == 0) { 310 | // echo "\tcandidate numChildred is zero\n"; 311 | // Do nothing 312 | } else if (!$this->interiorCovering && $numTerminals == 1 << $this->maxChildrenShift() 313 | && $candidate->cell->level() >= $this->minLevel) { 314 | // Optimization: add the parent cell rather than all of its children. 315 | // We can't do this for interior coverings, since the children just 316 | // intersect the region, but may not be contained by it - we need to 317 | // subdivide them further. 318 | $candidate->isTerminal = true; 319 | echo "addCandidato recurse: " . $candidate->cell->id() . "\n"; 320 | $this->addCandidate($candidate); 321 | } else { 322 | // We negate the priority so that smaller absolute priorities are returned 323 | // first. The heuristic is designed to refine the largest cells first, 324 | // since those are where we have the largest potential gain. Among cells 325 | // at the same level, we prefer the cells with the smallest number of 326 | // intersecting children. Finally, we prefer cells that have the smallest 327 | // number of children that cannot be refined any further. 328 | $priority = -(((($candidate->cell->level() << $this->maxChildrenShift()) + $candidate->numChildren) << $this->maxChildrenShift()) + $numTerminals); 329 | // echo "Push: " . $candidate . " ($priority)\n"; 330 | $this->candidateQueue->insert($candidate, $priority); 331 | // logger.info("Push: " + candidate.cell.id() + " (" + priority + ") "); 332 | } 333 | } 334 | 335 | /** 336 | * Populate the children of "candidate" by expanding the given number of 337 | * levels from the given cell. Returns the number of children that were marked 338 | * "terminal". 339 | */ 340 | private function expandChildren(Candidate $candidate, S2Cell $cell, $numLevels) { 341 | $numLevels--; 342 | $childCells = array(); 343 | for ($i = 0; $i < 4; ++$i) { 344 | $childCells[$i] = new S2Cell(); 345 | } 346 | $cell->subdivide($childCells); 347 | $numTerminals = 0; 348 | for ($i = 0; $i < 4; ++$i) { 349 | if ($numLevels > 0) { 350 | if ($this->region->mayIntersect($childCells[$i])) { 351 | $numTerminals += $this->expandChildren($candidate, $childCells[$i], $numLevels); 352 | } 353 | continue; 354 | } 355 | $child = $this->newCandidate($childCells[$i]); 356 | // echo "child for " . $childCells[$i] . " is " . $child . "\n"; 357 | 358 | if ($child != null) { 359 | $candidate->children[$candidate->numChildren++] = $child; 360 | if ($child->isTerminal) { 361 | ++$numTerminals; 362 | } 363 | } 364 | } 365 | return $numTerminals; 366 | } 367 | 368 | /** Computes a set of initial candidates that cover the given region. */ 369 | private function getInitialCandidates() { 370 | // Optimization: if at least 4 cells are desired (the normal case), 371 | // start with a 4-cell covering of the region's bounding cap. This 372 | // lets us skip quite a few levels of refinement when the region to 373 | // be covered is relatively small. 374 | if ($this->maxCells >= 4) { 375 | // Find the maximum level such that the bounding cap contains at most one 376 | // cell vertex at that level. 377 | $cap = $this->region->getCapBound(); 378 | $level = min( 379 | S2Projections::MIN_WIDTH()->getMaxLevel(2 * $cap->angle()->radians()), 380 | min($this->maxLevel(), S2CellId::MAX_LEVEL - 1) 381 | ); 382 | if ($this->levelMod() > 1 && $level > $this->minLevel()) { 383 | $level -= ($level - $this->minLevel()) % $this->levelMod(); 384 | } 385 | // We don't bother trying to optimize the level == 0 case, since more than 386 | // four face cells may be required. 387 | if ($level > 0) { 388 | // Find the leaf cell containing the cap axis, and determine which 389 | // subcell of the parent cell contains it. 390 | /** @var S2CellId[] $base */ 391 | $base = array(); 392 | 393 | $s2point_tmp = $cap->axis(); 394 | 395 | $id = S2CellId::fromPoint($s2point_tmp); 396 | $id->getVertexNeighbors($level, $base); 397 | for ($i = 0; $i < count($base); ++$i) { 398 | 399 | // printf("(face=%s pos=%s level=%s)\n", $base[$i]->face(), dechex($base[$i]->pos()), $base[$i]->level()); 400 | // echo "new S2Cell(base[i])\n"; 401 | $cell = new S2Cell($base[$i]); 402 | // echo "neighbour cell: " . $cell . "\n"; 403 | $c = $this->newCandidate($cell); 404 | // if ($c !== null) 405 | // echo "addCandidato getInitialCandidates: " . $c->cell->id() . "\n"; 406 | $this->addCandidate($c); 407 | } 408 | 409 | // echo "\n\n\n"; 410 | 411 | return; 412 | } 413 | } 414 | // Default: start with all six cube faces. 415 | $face_cells = self::FACE_CELLS(); 416 | for ($face = 0; $face < 6; ++$face) { 417 | $c = $this->newCandidate($face_cells[$face]); 418 | echo "addCandidato getInitialCandidates_default: " . $c->cell->id() . "\n"; 419 | $this->addCandidate($c); 420 | } 421 | } 422 | 423 | /** Generates a covering and stores it in result. */ 424 | private function getCoveringInternal(S2Region $region) { 425 | // Strategy: Start with the 6 faces of the cube. Discard any 426 | // that do not intersect the shape. Then repeatedly choose the 427 | // largest cell that intersects the shape and subdivide it. 428 | // 429 | // result contains the cells that will be part of the output, while the 430 | // priority queue contains cells that we may still subdivide further. Cells 431 | // that are entirely contained within the region are immediately added to 432 | // the output, while cells that do not intersect the region are immediately 433 | // discarded. 434 | // Therefore pq_ only contains cells that partially intersect the region. 435 | // Candidates are prioritized first according to cell size (larger cells 436 | // first), then by the number of intersecting children they have (fewest 437 | // children first), and then by the number of fully contained children 438 | // (fewest children first). 439 | 440 | $tmp1 = $this->candidateQueue->isEmpty(); 441 | if (!($tmp1 && count($this->result) == 0)) throw new Exception(); 442 | 443 | $this->region = $region; 444 | $this->candidatesCreatedCounter = 0; 445 | 446 | $this->getInitialCandidates(); 447 | while (!$this->candidateQueue->isEmpty() && (!$this->interiorCovering || $this->result->size() < $this->maxCells)) { 448 | $candidate = $this->candidateQueue->extract(); 449 | 450 | // logger.info("Pop: " + candidate.cell.id()); 451 | // echo "Pop: " . $candidate . "\n"; 452 | if ($candidate->cell->level() < $this->minLevel || $candidate->numChildren == 1 453 | || $this->result->size() + ($this->interiorCovering ? 0 : $this->candidateQueue->size()) + $candidate->numChildren <= $this->maxCells) { 454 | // Expand this candidate into its children. 455 | for ($i = 0; $i < $candidate->numChildren; ++$i) { 456 | $c = $candidate->children[$i]; 457 | // echo "call addCandidate on $c\n"; 458 | $this->addCandidate($c); 459 | } 460 | } else if ($this->interiorCovering) { 461 | // Do nothing 462 | } else { 463 | $candidate->isTerminal = true; 464 | $this->addCandidate($candidate); 465 | } 466 | } 467 | 468 | unset($this->candidateQueue); 469 | $this->candidateQueue = new SplPriorityQueue(); 470 | $this->region = null; 471 | } 472 | 473 | /** 474 | * Given a region and a starting cell, return the set of all the 475 | * edge-connected cells at the same level that intersect "region". The output 476 | * cells are returned in arbitrary order. 477 | *#/ 478 | * private static void floodFill(S2Region region, S2CellId start, ArrayList output) { 479 | * HashSet all = new HashSet(); 480 | * ArrayList frontier = new ArrayList(); 481 | * output.clear(); 482 | * all.add(start); 483 | * frontier.add(start); 484 | * while (!frontier.isEmpty()) { 485 | * S2CellId id = frontier.get(frontier.size() - 1); 486 | * frontier.remove(frontier.size() - 1); 487 | * if (!region.mayIntersect(new S2Cell(id))) { 488 | * continue; 489 | * } 490 | * output.add(id); 491 | * 492 | * S2CellId[] neighbors = new S2CellId[4]; 493 | * id.getEdgeNeighbors(neighbors); 494 | * for (int edge = 0; edge < 4; ++edge) { 495 | * S2CellId nbr = neighbors[edge]; 496 | * boolean hasNbr = all.contains(nbr); 497 | * if (!all.contains(nbr)) { 498 | * frontier.add(nbr); 499 | * all.add(nbr); 500 | * } 501 | * } 502 | * } 503 | * } 504 | * 505 | */ 506 | } 507 | 508 | class Candidate { 509 | public $cell; 510 | public $isTerminal; // Cell should not be expanded further. 511 | public $numChildren = 0; // Number of children that intersect the region. 512 | public $children; // Actual size may be 0, 4, 16, or 64 elements. 513 | public function __toString() { 514 | return sprintf("[%s t:%s n:%d]", $this->cell, $this->isTerminal ? 'true' : 'false', $this->numChildren); 515 | } 516 | } 517 | -------------------------------------------------------------------------------- /S2CellUnion.php: -------------------------------------------------------------------------------- 1 | cellIds) { 11 | initRawCellIds(cellIds); 12 | normalize(); 13 | } 14 | 15 | /** 16 | * Populates a cell union with the given S2CellIds or 64-bit cells ids, and 17 | * then calls Normalize(). The InitSwap() version takes ownership of the 18 | * vector data without copying and clears the given vector. These methods may 19 | * be called multiple times. 20 | *#/ 21 | public void initFromIds(ArrayList cellIds) { 22 | initRawIds(cellIds); 23 | normalize(); 24 | } 25 | */ 26 | public function initSwap($cellIds) { 27 | $this->initRawSwap($cellIds); 28 | $this->normalize(); 29 | } 30 | 31 | /* 32 | public void initRawCellIds(ArrayList cellIds) { 33 | this.cellIds = cellIds; 34 | } 35 | 36 | public void initRawIds(ArrayList cellIds) { 37 | int size = cellIds.size(); 38 | this.cellIds = new ArrayList(size); 39 | for (Long id : cellIds) { 40 | this.cellIds.add(new S2CellId(id)); 41 | } 42 | } 43 | 44 | /** 45 | * Like Init(), but does not call Normalize(). The cell union *must* be 46 | * normalized before doing any calculations with it, so it is the caller's 47 | * responsibility to make sure that the input is normalized. This method is 48 | * useful when converting cell unions to another representation and back. 49 | * These methods may be called multiple times. 50 | */ 51 | public function initRawSwap($cellIds) { 52 | $this->cellIds = $cellIds; 53 | // cellIds.clear(); 54 | } 55 | 56 | public function size() { 57 | return count($this->cellIds); 58 | } 59 | 60 | /** Convenience methods for accessing the individual cell ids. *#/ 61 | * public S2CellId cellId(int i) { 62 | * return cellIds.get(i); 63 | * } 64 | * 65 | * /** Enable iteration over the union's cells. *#/ 66 | * @Override 67 | * public Iterator iterator() { 68 | * return cellIds.iterator(); 69 | * } 70 | * 71 | * /** Direct access to the underlying vector for iteration . *#/ 72 | * public ArrayList cellIds() { 73 | * return cellIds; 74 | * } 75 | * 76 | * /** 77 | * Replaces "output" with an expanded version of the cell union where any 78 | * cells whose level is less than "min_level" or where (level - min_level) is 79 | * not a multiple of "level_mod" are replaced by their children, until either 80 | * both of these conditions are satisfied or the maximum level is reached. 81 | * 82 | * This method allows a covering generated by S2RegionCoverer using 83 | * min_level() or level_mod() constraints to be stored as a normalized cell 84 | * union (which allows various geometric computations to be done) and then 85 | * converted back to the original list of cell ids that satisfies the desired 86 | * constraints. 87 | */ 88 | public function denormalize($minLevel, $levelMod, &$output) { 89 | // assert (minLevel >= 0 && minLevel <= S2CellId.MAX_LEVEL); 90 | // assert (levelMod >= 1 && levelMod <= 3); 91 | 92 | $output = array(); 93 | /** @var $id S2CellId */ 94 | foreach ($this->cellIds as $id) { 95 | $level = $id->level(); 96 | $newLevel = max($minLevel, $level); 97 | if ($levelMod > 1) { 98 | // Round up so that (new_level - min_level) is a multiple of level_mod. 99 | // (Note that S2CellId::kMaxLevel is a multiple of 1, 2, and 3.) 100 | $newLevel += (S2CellId::MAX_LEVEL - ($newLevel - $minLevel)) % $levelMod; 101 | $newLevel = min(S2CellId::MAX_LEVEL, $newLevel); 102 | } 103 | if ($newLevel == $level) { 104 | $output[] = $id; 105 | } else { 106 | $end = $id->childEnd($newLevel); 107 | for ($id = $id->childBegin($newLevel); !$id->equals($end); $id = $id->next()) { 108 | $output[] = $id; 109 | } 110 | } 111 | } 112 | } 113 | 114 | /** 115 | * If there are more than "excess" elements of the cell_ids() vector that are 116 | * allocated but unused, reallocate the array to eliminate the excess space. 117 | * This reduces memory usage when many cell unions need to be held in memory 118 | * at once. 119 | *#/ 120 | * public void pack() { 121 | * cellIds.trimToSize(); 122 | * } 123 | * 124 | * 125 | * /** 126 | * Return true if the cell union contains the given cell id. Containment is 127 | * defined with respect to regions, e.g. a cell contains its 4 children. This 128 | * is a fast operation (logarithmic in the size of the cell union). 129 | *#/ 130 | * public boolean contains(S2CellId id) { 131 | * // This function requires that Normalize has been called first. 132 | * // 133 | * // This is an exact test. Each cell occupies a linear span of the S2 134 | * // space-filling curve, and the cell id is simply the position at the center 135 | * // of this span. The cell union ids are sorted in increasing order along 136 | * // the space-filling curve. So we simply find the pair of cell ids that 137 | * // surround the given cell id (using binary search). There is containment 138 | * // if and only if one of these two cell ids contains this cell. 139 | * 140 | * int pos = Collections.binarySearch(cellIds, id); 141 | * if (pos < 0) { 142 | * pos = -pos - 1; 143 | * } 144 | * if (pos < cellIds.size() && cellIds.get(pos).rangeMin().lessOrEquals(id)) { 145 | * return true; 146 | * } 147 | * return pos != 0 && cellIds.get(pos - 1).rangeMax().greaterOrEquals(id); 148 | * } 149 | * 150 | * /** 151 | * Return true if the cell union intersects the given cell id. This is a fast 152 | * operation (logarithmic in the size of the cell union). 153 | *#/ 154 | * public boolean intersects(S2CellId id) { 155 | * // This function requires that Normalize has been called first. 156 | * // This is an exact test; see the comments for Contains() above. 157 | * int pos = Collections.binarySearch(cellIds, id); 158 | * 159 | * if (pos < 0) { 160 | * pos = -pos - 1; 161 | * } 162 | * 163 | * 164 | * if (pos < cellIds.size() && cellIds.get(pos).rangeMin().lessOrEquals(id.rangeMax())) { 165 | * return true; 166 | * } 167 | * return pos != 0 && cellIds.get(pos - 1).rangeMax().greaterOrEquals(id.rangeMin()); 168 | * } 169 | * 170 | * public boolean contains(S2CellUnion that) { 171 | * // TODO(kirilll?): A divide-and-conquer or alternating-skip-search approach 172 | * // may be significantly faster in both the average and worst case. 173 | * for (S2CellId id : that) { 174 | * if (!this.contains(id)) { 175 | * return false; 176 | * } 177 | * } 178 | * return true; 179 | * } 180 | * 181 | * /** This is a fast operation (logarithmic in the size of the cell union). *#/ 182 | * @Override 183 | * public boolean contains(S2Cell cell) { 184 | * return contains(cell.id()); 185 | * } 186 | * 187 | * /** 188 | * Return true if this cell union contain/intersects the given other cell 189 | * union. 190 | *#/ 191 | * public boolean intersects(S2CellUnion union) { 192 | * // TODO(kirilll?): A divide-and-conquer or alternating-skip-search approach 193 | * // may be significantly faster in both the average and worst case. 194 | * for (S2CellId id : union) { 195 | * if (intersects(id)) { 196 | * return true; 197 | * } 198 | * } 199 | * return false; 200 | * } 201 | * 202 | * public void getUnion(S2CellUnion x, S2CellUnion y) { 203 | * // assert (x != this && y != this); 204 | * cellIds.clear(); 205 | * cellIds.ensureCapacity(x.size() + y.size()); 206 | * cellIds.addAll(x.cellIds); 207 | * cellIds.addAll(y.cellIds); 208 | * normalize(); 209 | * } 210 | * 211 | * /** 212 | * Specialized version of GetIntersection() that gets the intersection of a 213 | * cell union with the given cell id. This can be useful for "splitting" a 214 | * cell union into chunks. 215 | *#/ 216 | * public void getIntersection(S2CellUnion x, S2CellId id) { 217 | * // assert (x != this); 218 | * cellIds.clear(); 219 | * if (x.contains(id)) { 220 | * cellIds.add(id); 221 | * } else { 222 | * int pos = Collections.binarySearch(x.cellIds, id.rangeMin()); 223 | * 224 | * if (pos < 0) { 225 | * pos = -pos - 1; 226 | * } 227 | * 228 | * S2CellId idmax = id.rangeMax(); 229 | * int size = x.cellIds.size(); 230 | * while (pos < size && x.cellIds.get(pos).lessOrEquals(idmax)) { 231 | * cellIds.add(x.cellIds.get(pos++)); 232 | * } 233 | * } 234 | * } 235 | * 236 | * /** 237 | * Initialize this cell union to the union or intersection of the two given 238 | * cell unions. Requires: x != this and y != this. 239 | *#/ 240 | * public void getIntersection(S2CellUnion x, S2CellUnion y) { 241 | * // assert (x != this && y != this); 242 | * 243 | * // This is a fairly efficient calculation that uses binary search to skip 244 | * // over sections of both input vectors. It takes constant time if all the 245 | * // cells of "x" come before or after all the cells of "y" in S2CellId order. 246 | * 247 | * cellIds.clear(); 248 | * 249 | * int i = 0; 250 | * int j = 0; 251 | * 252 | * while (i < x.cellIds.size() && j < y.cellIds.size()) { 253 | * S2CellId imin = x.cellId(i).rangeMin(); 254 | * S2CellId jmin = y.cellId(j).rangeMin(); 255 | * if (imin.greaterThan(jmin)) { 256 | * // Either j->contains(*i) or the two cells are disjoint. 257 | * if (x.cellId(i).lessOrEquals(y.cellId(j).rangeMax())) { 258 | * cellIds.add(x.cellId(i++)); 259 | * } else { 260 | * // Advance "j" to the first cell possibly contained by *i. 261 | * j = indexedBinarySearch(y.cellIds, imin, j + 1); 262 | * // The previous cell *(j-1) may now contain *i. 263 | * if (x.cellId(i).lessOrEquals(y.cellId(j - 1).rangeMax())) { 264 | * --j; 265 | * } 266 | * } 267 | * } else if (jmin.greaterThan(imin)) { 268 | * // Identical to the code above with "i" and "j" reversed. 269 | * if (y.cellId(j).lessOrEquals(x.cellId(i).rangeMax())) { 270 | * cellIds.add(y.cellId(j++)); 271 | * } else { 272 | * i = indexedBinarySearch(x.cellIds, jmin, i + 1); 273 | * if (y.cellId(j).lessOrEquals(x.cellId(i - 1).rangeMax())) { 274 | * --i; 275 | * } 276 | * } 277 | * } else { 278 | * // "i" and "j" have the same range_min(), so one contains the other. 279 | * if (x.cellId(i).lessThan(y.cellId(j))) { 280 | * cellIds.add(x.cellId(i++)); 281 | * } else { 282 | * cellIds.add(y.cellId(j++)); 283 | * } 284 | * } 285 | * } 286 | * // The output is generated in sorted order, and there should not be any 287 | * // cells that can be merged (provided that both inputs were normalized). 288 | * // assert (!normalize()); 289 | * } 290 | * 291 | * /** 292 | * Just as normal binary search, except that it allows specifying the starting 293 | * value for the lower bound. 294 | * 295 | * @return The position of the searched element in the list (if found), or the 296 | * position where the element could be inserted without violating the 297 | * order. 298 | *#/ 299 | * private int indexedBinarySearch(List l, S2CellId key, int low) { 300 | * int high = l.size() - 1; 301 | * 302 | * while (low <= high) { 303 | * int mid = (low + high) >> 1; 304 | * S2CellId midVal = l.get(mid); 305 | * int cmp = midVal.compareTo(key); 306 | * 307 | * if (cmp < 0) { 308 | * low = mid + 1; 309 | * } else if (cmp > 0) { 310 | * high = mid - 1; 311 | * } else { 312 | * return mid; // key found 313 | * } 314 | * } 315 | * return low; // key not found 316 | * } 317 | * 318 | * /** 319 | * Expands the cell union such that it contains all cells of the given level 320 | * that are adjacent to any cell of the original union. Two cells are defined 321 | * as adjacent if their boundaries have any points in common, i.e. most cells 322 | * have 8 adjacent cells (not counting the cell itself). 323 | * 324 | * Note that the size of the output is exponential in "level". For example, 325 | * if level == 20 and the input has a cell at level 10, there will be on the 326 | * order of 4000 adjacent cells in the output. For most applications the 327 | * Expand(min_fraction, min_distance) method below is easier to use. 328 | *#/ 329 | * public void expand(int level) { 330 | * ArrayList output = new ArrayList(); 331 | * long levelLsb = S2CellId.lowestOnBitForLevel(level); 332 | * int i = size() - 1; 333 | * do { 334 | * S2CellId id = cellId(i); 335 | * if (id.lowestOnBit() < levelLsb) { 336 | * id = id.parent(level); 337 | * // Optimization: skip over any cells contained by this one. This is 338 | * // especially important when very small regions are being expanded. 339 | * while (i > 0 && id.contains(cellId(i - 1))) { 340 | * --i; 341 | * } 342 | * } 343 | * output.add(id); 344 | * id.getAllNeighbors(level, output); 345 | * } while (--i >= 0); 346 | * initSwap(output); 347 | * } 348 | * 349 | * /** 350 | * Expand the cell union such that it contains all points whose distance to 351 | * the cell union is at most minRadius, but do not use cells that are more 352 | * than maxLevelDiff levels higher than the largest cell in the input. The 353 | * second parameter controls the tradeoff between accuracy and output size 354 | * when a large region is being expanded by a small amount (e.g. expanding 355 | * Canada by 1km). 356 | * 357 | * For example, if maxLevelDiff == 4, the region will always be expanded by 358 | * approximately 1/16 the width of its largest cell. Note that in the worst 359 | * case, the number of cells in the output can be up to 4 * (1 + 2 ** 360 | * maxLevelDiff) times larger than the number of cells in the input. 361 | *#/ 362 | * public void expand(S1Angle minRadius, int maxLevelDiff) { 363 | * int minLevel = S2CellId.MAX_LEVEL; 364 | * for (S2CellId id : this) { 365 | * minLevel = Math.min(minLevel, id.level()); 366 | * } 367 | * // Find the maximum level such that all cells are at least "min_radius" 368 | * // wide. 369 | * int radiusLevel = S2Projections.MIN_WIDTH.getMaxLevel(minRadius.radians()); 370 | * if (radiusLevel == 0 && minRadius.radians() > S2Projections.MIN_WIDTH.getValue(0)) { 371 | * // The requested expansion is greater than the width of a face cell. 372 | * // The easiest way to handle this is to expand twice. 373 | * expand(0); 374 | * } 375 | * expand(Math.min(minLevel + maxLevelDiff, radiusLevel)); 376 | * } 377 | * 378 | * @Override 379 | * public S2Region clone() { 380 | * S2CellUnion copy = new S2CellUnion(); 381 | * copy.initRawCellIds(Lists.newArrayList(cellIds)); 382 | * return copy; 383 | * } 384 | * 385 | * @Override 386 | * public S2Cap getCapBound() { 387 | * // Compute the approximate centroid of the region. This won't produce the 388 | * // bounding cap of minimal area, but it should be close enough. 389 | * if (cellIds.isEmpty()) { 390 | * return S2Cap.empty(); 391 | * } 392 | * S2Point centroid = new S2Point(0, 0, 0); 393 | * for (S2CellId id : this) { 394 | * double area = S2Cell.averageArea(id.level()); 395 | * centroid = S2Point.add(centroid, S2Point.mul(id.toPoint(), area)); 396 | * } 397 | * if (centroid.equals(new S2Point(0, 0, 0))) { 398 | * centroid = new S2Point(1, 0, 0); 399 | * } else { 400 | * centroid = S2Point.normalize(centroid); 401 | * } 402 | * 403 | * // Use the centroid as the cap axis, and expand the cap angle so that it 404 | * // contains the bounding caps of all the individual cells. Note that it is 405 | * // *not* sufficient to just bound all the cell vertices because the bounding 406 | * // cap may be concave (i.e. cover more than one hemisphere). 407 | * S2Cap cap = S2Cap.fromAxisHeight(centroid, 0); 408 | * for (S2CellId id : this) { 409 | * cap = cap.addCap(new S2Cell(id).getCapBound()); 410 | * } 411 | * return cap; 412 | * } 413 | * 414 | * @Override 415 | * public S2LatLngRect getRectBound() { 416 | * S2LatLngRect bound = S2LatLngRect.empty(); 417 | * for (S2CellId id : this) { 418 | * bound = bound.union(new S2Cell(id).getRectBound()); 419 | * } 420 | * return bound; 421 | * } 422 | * 423 | * 424 | * /** This is a fast operation (logarithmic in the size of the cell union). *#/ 425 | * @Override 426 | * public boolean mayIntersect(S2Cell cell) { 427 | * return intersects(cell.id()); 428 | * } 429 | * 430 | * /** 431 | * The point 'p' does not need to be normalized. This is a fast operation 432 | * (logarithmic in the size of the cell union). 433 | *#/ 434 | * public boolean contains(S2Point p) { 435 | * return contains(S2CellId.fromPoint(p)); 436 | * 437 | * } 438 | * 439 | * /** 440 | * The number of leaf cells covered by the union. 441 | * This will be no more than 6*2^60 for the whole sphere. 442 | * 443 | * @return the number of leaf cells covered by the union 444 | *#/ 445 | * public long leafCellsCovered() { 446 | * long numLeaves = 0; 447 | * for (S2CellId cellId : cellIds) { 448 | * int invertedLevel = S2CellId.MAX_LEVEL - cellId.level(); 449 | * numLeaves += (1L << (invertedLevel << 1)); 450 | * } 451 | * return numLeaves; 452 | * } 453 | * 454 | * 455 | * /** 456 | * Approximate this cell union's area by summing the average area of 457 | * each contained cell's average area, using {@link S2Cell#averageArea()}. 458 | * This is equivalent to the number of leaves covered, multiplied by 459 | * the average area of a leaf. 460 | * Note that {@link S2Cell#averageArea()} does not take into account 461 | * distortion of cell, and thus may be off by up to a factor of 1.7. 462 | * NOTE: Since this is proportional to LeafCellsCovered(), it is 463 | * always better to use the other function if all you care about is 464 | * the relative average area between objects. 465 | * 466 | * @return the sum of the average area of each contained cell's average area 467 | *#/ 468 | * public double averageBasedArea() { 469 | * return S2Cell.averageArea(S2CellId.MAX_LEVEL) * leafCellsCovered(); 470 | * } 471 | * 472 | * /** 473 | * Calculates this cell union's area by summing the approximate area for each 474 | * contained cell, using {@link S2Cell#approxArea()}. 475 | * 476 | * @return approximate area of the cell union 477 | *#/ 478 | * public double approxArea() { 479 | * double area = 0; 480 | * for (S2CellId cellId : cellIds) { 481 | * area += new S2Cell(cellId).approxArea(); 482 | * } 483 | * return area; 484 | * } 485 | * 486 | * /** 487 | * Calculates this cell union's area by summing the exact area for each 488 | * contained cell, using the {@link S2Cell#exactArea()}. 489 | * 490 | * @return the exact area of the cell union 491 | *#/ 492 | * public double exactArea() { 493 | * double area = 0; 494 | * for (S2CellId cellId : cellIds) { 495 | * area += new S2Cell(cellId).exactArea(); 496 | * } 497 | * return area; 498 | * } 499 | * 500 | * /** Return true if two cell unions are identical. *#/ 501 | * @Override 502 | * public boolean equals(Object that) { 503 | * if (!(that instanceof S2CellUnion)) { 504 | * return false; 505 | * } 506 | * S2CellUnion union = (S2CellUnion) that; 507 | * return this.cellIds.equals(union.cellIds); 508 | * } 509 | * 510 | * @Override 511 | * public int hashCode() { 512 | * int value = 17; 513 | * for (S2CellId id : this) { 514 | * value = 37 * value + id.hashCode(); 515 | * } 516 | * return value; 517 | * } 518 | * 519 | * /** 520 | * Normalizes the cell union by discarding cells that are contained by other 521 | * cells, replacing groups of 4 child cells by their parent cell whenever 522 | * possible, and sorting all the cell ids in increasing order. Returns true if 523 | * the number of cells was reduced. 524 | * 525 | * This method *must* be called before doing any calculations on the cell 526 | * union, such as Intersects() or Contains(). 527 | * 528 | * @return true if the normalize operation had any effect on the cell union, 529 | * false if the union was already normalized 530 | */ 531 | public function normalize() { 532 | // Optimize the representation by looking for cases where all subcells 533 | // of a parent cell are present. 534 | 535 | /** @var S2CellId[] $output */ 536 | $output = array(); 537 | sort($this->cellIds); 538 | 539 | // echo "\n\n\n"; 540 | 541 | // foreach ($this->cellIds as $id) { 542 | // echo $id . "\n"; 543 | // } 544 | 545 | foreach ($this->cellIds as $id) { 546 | $size = count($output); 547 | // Check whether this cell is contained by the previous cell. 548 | if ($size && $output[$size - 1]->contains($id)) { 549 | continue; 550 | } 551 | 552 | // Discard any previous cells contained by this cell. 553 | while (!empty($output) && $id->contains($output[count($output) - 1])) { 554 | unset($output[count($output) - 1]); 555 | } 556 | 557 | // Check whether the last 3 elements of "output" plus "id" can be 558 | // collapsed into a single parent cell. 559 | while (count($output) >= 3) { 560 | $size = count($output); 561 | // A necessary (but not sufficient) condition is that the XOR of the 562 | // four cells must be zero. This is also very fast to test. 563 | if (($output[$size - 3]->id() ^ $output[$size - 2]->id() ^ $output[$size - 1]->id()) != $id->id()) { 564 | break; 565 | } 566 | 567 | // Now we do a slightly more expensive but exact test. First, compute a 568 | // mask that blocks out the two bits that encode the child position of 569 | // "id" with respect to its parent, then check that the other three 570 | // children all agree with "mask. 571 | $mask = $id->lowestOnBit() << 1; 572 | $mask = ~($mask + ($mask << 1)); 573 | $idMasked = ($id->id() & $mask); 574 | if (($output[$size - 3]->id() & $mask) != $idMasked 575 | || ($output[$size - 2]->id() & $mask) != $idMasked 576 | || ($output[$size - 1]->id() & $mask) != $idMasked || $id->isFace()) { 577 | break; 578 | } 579 | 580 | // Replace four children by their parent cell. 581 | unset($output[$size - 1]); 582 | unset($output[$size - 2]); 583 | unset($output[$size - 3]); 584 | $id = $id->parent(); 585 | } 586 | $size = count($output); 587 | $output[$size] = $id; 588 | } 589 | 590 | // echo "===\n"; 591 | // foreach ($output as $id) { 592 | // echo $id . "\n"; 593 | // } 594 | // echo "\n"; 595 | 596 | if (count($output) < $this->size()) { 597 | $this->initRawSwap($output); 598 | return true; 599 | } 600 | return false; 601 | } 602 | } 603 | -------------------------------------------------------------------------------- /S2PolygonBuilder.php: -------------------------------------------------------------------------------- 1 | > edges; 42 | 43 | /** 44 | * Default constructor for well-behaved polygons. Uses the DIRECTED_XOR 45 | * options. 46 | *#/ 47 | public S2PolygonBuilder() { 48 | this(Options.DIRECTED_XOR); 49 | 50 | } 51 | 52 | public S2PolygonBuilder(Options options) { 53 | this.options = options; 54 | this.edges = Maps.newHashMap(); 55 | } 56 | 57 | public enum Options { 58 | 59 | /** 60 | * These are the options that should be used for assembling well-behaved 61 | * input data into polygons. All edges should be directed such that "shells" 62 | * and "holes" have opposite orientations (typically CCW shells and 63 | * clockwise holes), unless it is known that shells and holes do not share 64 | * any edges. 65 | *#/ 66 | DIRECTED_XOR(false, true), 67 | 68 | /** 69 | * These are the options that should be used for assembling polygons that do 70 | * not follow the conventions above, e.g. where edge directions may vary 71 | * within a single loop, or shells and holes are not oppositely oriented. 72 | *#/ 73 | UNDIRECTED_XOR(true, true), 74 | 75 | /** 76 | * These are the options that should be used for assembling edges where the 77 | * desired output is a collection of loops rather than a polygon, and edges 78 | * may occur more than once. Edges are treated as undirected and are not 79 | * XORed together, in particular, adding edge A->B also adds B->A. 80 | *#/ 81 | UNDIRECTED_UNION(true, false), 82 | 83 | /** 84 | * Finally, select this option when the desired output is a collection of 85 | * loops rather than a polygon, but your input edges are directed and you do 86 | * not want reverse edges to be added implicitly as above. 87 | *#/ 88 | DIRECTED_UNION(false, false); 89 | 90 | private boolean undirectedEdges; 91 | private boolean xorEdges; 92 | private boolean validate; 93 | private S1Angle mergeDistance; 94 | 95 | private Options(boolean undirectedEdges, boolean xorEdges) { 96 | this.undirectedEdges = undirectedEdges; 97 | this.xorEdges = xorEdges; 98 | this.validate = false; 99 | this.mergeDistance = S1Angle.radians(0); 100 | } 101 | 102 | /** 103 | * If "undirected_edges" is false, then the input is assumed to consist of 104 | * edges that can be assembled into oriented loops without reversing any of 105 | * the edges. Otherwise, "undirected_edges" should be set to true. 106 | *#/ 107 | public boolean getUndirectedEdges() { 108 | return undirectedEdges; 109 | } 110 | 111 | /** 112 | * If "xor_edges" is true, then any duplicate edge pairs are removed. This 113 | * is useful for computing the union of a collection of polygons whose 114 | * interiors are disjoint but whose boundaries may share some common edges 115 | * (e.g. computing the union of South Africa, Lesotho, and Swaziland). 116 | * 117 | * Note that for directed edges, a "duplicate edge pair" consists of an 118 | * edge and its corresponding reverse edge. This means that either (a) 119 | * "shells" and "holes" must have opposite orientations, or (b) shells and 120 | * holes do not share edges. Otherwise undirected_edges() should be 121 | * specified. 122 | * 123 | * There are only two reasons to turn off xor_edges(): 124 | * 125 | * (1) assemblePolygon() will be called, and you want to assert that there 126 | * are no duplicate edge pairs in the input. 127 | * 128 | * (2) assembleLoops() will be called, and you want to keep abutting loops 129 | * separate in the output rather than merging their regions together (e.g. 130 | * assembling loops for Kansas City, KS and Kansas City, MO simultaneously). 131 | *#/ 132 | public boolean getXorEdges() { 133 | return xorEdges; 134 | } 135 | 136 | /** 137 | * Default value: false 138 | *#/ 139 | public boolean getValidate() { 140 | return validate; 141 | } 142 | 143 | /** 144 | * Default value: 0 145 | *#/ 146 | public S1Angle getMergeDistance() { 147 | return mergeDistance; 148 | } 149 | 150 | /** 151 | * If true, isValid() is called on all loops and polygons before 152 | * constructing them. If any loop is invalid (e.g. self-intersecting), it is 153 | * rejected and returned as a set of "unused edges". Any remaining valid 154 | * loops are kept. If the entire polygon is invalid (e.g. two loops 155 | * intersect), then all loops are rejected and returned as unused edges. 156 | *#/ 157 | public void setValidate(boolean validate) { 158 | this.validate = validate; 159 | } 160 | 161 | /** 162 | * If set to a positive value, all vertices that are separated by at most 163 | * this distance will be merged together. In addition, vertices that are 164 | * closer than this distance to a non-incident edge will be spliced into it 165 | * (TODO). 166 | * 167 | * The merging is done in such a way that all vertex-vertex and vertex-edge 168 | * distances in the output are greater than 'merge_distance'. 169 | * 170 | * This method is useful for assembling polygons out of input data where 171 | * vertices and/or edges may not be perfectly aligned. 172 | *#/ 173 | public void setMergeDistance(S1Angle mergeDistance) { 174 | this.mergeDistance = mergeDistance; 175 | } 176 | 177 | // Used for testing only 178 | void setUndirectedEdges(boolean undirectedEdges) { 179 | this.undirectedEdges = undirectedEdges; 180 | } 181 | 182 | // Used for testing only 183 | void setXorEdges(boolean xorEdges) { 184 | this.xorEdges = xorEdges; 185 | } 186 | } 187 | 188 | public Options options() { 189 | return options; 190 | } 191 | 192 | /** 193 | * Add the given edge to the polygon builder. This method should be used for 194 | * input data that may not follow S2 polygon conventions. Note that edges are 195 | * not allowed to cross each other. Also note that as a convenience, edges 196 | * where v0 == v1 are ignored. 197 | *#/ 198 | public void addEdge(S2Point v0, S2Point v1) { 199 | // If xor_edges is true, we look for an existing edge in the opposite 200 | // direction. We either delete that edge or insert a new one. 201 | 202 | if (v0.equals(v1)) { 203 | return; 204 | } 205 | 206 | if (options.getXorEdges()) { 207 | Multiset 208 | candidates = edges.get(v1); 209 | if (candidates != null && candidates.count(v0) > 0) { 210 | eraseEdge(v1, v0); 211 | return; 212 | } 213 | } 214 | 215 | if (edges.get(v0) == null) { 216 | edges.put(v0, HashMultiset. 217 | create()); 218 | } 219 | 220 | edges.get(v0).add(v1); 221 | if (options.getUndirectedEdges()) { 222 | if (edges.get(v1) == null) { 223 | edges.put(v1, HashMultiset. 224 | create()); 225 | } 226 | edges.get(v1).add(v0); 227 | } 228 | } 229 | 230 | /** 231 | * Add all edges in the given loop. If the sign() of the loop is negative 232 | * (i.e. this loop represents a hole), the reverse edges are added instead. 233 | * This implies that "shells" are CCW and "holes" are CW, as required for the 234 | * directed edges convention described above. 235 | * 236 | * This method does not take ownership of the loop. 237 | *#/ 238 | public void addLoop(S2Loop loop) { 239 | int sign = loop.sign(); 240 | for (int i = loop.numVertices(); i > 0; --i) { 241 | // Vertex indices need to be in the range [0, 2*num_vertices()-1]. 242 | addEdge(loop.vertex(i), loop.vertex(i + sign)); 243 | } 244 | } 245 | 246 | /** 247 | * Add all loops in the given polygon. Shells and holes are added with 248 | * opposite orientations as described for AddLoop(). This method does not take 249 | * ownership of the polygon. 250 | *#/ 251 | public void addPolygon(S2Polygon polygon) { 252 | for (int i = 0; i < polygon.numLoops(); ++i) { 253 | addLoop(polygon.loop(i)); 254 | } 255 | } 256 | 257 | /** 258 | * Assembles the given edges into as many non-crossing loops as possible. When 259 | * there is a choice about how to assemble the loops, then CCW loops are 260 | * preferred. Returns true if all edges were assembled. If "unused_edges" is 261 | * not NULL, it is initialized to the set of edges that could not be assembled 262 | * into loops. 263 | * 264 | * Note that if xor_edges() is false and duplicate edge pairs may be present, 265 | * then undirected_edges() should be specified unless all loops can be 266 | * assembled in a counter-clockwise direction. Otherwise this method may not 267 | * be able to assemble all loops due to its preference for CCW loops. 268 | * 269 | * This method resets the S2PolygonBuilder state so that it can be reused. 270 | *#/ 271 | public boolean assembleLoops(List 272 | loops, List 273 | unusedEdges) { 274 | if (options.getMergeDistance().radians() > 0) { 275 | mergeVertices(); 276 | } 277 | 278 | List 279 | dummyUnusedEdges = Lists.newArrayList(); 280 | if (unusedEdges == null) { 281 | unusedEdges = dummyUnusedEdges; 282 | } 283 | 284 | // We repeatedly choose an arbitrary edge and attempt to assemble a loop 285 | // starting from that edge. (This is always possible unless the input 286 | // includes extra edges that are not part of any loop.) 287 | 288 | unusedEdges.clear(); 289 | while (!edges.isEmpty()) { 290 | Map.Entry 291 | > edge = edges.entrySet().iterator().next(); 294 | 295 | S2Point v0 = edge.getKey(); 296 | S2Point v1 = edge.getValue().iterator().next(); 297 | 298 | S2Loop loop = assembleLoop(v0, v1, unusedEdges); 299 | if (loop == null) { 300 | continue; 301 | } 302 | 303 | // In the case of undirected edges, we may have assembled a clockwise 304 | // loop while trying to assemble a CCW loop. To fix this, we assemble 305 | // a new loop starting with an arbitrary edge in the reverse direction. 306 | // This is guaranteed to assemble a loop that is interior to the previous 307 | // one and will therefore eventually terminate. 308 | 309 | while (options.getUndirectedEdges() && !loop.isNormalized()) { 310 | loop = assembleLoop(loop.vertex(1), loop.vertex(0), unusedEdges); 311 | } 312 | loops.add(loop); 313 | eraseLoop(loop, loop.numVertices()); 314 | } 315 | return unusedEdges.isEmpty(); 316 | } 317 | 318 | /** 319 | * Like AssembleLoops, but normalizes all the loops so that they enclose less 320 | * than half the sphere, and then assembles the loops into a polygon. 321 | * 322 | * For this method to succeed, there should be no duplicate edges in the 323 | * input. If this is not known to be true, then the "xor_edges" option should 324 | * be set (which is true by default). 325 | * 326 | * Note that S2Polygons cannot represent arbitrary regions on the sphere, 327 | * because of the limitation that no loop encloses more than half of the 328 | * sphere. For example, an S2Polygon cannot represent a 100km wide band around 329 | * the equator. In such cases, this method will return the *complement* of the 330 | * expected region. So for example if all the world's coastlines were 331 | * assembled, the output S2Polygon would represent the land area (irrespective 332 | * of the input edge or loop orientations). 333 | *#/ 334 | public boolean assemblePolygon(S2Polygon polygon, List 335 | unusedEdges) { 336 | List 337 | loops = Lists.newArrayList(); 338 | boolean success = assembleLoops(loops, unusedEdges); 339 | 340 | // If edges are undirected, then all loops are already CCW. Otherwise we 341 | // need to make sure the loops are normalized. 342 | if (!options.getUndirectedEdges()) { 343 | for (int i = 0; i < loops.size(); ++i) { 344 | loops.get(i).normalize(); 345 | } 346 | } 347 | if (options.getValidate() && !S2Polygon.isValid(loops)) { 348 | if (unusedEdges != null) { 349 | for (S2Loop loop : loops) { 350 | rejectLoop(loop, loop.numVertices(), unusedEdges); 351 | } 352 | } 353 | return false; 354 | } 355 | polygon.init(loops); 356 | return success; 357 | } 358 | 359 | /** 360 | * Convenience method for when you don't care about unused edges. 361 | *#/ 362 | public S2Polygon assemblePolygon() { 363 | S2Polygon polygon = new S2Polygon(); 364 | List 365 | unusedEdges = Lists.newArrayList(); 366 | 367 | assemblePolygon(polygon, unusedEdges); 368 | 369 | return polygon; 370 | } 371 | 372 | // Debugging functions: 373 | 374 | protected void dumpEdges(S2Point v0) { 375 | log.info(v0.toString()); 376 | Multiset 377 | vset = edges.get(v0); 378 | if (vset != null) { 379 | for (S2Point v : vset) { 380 | log.info(" " + v.toString()); 381 | } 382 | } 383 | } 384 | 385 | protected void dump() { 386 | for (S2Point v : edges.keySet()) { 387 | dumpEdges(v); 388 | } 389 | } 390 | 391 | private void eraseEdge(S2Point v0, S2Point v1) { 392 | // Note that there may be more than one copy of an edge if we are not XORing 393 | // them, so a VertexSet is a multiset. 394 | 395 | Multiset 396 | vset = edges.get(v0); 397 | // assert (vset.count(v1) > 0); 398 | vset.remove(v1); 399 | if (vset.isEmpty()) { 400 | edges.remove(v0); 401 | } 402 | 403 | if (options.getUndirectedEdges()) { 404 | vset = edges.get(v1); 405 | // assert (vset.count(v0) > 0); 406 | vset.remove(v0); 407 | if (vset.isEmpty()) { 408 | edges.remove(v1); 409 | } 410 | } 411 | } 412 | 413 | private void eraseLoop(List 414 | v, int n) { 415 | for (int i = n - 1, j = 0; j < n; i = j++) { 416 | eraseEdge(v.get(i), v.get(j)); 417 | } 418 | } 419 | 420 | private void eraseLoop(S2Loop v, int n) { 421 | for (int i = n - 1, j = 0; j < n; i = j++) { 422 | eraseEdge(v.vertex(i), v.vertex(j)); 423 | } 424 | } 425 | 426 | /** 427 | * We start at the given edge and assemble a loop taking left turns whenever 428 | * possible. We stop the loop as soon as we encounter any vertex that we have 429 | * seen before *except* for the first vertex (v0). This ensures that only CCW 430 | * loops are constructed when possible. 431 | *#/ 432 | private S2Loop assembleLoop(S2Point v0, S2Point v1, List 433 | unusedEdges) { 434 | 435 | // The path so far. 436 | List 437 | path = Lists.newArrayList(); 438 | 439 | // Maps a vertex to its index in "path". 440 | Map 441 | index = Maps.newHashMap(); 443 | path.add(v0); 444 | path.add(v1); 445 | 446 | index.put(v1, 1); 447 | 448 | while (path.size() >= 2) { 449 | // Note that "v0" and "v1" become invalid if "path" is modified. 450 | v0 = path.get(path.size() - 2); 451 | v1 = path.get(path.size() - 1); 452 | 453 | S2Point v2 = null; 454 | boolean v2Found = false; 455 | Multiset 456 | vset = edges.get(v1); 457 | if (vset != null) { 458 | for (S2Point v : vset) { 459 | // We prefer the leftmost outgoing edge, ignoring any reverse edges. 460 | if (v.equals(v0)) { 461 | continue; 462 | } 463 | if (!v2Found || S2.orderedCCW(v0, v2, v, v1)) { 464 | v2 = v; 465 | } 466 | v2Found = true; 467 | } 468 | } 469 | if (!v2Found) { 470 | // We've hit a dead end. Remove this edge and backtrack. 471 | unusedEdges.add(new S2Edge(v0, v1)); 472 | eraseEdge(v0, v1); 473 | index.remove(v1); 474 | path.remove(path.size() - 1); 475 | } else if (index.get(v2) == null) { 476 | // This is the first time we've visited this vertex. 477 | index.put(v2, path.size()); 478 | path.add(v2); 479 | } else { 480 | // We've completed a loop. Throw away any initial vertices that 481 | // are not part of the loop. 482 | path = path.subList(index.get(v2), path.size()); 483 | 484 | if (options.getValidate() && !S2Loop.isValid(path)) { 485 | // We've constructed a loop that crosses itself, which can only happen 486 | // if there is bad input data. Throw away the whole loop. 487 | rejectLoop(path, path.size(), unusedEdges); 488 | eraseLoop(path, path.size()); 489 | return null; 490 | } 491 | return new S2Loop(path); 492 | } 493 | } 494 | return null; 495 | } 496 | 497 | /** Erases all edges of the given loop and marks them as unused. *#/ 498 | private void rejectLoop(S2Loop v, int n, List 499 | unusedEdges) { 500 | for (int i = n - 1, j = 0; j < n; i = j++) { 501 | unusedEdges.add(new S2Edge(v.vertex(i), v.vertex(j))); 502 | } 503 | } 504 | 505 | /** Erases all edges of the given loop and marks them as unused. *#/ 506 | private void rejectLoop(List 507 | v, int n, List 508 | unusedEdges) { 509 | for (int i = n - 1, j = 0; j < n; i = j++) { 510 | unusedEdges.add(new S2Edge(v.get(i), v.get(j))); 511 | } 512 | } 513 | 514 | /** Moves a set of vertices from old to new positions. *#/ 515 | private void moveVertices(Map 516 | mergeMap) { 518 | if (mergeMap.isEmpty()) { 519 | return; 520 | } 521 | 522 | // We need to copy the set of edges affected by the move, since 523 | // this.edges_could be reallocated when we start modifying it. 524 | List 525 | edgesCopy = Lists.newArrayList(); 526 | for (Map.Entry 527 | > edge : this.edges.entrySet()) { 530 | S2Point v0 = edge.getKey(); 531 | Multiset 532 | vset = edge.getValue(); 533 | for (S2Point v1 : vset) { 534 | if (mergeMap.get(v0) != null || mergeMap.get(v1) != null) { 535 | 536 | // We only need to modify one copy of each undirected edge. 537 | if (!options.getUndirectedEdges() || v0.lessThan(v1)) { 538 | edgesCopy.add(new S2Edge(v0, v1)); 539 | } 540 | } 541 | } 542 | } 543 | 544 | // Now erase all the old edges, and add all the new edges. This will 545 | // automatically take care of any XORing that needs to be done, because 546 | // EraseEdge also erases the sibiling of undirected edges. 547 | for (int i = 0; i < edgesCopy.size(); ++i) { 548 | S2Point v0 = edgesCopy.get(i).getStart(); 549 | S2Point v1 = edgesCopy.get(i).getEnd(); 550 | eraseEdge(v0, v1); 551 | if (mergeMap.get(v0) != null) { 552 | v0 = mergeMap.get(v0); 553 | } 554 | if (mergeMap.get(v1) != null) { 555 | v1 = mergeMap.get(v1); 556 | } 557 | addEdge(v0, v1); 558 | } 559 | } 560 | 561 | /** 562 | * Look for groups of vertices that are separated by at most merge_distance() 563 | * and merge them into a single vertex. 564 | *#/ 565 | private void mergeVertices() { 566 | // The overall strategy is to start from each vertex and grow a maximal 567 | // cluster of mergable vertices. In graph theoretic terms, we find the 568 | // connected components of the undirected graph whose edges connect pairs of 569 | // vertices that are separated by at most merge_distance. 570 | // 571 | // We then choose a single representative vertex for each cluster, and 572 | // update all the edges appropriately. We choose an arbitrary existing 573 | // vertex rather than computing the centroid of all the vertices to avoid 574 | // creating new vertex pairs that need to be merged. (We guarantee that all 575 | // vertex pairs are separated by at least merge_distance in the output.) 576 | 577 | PointIndex index = new PointIndex(options.getMergeDistance().radians()); 578 | 579 | for (Map.Entry 580 | > edge : edges.entrySet()) { 583 | index.add(edge.getKey()); 584 | Multiset 585 | vset = edge.getValue(); 586 | for (S2Point v : vset) { 587 | index.add(v); 588 | } 589 | } 590 | 591 | // Next, we loop through all the vertices and attempt to grow a maximial 592 | // mergeable group starting from each vertex. 593 | 594 | Map 595 | mergeMap = Maps.newHashMap(); 597 | Stack 598 | frontier = new Stack 599 | (); 600 | List 601 | mergeable = Lists.newArrayList(); 602 | 603 | for (Map.Entry 604 | entry : index.entries()) { 606 | MarkedS2Point point = entry.getValue(); 607 | if (point.isMarked()) { 608 | continue; // Already processed. 609 | } 610 | 611 | point.mark(); 612 | 613 | // Grow a maximal mergeable component starting from "vstart", the 614 | // canonical representative of the mergeable group. 615 | S2Point vstart = point.getPoint(); 616 | frontier.push(vstart); 617 | while (!frontier.isEmpty()) { 618 | S2Point v0 = frontier.pop(); 619 | 620 | index.query(v0, mergeable); 621 | for (S2Point v1 : mergeable) { 622 | frontier.push(v1); 623 | mergeMap.put(v1, vstart); 624 | } 625 | } 626 | } 627 | 628 | // Finally, we need to replace vertices according to the merge_map. 629 | moveVertices(mergeMap); 630 | } 631 | 632 | /** 633 | * A PointIndex is a cheap spatial index to help us find mergeable vertices. 634 | * Given a set of points, it can efficiently find all of the points within a 635 | * given search radius of an arbitrary query location. It is essentially just 636 | * a hash map from cell ids at a given fixed level to the set of points 637 | * contained by that cell id. 638 | * 639 | * This class is not suitable for general use because it only supports 640 | * fixed-radius queries and has various special-purpose operations to avoid 641 | * the need for additional data structures. 642 | *#/ 643 | private class PointIndex extends ForwardingMultimap 644 | { 646 | private double searchRadius; 647 | private int level; 648 | private final Multimap 649 | delegate = HashMultimap.create(); 651 | 652 | public PointIndex(double searchRadius) { 653 | 654 | this.searchRadius = searchRadius; 655 | 656 | // We choose a cell level such that if dist(A,B) <= search_radius, the 657 | // S2CellId at that level containing A is a vertex neighbor of B (see 658 | // S2CellId.getVertexNeighbors). This turns out to be the highest 659 | // level such that a spherical cap (i.e. "disc") of the given radius 660 | // fits completely inside all cells at that level. 661 | this.level = 662 | Math.min(S2Projections.MIN_WIDTH.getMaxLevel(2 * searchRadius), S2CellId.MAX_LEVEL - 1); 663 | } 664 | 665 | @Override 666 | protected Multimap 667 | delegate() { 669 | return delegate; 670 | } 671 | 672 | /** Add a point to the index if it does not already exist. *#/ 673 | public void add(S2Point p) { 674 | S2CellId id = S2CellId.fromPoint(p).parent(level); 675 | Collection 676 | pointSet = get(id); 677 | for (MarkedS2Point point : pointSet) { 678 | if (point.getPoint().equals(p)) { 679 | return; 680 | } 681 | } 682 | put(id, new MarkedS2Point(p)); 683 | } 684 | 685 | /** 686 | * Return the set the unmarked points whose distance to "center" is less 687 | * than search_radius_, and mark these points. By construction, these points 688 | * will be contained by one of the vertex neighbors of "center". 689 | *#/ 690 | public void query(S2Point center, List 691 | output) { 692 | output.clear(); 693 | 694 | List 695 | neighbors = Lists.newArrayList(); 696 | S2CellId.fromPoint(center).getVertexNeighbors(level, neighbors); 697 | for (S2CellId id : neighbors) { 698 | // Iterate over the points contained by each vertex neighbor. 699 | for (MarkedS2Point mp : get(id)) { 700 | if (mp.isMarked()) { 701 | continue; 702 | } 703 | S2Point p = mp.getPoint(); 704 | 705 | if (center.angle(p) <= searchRadius) { 706 | output.add(p); 707 | mp.mark(); 708 | } 709 | } 710 | } 711 | } 712 | } 713 | 714 | /** 715 | * An S2Point that can be marked. Used in PointIndex. 716 | *#/ 717 | private class MarkedS2Point { 718 | private S2Point point; 719 | private boolean mark; 720 | 721 | public MarkedS2Point(S2Point point) { 722 | this.point = point; 723 | this.mark = false; 724 | } 725 | 726 | public boolean isMarked() { 727 | return mark; 728 | } 729 | 730 | public S2Point getPoint() { 731 | return point; 732 | } 733 | 734 | public void mark() { 735 | // assert (!isMarked()); 736 | this.mark = true; 737 | } 738 | } 739 | */} 740 | --------------------------------------------------------------------------------