Passed
Branch master (aff60e)
by Doug
13:03
created

BoundingArea::getCentre()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 29
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 18
c 1
b 0
f 0
nc 8
nop 1
dl 0
loc 29
ccs 19
cts 19
cp 1
crap 4
rs 9.6666
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 class_exists;
13
use function count;
14
use function implode;
15
use PHPCoord\CoordinateOperation\GeographicValue;
16
use PHPCoord\UnitOfMeasure\Angle\Angle;
17
use PHPCoord\UnitOfMeasure\Angle\Degree;
18
19
class BoundingArea
20
{
21
    /**
22
     * Vertices in GeoJSON-type format (an array of polygons, which is an array of rings which is an array of long,lat points).
23
     * @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...
24
     */
25
    protected array $vertices;
26
27
    protected bool $longitudeExtendsFurtherThanMinus180 = false;
28
29
    protected bool $longitudeExtendsFurtherThanPlus180 = false;
30
31
    private static array $cachedObjects = [];
32
33
    private const BUFFER_THRESHOLD = 200; // rough guess at where map maker got bored adding vertices for complex shapes
34
35
    private const BUFFER_SIZE = 0.1; // approx 10km
36
37 621
    protected function __construct(array $vertices)
38
    {
39 621
        $this->vertices = $vertices;
40 621
        foreach ($this->vertices as $polygon) {
41 621
            foreach ($polygon as $ring) {
42 621
                foreach ($ring as $vertex) {
43 621
                    if ($vertex[0] > 180) {
44 99
                        $this->longitudeExtendsFurtherThanPlus180 = true;
45
                    }
46 621
                    if ($vertex[0] < -180) {
47 36
                        $this->longitudeExtendsFurtherThanMinus180 = true;
48
                    }
49
                }
50
            }
51
        }
52 621
    }
53
54
    /**
55
     * @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...
56
     */
57 594
    public static function createFromArray(array $vertices): self
58
    {
59 594
        return new static($vertices);
60
    }
61
62 54
    public static function createWorld(): self
63
    {
64 54
        return new static([[[[-180, -90], [-180, 90], [180, 90], [180, -90], [-180, -90]]]]);
65
    }
66
67
    /**
68
     * @internal
69
     */
70 648
    public static function createFromExtentCodes(array $extentCodes): self
71
    {
72 648
        $cacheKey = implode('', $extentCodes);
73 648
        if (!isset(self::$cachedObjects[$cacheKey])) {
74 477
            $extents = [];
75 477
            foreach ($extentCodes as $extentCode) {
76 477
                $fullExtent = "PHPCoord\\Geometry\\Extents\\Extent{$extentCode}";
77 477
                $basicExtent = "PHPCoord\\Geometry\\Extents\\BoundingBoxOnly\\Extent{$extentCode}";
78 477
                $extentClass = class_exists($fullExtent) ? new $fullExtent() : new $basicExtent();
79 477
                $extents = array_merge($extents, $extentClass());
80
            }
81
82 477
            self::$cachedObjects[$cacheKey] = self::createFromArray($extents);
83
        }
84
85 648
        return self::$cachedObjects[$cacheKey];
86
    }
87
88 234
    public function containsPoint(GeographicValue $point): bool
89
    {
90 234
        $latitude = $point->getLongitude()->asDegrees()->getValue();
91 234
        $longitude = $point->getLatitude()->asDegrees()->getValue();
92
93
        $pointsToCheck = [
94
            [
95 234
                $latitude,
96 234
                $longitude,
97
            ],
98
        ];
99
100 234
        if ($this->longitudeExtendsFurtherThanMinus180) {
101 9
            $pointsToCheck[] = [
102 9
                $latitude - 360,
103 9
                $longitude,
104
            ];
105
        }
106
107 234
        if ($this->longitudeExtendsFurtherThanPlus180) {
108 36
            $pointsToCheck[] = [
109 36
                $latitude + 360,
110 36
                $longitude,
111
            ];
112
        }
113
114
        /*
115
         * @see https://observablehq.com/@tmcw/understanding-point-in-polygon
116
         */
117 234
        foreach ($pointsToCheck as $pointToCheck) {
118 234
            [$x, $y] = $pointToCheck;
119 234
            foreach ($this->vertices as $polygonId => $polygon) {
120 234
                $centre = $this->getCentre($polygonId);
121 234
                $centreX = $centre[1]->asDegrees()->getValue();
122 234
                $centreY = $centre[0]->asDegrees()->getValue();
123 234
                $vertices = [];
124 234
                foreach ($polygon as $ringId => $ring) {
125 234
                    if ($ringId === 0 && count($ring) > self::BUFFER_THRESHOLD) {
126 100
                        foreach ($ring as $vertexId => $vertex) {
127 100
                            if ($vertex[0] > $centreX) {
128 100
                                $ring[$vertexId][0] += self::BUFFER_SIZE;
129 100
                            } elseif ($vertex[0] < $centreX) {
130 100
                                $ring[$vertexId][0] -= self::BUFFER_SIZE;
131
                            }
132
133 100
                            if ($vertex[1] > $centreY) {
134 100
                                $ring[$vertexId][1] += self::BUFFER_SIZE;
135 100
                            } elseif ($vertex[1] < $centreY) {
136 100
                                $ring[$vertexId][1] -= self::BUFFER_SIZE;
137
                            }
138
                        }
139
                    }
140 234
                    $vertices = array_merge($vertices, $ring);
141
                }
142
143 234
                $n = count($vertices);
144 234
                $inside = false;
145 234
                for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) {
146 234
                    $xi = $vertices[$i][0];
147 234
                    $yi = $vertices[$i][1];
148 234
                    $xj = $vertices[$j][0];
149 234
                    $yj = $vertices[$j][1];
150
151 234
                    $intersect = (($yi > $y) !== ($yj > $y)) // horizontal ray from $y, intersects if vertices are on opposite sides of it
152 234
                        && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi);
153 234
                    if ($intersect) {
154 225
                        $inside = !$inside;
0 ignored issues
show
introduced by
The condition $inside is always false.
Loading history...
155
                    }
156
                }
157
158 234
                if ($inside) {
159 225
                    return true;
160
                }
161
            }
162
        }
