Passed
Push — master ( fc1d9d...4229b1 )
by Doug
62:32
created

BoundingArea::addBuffer()   B

Complexity

Conditions 10
Paths 13

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 10

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 15
c 1
b 0
f 0
nc 13
nop 0
dl 0
loc 19
ccs 9
cts 9
cp 1
crap 10
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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