Passed
Push — master ( dad6c0...698766 )
by Doug
16:15
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 644
    protected function __construct(array $vertices)
45
    {
46 644
        $this->vertices = $vertices;
47 644
    }
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 617
    public static function createFromArray(array $vertices): self
53
    {
54 617
        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 710
    public static function createFromExtentCodes(array $extentCodes): self
66
    {
67 710
        $cacheKey = implode('|', $extentCodes);
68 710
        if (!isset(self::$cachedObjects[$cacheKey])) {
69 500
            $extents = [];
70 500
            foreach ($extentCodes as $extentCode) {
71 500
                $fullExtent = "PHPCoord\\Geometry\\Extents\\Extent{$extentCode}";
72 500
                $basicExtent = "PHPCoord\\Geometry\\Extents\\BoundingBoxOnly\\Extent{$extentCode}";
73 500
                $extentClass = class_exists($fullExtent) ? new $fullExtent() : new $basicExtent();
74 500
                array_push($extents, ...$extentClass());
75
            }
76
77 500
            $extentData = self::createFromArray($extents);
78 500
            $extentData->addBuffer();
79
80 500
            self::$cachedObjects[$cacheKey] = $extentData;
81
        }
82
83 710
        return self::$cachedObjects[$cacheKey];
84
    }
85
86 272
    public function containsPoint(GeographicValue $point): bool
87
    {
88 272
        if (!$this->longitudeWrapAroundChecked) {
89 272
            $this->checkLongitudeWrapAround();
90
        }
91
92 272
        $latitude = $point->getLongitude()->asDegrees()->getValue();
93 272
        $longitude = $point->getLatitude()->asDegrees()->getValue();
94
95 272
        $pointsToCheck = [
96
            [
97 272
                $latitude,
98 272
                $longitude,
99
            ],
100
        ];
101
102 272
        if ($this->longitudeExtendsFurtherThanMinus180) {
103 98
            $pointsToCheck[] = [
104 98
                $latitude - 360,
105 98
                $longitude,
106
            ];
107
        }
108
109 272
        if ($this->longitudeExtendsFurtherThanPlus180) {
110 100
            $pointsToCheck[] = [
111 100
                $latitude + 360,
112 100
                $longitude,
113
            ];
114
        }
115
116
        /*
117
         * @see https://observablehq.com/@tmcw/understanding-point-in-polygon
118
         */
119 272
        foreach ($pointsToCheck as $pointToCheck) {
120 272
            [$x, $y] = $pointToCheck;
121 272
            foreach ($this->vertices as $polygon) {
122 272
                $vertices = array_merge(...$polygon);
123
124 272
                $n = count($vertices);
125 272
                $inside = false;
126 272
                for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) {
127 272
                    $xi = $vertices[$i][0];
128 272
                    $yi = $vertices[$i][1];
129 272
                    $xj = $vertices[$j][0];
130 272
                    $yj = $vertices[$j][1];
131
132 272
                    $intersect = (($yi > $y) !== ($yj > $y)) // horizontal ray from $y, intersects if vertices are on opposite sides of it
133 272
                        && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi);
134 272
                    if ($intersect) {
135 263
                        $inside = !$inside;
0 ignored issues
show
introduced by
The condition $inside is always false.
Loading history...
136
                    }
137
                }
138
139 272
                if ($inside) {
140 263
                    return true;
141
                }
142
            }
143
        }
144
145 108
        return false;
146
    }
147
148
    /**
149
     * @internal used for testing
150
     * @return array<Angle,Angle>
151
     */
152 27
    public function getPointInside(): array
153
    {
154 27
        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...
155 27
            $this->pointInside = $this->getCentre(0); // any polygon will do, use the first
156
        }
157
158 27
        return $this->pointInside;
159
    }
160
161
    /**
162
     * @internal used for testing
163
     * @return array<Angle,Angle>
164
     */
165 527
    protected function getCentre(int $polygonId): array
166
    {
167 527
        if (!isset($this->centre[$polygonId])) {
168
            // Calculates the "centre" (centroid) of a polygon.
169 527
            $vertices = $this->vertices[$polygonId][0]; // only consider outer ring
170 527
            $n = count($vertices) - 1;
171 527
            $area = 0;
172
173 527
            for ($i = 0; $i < $n; ++$i) {
174 527
                $area += $vertices[$i][0] * $vertices[$i + 1][1];
175
            }
176 527
            $area += $vertices[$n][0] * $vertices[0][1];
177
178 527
            for ($i = 0; $i < $n; ++$i) {
179 527
                $area -= $vertices[$i + 1][0] * $vertices[$i][1];
180
            }
181 527
            $area -= $vertices[0][0] * $vertices[$n][1];
182 527
            $area /= 2;
183
184 527
            $latitude = 0;
185 527
            $longitude = 0;
186
187 527
            for ($i = 0; $i < $n; ++$i) {
188 527
                $latitude += ($vertices[$i][1] + $vertices[$i + 1][1]) * ($vertices[$i][0] * $vertices[$i + 1][1] - $vertices[$i + 1][0] * $vertices[$i][1]);
189 527
                $longitude += ($vertices[$i][0] + $vertices[$i + 1][0]) * ($vertices[$i][0] * $vertices[$i + 1][1] - $vertices[$i + 1][0] * $vertices[$i][1]);
190
            }
191 527
            $latitude = new Degree($latitude / 6 / $area);
192 527
            $longitude = new Degree($longitude / 6 / $area);
193
194 527
            $this->centre[$polygonId] = [$latitude, $longitude];
195
        }
196
197 527
        return $this->centre[$polygonId];
198
    }
199
200
    /**
201
     * @internal
202
     */
203 500
    private function addBuffer(): void
204
    {
205 500
        foreach ($this->vertices as $polygonId => $polygon) {
206 500
            $centre = $this->getCentre($polygonId);
207 500
            $centreX = $centre[1]->asDegrees()->getValue();
208 500
            $centreY = $centre[0]->asDegrees()->getValue();
209 500
            foreach ($polygon as $ringId => $ring) {
210 500
                if ($ringId === 0 && count($ring) > self::BUFFER_THRESHOLD) {
211 119
                    foreach ($ring as $vertexId => $vertex) {
212 119
                        if ($vertex[0] > $centreX) {
213 119
                            $this->vertices[$polygonId][$ringId][$vertexId][0] += self::BUFFER_SIZE;
214 119
                        } elseif ($vertex[0] < $centreX) {
215 119
                            $this->vertices[$polygonId][$ringId][$vertexId][0] -= self::BUFFER_SIZE;
216
                        }
217
218 119
                        if ($vertex[1] > $centreY) {
219 119
                            $this->vertices[$polygonId][$ringId][$vertexId][1] += self::BUFFER_SIZE;
220 119
                        } elseif ($vertex[1] < $centreY) {
221 119
                            $this->vertices[$polygonId][$ringId][$vertexId][1] -= self::BUFFER_SIZE;
222
                        }
223
                    }
224
                }
225
            }
226
        }
227 500
    }
228
229 272
    private function checkLongitudeWrapAround(): void
230
    {
231 272
        foreach ($this->vertices as $polygon) {
232 272
            foreach ($polygon as $ring) {
233 272
                foreach ($ring as $vertex) {
234 272
                    if ($vertex[0] > 180) {
235 100
                        $this->longitudeExtendsFurtherThanPlus180 = true;
236
                    }
237 272
                    if ($vertex[0] < -180) {
238 98
                        $this->longitudeExtendsFurtherThanMinus180 = true;
239
                    }
240
                }
241
            }
242
        }
243 272
    }
244
}
245