Passed
Push — extents ( 5b6152...f35511 )
by Doug
63:43 queued 55s
created

BoundingArea::checkLongitudeWrapAround()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 7
c 1
b 0
f 0
nc 7
nop 0
dl 0
loc 10
ccs 8
cts 8
cp 1
crap 6
rs 9.2222
1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord\Geometry;
10
11
use function array_merge;
12
use function array_push;
13
use function class_exists;
14
use function count;
15
use function implode;
16
use PHPCoord\CoordinateOperation\GeographicValue;
17
use PHPCoord\UnitOfMeasure\Angle\Angle;
18
use PHPCoord\UnitOfMeasure\Angle\Degree;
19
20
class BoundingArea
21
{
22
    /**
23
     * Vertices in GeoJSON-type format (an array of polygons, which is an array of rings which is an array of long,lat points).
24
     * @var array<array<array<array<float, float>>>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array<array<array<float, float>>> at position 13 could not be parsed: Expected '>' at position 13, but found '>'.
Loading history...
25
     */
26
    protected array $vertices;
27
28
    protected bool $longitudeWrapAroundChecked = false;
29
30
    protected bool $longitudeExtendsFurtherThanMinus180 = false;
31
32
    protected bool $longitudeExtendsFurtherThanPlus180 = false;
33
34
    private static array $cachedObjects = [];
35
36
    private array $pointInside = [];
37
38
    private array $centre = [];
39
40
    private const BUFFER_THRESHOLD = 200; // rough guess at where map maker got bored adding vertices for complex shapes
41
42
    private const BUFFER_SIZE = 0.1; // approx 10km
43
44 24876
    protected function __construct(array $vertices)
45
    {
46 24876
        $this->vertices = $vertices;
47 24876
    }
48
49
    /**
50
     * @param array<array<array<array<float, float>>> $vertices [[[long,lat], [long,lat]...]]
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array<array<array<float, float>>> at position 13 could not be parsed: Expected '>' at position 13, but found '>'.
Loading history...
51
     */
52 24849
    public static function createFromArray(array $vertices): self
53
    {
54 24849
        return new static($vertices);
55
    }
56
57 54
    public static function createWorld(): self
58
    {
59 54
        return new static([[[[-180, -90], [-180, 90], [180, 90], [180, -90], [-180, -90]]]]);
60
    }
61
62
    /**
63
     * @internal
64
     */
65 66407
    public static function createFromExtentCodes(array $extentCodes): self
66
    {
67 66407
        $cacheKey = implode('|', $extentCodes);
68 66407
        if (!isset(self::$cachedObjects[$cacheKey])) {
69 24732
            $extents = [];
70 24732
            foreach ($extentCodes as $extentCode) {
71 24732
                $fullExtent = "PHPCoord\\Geometry\\Extents\\Extent{$extentCode}";
72 24732
                $basicExtent = "PHPCoord\\Geometry\\Extents\\BoundingBoxOnly\\Extent{$extentCode}";
73 24732
                $extentClass = class_exists($fullExtent) ? new $fullExtent() : new $basicExtent();
74 24732
                array_push($extents, ...$extentClass());
75
            }
76
77 24732
            $extentData = self::createFromArray($extents);
78 24732
            $extentData->addBuffer();
79
80 24732
            self::$cachedObjects[$cacheKey] = $extentData;
81
        }
82
83 66407
        return self::$cachedObjects[$cacheKey];
84
    }
85
86 272
    public function containsPoint(GeographicValue $point): bool
87
    {
88 272
        if (!$this->longitudeWrapAroundChecked) {
89 192
            $this->longitudeWrapAroundChecked = true;
90 192
            $this->checkLongitudeWrapAround();
91
        }
92
93 272
        $latitude = $point->getLongitude()->asDegrees()->getValue();
94 272
        $longitude = $point->getLatitude()->asDegrees()->getValue();
95
96 272
        $pointsToCheck = [
97
            [
98 272
                $latitude,
99 272
                $longitude,
100
            ],
101
        ];
102
103 272
        if ($this->longitudeExtendsFurtherThanMinus180) {
104 98
            $pointsToCheck[] = [
105 98
                $latitude - 360,
106 98
                $longitude,
107
            ];
108
        }
109
110 272
        if ($this->longitudeExtendsFurtherThanPlus180) {
111 100
            $pointsToCheck[] = [
112 100
                $latitude + 360,
113 100
                $longitude,
114
            ];
115
        }
116
117
        /*
118
         * @see https://observablehq.com/@tmcw/understanding-point-in-polygon
119
         */
120 272
        foreach ($pointsToCheck as $pointToCheck) {
121 272
            [$x, $y] = $pointToCheck;
122 272
            foreach ($this->vertices as $polygon) {
123 272
                $vertices = array_merge(...$polygon);
124
125 272
                $n = count($vertices);
126 272
                $inside = false;
127 272
                for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) {
128 272
                    $xi = $vertices[$i][0];
129 272
                    $yi = $vertices[$i][1];
130 272
                    $xj = $vertices[$j][0];
131 272
                    $yj = $vertices[$j][1];
132
133 272
                    $intersect = (($yi > $y) !== ($yj > $y)) // horizontal ray from $y, intersects if vertices are on opposite sides of it
134 272
                        && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi);
135 272
                    if ($intersect) {
136 263
                        $inside = !$inside;
0 ignored issues
show
introduced by
The condition $inside is always false.
Loading history...
137
                    }
138
                }
139
140 272
                if ($inside) {
141 263
                    return true;
142
                }
143
            }
144
        }