163
164 99
        return false;
165
    }
166
167
    /**
168
     * @internal used for testing
169
     * @return array<Angle,Angle>
170
     */
171 27
    public function getPointInside(): array
172
    {
173 27
        return $this->getCentre(0); // any polygon will do, use the first
174
    }
175
176
    /**
177
     * @internal used for testing
178
     * @return array<Angle,Angle>
179
     */
180 234
    protected function getCentre(int $polygonId): array
181
    {
182
        // Calculates the "centre" (centroid) of a polygon.
183 234
        $vertices = $this->vertices[$polygonId][0]; // only consider outer ring
184 234
        $n = count($vertices);
185 234
        $area = 0;
186
187 234
        for ($i = 0; $i < ($n - 1); ++$i) {
188 234
            $area += $vertices[$i][0] * $vertices[$i + 1][1];
189
        }
190 234
        $area += $vertices[$n - 1][0] * $vertices[0][1];
191
192 234
        for ($i = 0; $i < ($n - 1); ++$i) {
193 234
            $area -= $vertices[$i + 1][0] * $vertices[$i][1];
194
        }
195 234
        $area -= $vertices[0][0] * $vertices[$n - 1][1];
196 234
        $area /= 2;
197
198 234
        $latitude = 0;
199 234
        $longitude = 0;
200
201 234
        for ($i = 0; $i < ($n - 1); ++$i) {
202 234
            $latitude += ($vertices[$i][1] + $vertices[$i + 1][1]) * ($vertices[$i][0] * $vertices[$i + 1][1] - $vertices[$i + 1][0] * $vertices[$i][1]);
203 234
            $longitude += ($vertices[$i][0] + $vertices[$i + 1][0]) * ($vertices[$i][0] * $vertices[$i + 1][1] - $vertices[$i + 1][0] * $vertices[$i][1]);
204
        }
205 234
        $latitude = new Degree($latitude / 6 / $area);
206 234
        $longitude = new Degree($longitude / 6 / $area);
207
208 234
        return [$latitude, $longitude];
209
    }
210
}
211