145
146 108
        return false;
147
    }
148
149
    /**
150
     * @internal used for testing
151
     * @return array<Angle,Angle>
152
     */
153 62253
    public function getPointInside(): array
154
    {
155 62253
        if (!$this->pointInside) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->pointInside of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
156 19515
            $this->pointInside = $this->getCentre(0); // any polygon will do, use the first
157
        }
158
159 62253
        return $this->pointInside;
160
    }
161
162
    /**
163
     * @internal used for testing
164
     * @return array<Angle,Angle>
165
     */
166 29311
    protected function getCentre(int $polygonId): array
167
    {
168 29311
        if (!isset($this->centre[$polygonId])) {
169
            // Calculates the "centre" (centroid) of a polygon.
170 24759
            $vertices = $this->vertices[$polygonId][0]; // only consider outer ring
171 24759
            $n = count($vertices) - 1;
172 24759
            $area = 0;
173
174 24759
            for ($i = 0; $i < $n; ++$i) {
175 24759
                $area += $vertices[$i][0] * $vertices[$i + 1][1];
176
            }
177 24759
            $area += $vertices[$n][0] * $vertices[0][1];
178
179 24759
            for ($i = 0; $i < $n; ++$i) {
180 24759
                $area -= $vertices[$i + 1][0] * $vertices[$i][1];
181
            }
182 24759
            $area -= $vertices[0][0] * $vertices[$n][1];
183 24759
            $area /= 2;
184
185 24759
            $latitude = 0;
186 24759
            $longitude = 0;
187
188 24759
            for ($i = 0; $i < $n; ++$i) {
189 24759
                $latitude += ($vertices[$i][1] + $vertices[$i + 1][1]) * ($vertices[$i][0] * $vertices[$i + 1][1] - $vertices[$i + 1][0] * $vertices[$i][1]);
190 24759
                $longitude += ($vertices[$i][0] + $vertices[$i + 1][0]) * ($vertices[$i][0] * $vertices[$i + 1][1] - $vertices[$i + 1][0] * $vertices[$i][1]);
191
            }
192 24759
            $latitude = new Degree($latitude / 6 / $area);
193 24759
            $longitude = new Degree($longitude / 6 / $area);
194
195 24759
            $this->centre[$polygonId] = [$latitude, $longitude];
196
        }
197
198 29311
        return $this->centre[$polygonId];
199
    }
200
201
    /**
202
     * @internal
203
     */
204 24732
    private function addBuffer(): void
205
    {
206 24732
        foreach ($this->vertices as $polygonId => $polygon) {
207 24732
            $centre = $this->getCentre($polygonId);
208 24732
            $centreX = $centre[1]->asDegrees()->getValue();
209 24732
            $centreY = $centre[0]->asDegrees()->getValue();
210 24732
            foreach ($polygon as $ringId => $ring) {
211 24732
                if ($ringId === 0 && count($ring) > self::BUFFER_THRESHOLD) {
212 4444
                    foreach ($ring as $vertexId => $vertex) {
213 4444
                        if ($vertex[0] > $centreX) {
214 4444
                            $this->vertices[$polygonId][$ringId][$vertexId][0] += self::BUFFER_SIZE;
215 4444
                        } elseif ($vertex[0] < $centreX) {
216 4444
                            $this->vertices[$polygonId][$ringId][$vertexId][0] -= self::BUFFER_SIZE;
217
                        }
218
219 4444
                        if ($vertex[1] > $centreY) {
220 4444
                            $this->vertices[$polygonId][$ringId][$vertexId][1] += self::BUFFER_SIZE;
221 4444
                        } elseif ($vertex[1] < $centreY) {
222 4444
                            $this->vertices[$polygonId][$ringId][$vertexId][1] -= self::BUFFER_SIZE;
223
                        }
224
                    }
225
                }
226
            }
227
        }
228 24732
    }
229
230 192
    private function checkLongitudeWrapAround(): void
231
    {
232 192
        foreach ($this->vertices as $polygon) {
233 192
            foreach ($polygon as $ring) {
234 192
                foreach ($ring as $vertex) {
235 192
                    if ($vertex[0] > 180) {
236 64
                        $this->longitudeExtendsFurtherThanPlus180 = true;
237
                    }
238 192
                    if ($vertex[0] < -180) {
239 45
                        $this->longitudeExtendsFurtherThanMinus180 = true;
240
                    }
241
                }
242
            }
243
        }
244 192
    }
245
}
246