BoundingArea::checkLongitudeWrapAround()   A
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
eloc 7
c 2
b 0
f 0
nc 7
nop 0
dl 0
loc 10
ccs 7
cts 7
cp 1
crap 6
rs 9.2222
1
<?php
2
3
/**
4
 * PHPCoord.
5
 *
6
 * @author Doug Wright
7
 */
8
declare(strict_types=1);
9
10
namespace PHPCoord\Geometry;
11
12
use Composer\InstalledVersions;
13
use PHPCoord\CoordinateOperation\GeographicValue;
14
use PHPCoord\Datum\Datum;
15
use PHPCoord\Geometry\GeoJSON\GeoJSON;
16
use PHPCoord\UnitOfMeasure\Angle\Angle;
17
use PHPCoord\UnitOfMeasure\Angle\Degree;
18
use UnhandledMatchError;
19
20
use function assert;
21
use function count;
22
use function implode;
23
use function usort;
24
use function in_array;
25
use function array_keys;
26
use function str_replace;
27
use function array_push;
28
use function array_unique;
29
use function file_exists;
30
31
class BoundingArea
32
{
33
    /**
34
     * @var Polygon[]
35
     */
36
    protected array $polygons;
37
38
    protected string $region;
39
40
    protected bool $longitudeWrapAroundChecked = false;
41
42
    protected bool $longitudeExtendsFurtherThanMinus180 = false;
43
44
    protected bool $longitudeExtendsFurtherThanPlus180 = false;
45
46
    /**
47
     * @var array<string, self>
48
     */
49
    private static array $cachedObjects = [];
50
51
    /**
52
     * @var array{0: Angle, 1: Angle}
53
     */
54
    private array $pointInside;
55
56
    /**
57
     * @var array<int, array{0: Angle, 1: Angle}>
58
     */
59
    private array $centre = [];
60
61
    /**
62
     * @param Polygon[] $polygons
63 714
     */
64
    protected function __construct(array $polygons, string $region)
65
    {
66 714
        // put largest polygon (outer ring size) first
67 714
        usort($polygons, fn (Polygon $polygonA, Polygon $polygonB) => count($polygonB->coordinates[0]->coordinates) <=> count($polygonA->coordinates[0]->coordinates));
68 714
        $this->polygons = $polygons;
69
        $this->region = $region;
70
    }
71 274
72
    public function getRegion(): string
73 274
    {
74
        return $this->region;
75
    }
76
77
    /**
78
     * Vertices in GeoJSON-type format (an array of polygons, which is an array of rings which is an array of long,lat points).
79
     * @param Polygon[]           $polygons
80
     * @param RegionMap::REGION_* $region
81 687
     */
82
    public static function createFromPolygons(array $polygons, string $region): self
83 687
    {
84
        return new self($polygons, $region);
85
    }
86 54
87
    public static function createWorld(): self
88 54
    {
89 54
        return new self(
90 54
            [
91 54
                new Polygon(
92 54
                    new LinearRing(
93 54
                        new Position(-180, -90),
94 54
                        new Position(-180, 90),
95 54
                        new Position(180, 90),
96 54
                        new Position(180, -90),
97 54
                        new Position(-180, -90),
98 54
                    )
99 54
                ),
100 54
            ],
101 54
            RegionMap::REGION_GLOBAL
102
        );
103
    }
104
105
    /**
106
     * @internal
107
     * @param string[] $extentCodes
108 779
     */
109
    public static function createFromExtentCodes(array $extentCodes, bool $boundingBoxOnly = false): self
110 779
    {
111 779
        $cacheKey = implode('|', $extentCodes) . ($boundingBoxOnly ? 'bboxOnly' : '');
112 597
        if (!isset(self::$cachedObjects[$cacheKey])) {
113
            $regions = [];
114
115 597
            /** @var Polygon[] $extents */
116 597
            $extents = [];
117 597
            foreach ($extentCodes as $extentUrn) {
118 597
                $region = RegionMap::EXTENTS[$extentUrn];
119
                $regions[] = $region;
120 597
121 597
                $filename = str_replace('urn:ogc:def:area:EPSG::', '', $extentUrn) . '.json';
122 597
                $pathToExtent = __DIR__ . '/Extents/BoundingBoxOnly/' . $filename;
123 127
                if (InstalledVersions::isInstalled(RegionMap::PACKAGES[$region])) {
124 127
                    $baseDir = InstalledVersions::getInstallPath(RegionMap::PACKAGES[$region]) . '/src/Geometry/Extents/';
125 126
                    if ((!$boundingBoxOnly || $region === RegionMap::REGION_GLOBAL) && file_exists($baseDir . $filename)) {
126
                        $pathToExtent = $baseDir . $filename;
127
                    }
128
                }
129 597
130 597
                $extent = GeoJSON::readFile($pathToExtent);
131 589
                match ($extent::class) {
132 19
                    Polygon::class => $extents[] = $extent,
133
                    MultiPolygon::class => array_push($extents, ...$extent->coordinates),
134 597
                    default => throw new UnhandledMatchError()
135
                };
136
            }
137 597
138
            assert(count(array_unique($regions)) === 1);
139
140 597
            /** @var RegionMap::REGION_* $region */
141 597
            $region = $regions[0];
142
            $extentData = self::createFromPolygons($extents, $region);
0 ignored issues
show
Bug introduced by
$region of type PHPCoord\Geometry\RegionMap is incompatible with the type string expected by parameter $region of PHPCoord\Geometry\Boundi...a::createFromPolygons(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

142
            $extentData = self::createFromPolygons($extents, /** @scrutinizer ignore-type */ $region);
Loading history...
143 597
144
            if (in_array('urn:ogc:def:area:EPSG::1352', $extentCodes)) {
145
                $extentData->pointInside = [new Degree(60.202778), new Degree(11.083889)];
146
            }
147 597
148
            self::$cachedObjects[$cacheKey] = $extentData;
149
        }
150 779
151
        return self::$cachedObjects[$cacheKey];
152
    }
153 328
154
    public function containsPoint(GeographicValue $point): bool
155 328
    {
156 220
        if (!$this->longitudeWrapAroundChecked) {
157 220
            $this->longitudeWrapAroundChecked = true;
158
            $this->checkLongitudeWrapAround();
159
        }
160 328
161 328
        $latitude = $point->getLongitude()->asDegrees()->getValue();
162
        $longitude = $point->getLatitude()->asDegrees()->getValue();
163 328
164 328
        $pointsToCheck = [
165 328
            [
166 328
                $latitude,
167 328
                $longitude,
168 328
            ],
169
        ];
170 328
171 181
        if ($this->longitudeExtendsFurtherThanMinus180) {
172 181
            $pointsToCheck[] = [
173 181
                $latitude - 360,
174 181
                $longitude,
175
            ];
176
        }
177 328
178 173
        if ($this->longitudeExtendsFurtherThanPlus180) {
179 173
            $pointsToCheck[] = [
180 173
                $latitude + 360,
181 173
                $longitude,
182
            ];
183
        }
184
185
        /*
186
         * @see https://observablehq.com/@tmcw/understanding-point-in-polygon
187 328
         */
188 328
        foreach ($pointsToCheck as $pointToCheck) {
189 328
            [$x, $y] = $pointToCheck;
190 328
            foreach ($this->polygons as $polygon) {
191
                $vertices = $polygon->coordinates[0]->coordinates; // this algo works on simple polygons (no holes)
192 328
193 328
                $n = count($vertices);
194 328
                $inside = false;
195 328
                for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) {
196 328
                    $xi = $vertices[$i]->x;
197 328
                    $yi = $vertices[$i]->y;
198 328
                    $xj = $vertices[$j]->x;
199
                    $yj = $vertices[$j]->y;
200 328
201 328
                    $intersect = (($yi > $y) !== ($yj > $y)) // horizontal ray from $y, intersects if vertices are on opposite sides of it
202 328
                        && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi);
203 319
                    if ($intersect) {
204
                        $inside = !$inside;
0 ignored issues
show
introduced by
The condition $inside is always false.
Loading history...
205
                    }
206
                }
207 328
208 319
                if ($inside) {
209
                    return true;
210
                }
211
            }
212
        }
213 108
214
        return false;
215
    }
216
217
    /**
218
     * @internal used for testing
219
     * @return array{0: Angle, 1: Angle}
220 27
     */
221
    public function getPointInside(): array
222 27
    {
223 27
        if (!isset($this->pointInside)) {
224 27
            foreach (array_keys($this->polygons) as $polygonId) {
225 27
                $point = $this->getCentre($polygonId);
226 27
                $this->pointInside = $point;
227 27
                if ($this->containsPoint(new GeographicValue($point[0], $point[1], null, Datum::fromSRID(Datum::EPSG_WORLD_GEODETIC_SYSTEM_1984_ENSEMBLE)))) {
228 27
                    $this->pointInside = $point;
229
                    break;
230
                }
231
            }
232
        }
233 27
234
        return $this->pointInside;
235
    }
236
237
    /**
238
     * @internal used for testing
239
     * @return array{0: Angle, 1: Angle}
240 27
     */
241
    protected function getCentre(int $polygonId): array
242 27
    {
243
        if (!isset($this->centre[$polygonId])) {
244 27
            // Calculates the "centre" (centroid) of a polygon.
245 27
            $vertices = $this->polygons[$polygonId]->coordinates[0]->coordinates; // only consider outer ring
246 27
            $n = count($vertices) - 1;
247
            $area = 0;
248 27
249 27
            for ($i = 0; $i < $n; ++$i) {
250
                $area += $vertices[$i]->x * $vertices[$i + 1]->y;
251 27
            }
252
            $area += $vertices[$n]->x * $vertices[0]->y;
253 27
254 27
            for ($i = 0; $i < $n; ++$i) {
255
                $area -= $vertices[$i + 1]->x * $vertices[$i]->y;
256 27
            }
257 27
            $area -= $vertices[0]->x * $vertices[$n]->y;
258
            $area /= 2;
259 27
260 27
            $latitude = 0;
261
            $longitude = 0;
262 27
263 27
            for ($i = 0; $i < $n; ++$i) {
264 27
                $latitude += ($vertices[$i]->y + $vertices[$i + 1]->y) * ($vertices[$i]->x * $vertices[$i + 1]->y - $vertices[$i + 1]->x * $vertices[$i]->y);
265
                $longitude += ($vertices[$i]->x + $vertices[$i + 1]->x) * ($vertices[$i]->x * $vertices[$i + 1]->y - $vertices[$i + 1]->x * $vertices[$i]->y);
266 27
            }
267 27
            $latitude = new Degree($latitude / 6 / $area);
268
            $longitude = new Degree($longitude / 6 / $area);
269 27
270
            $this->centre[$polygonId] = [$latitude, $longitude];
271
        }
272 27
273
        return $this->centre[$polygonId];
274
    }
275 220
276
    private function checkLongitudeWrapAround(): void
277 220
    {
278 220
        foreach ($this->polygons as $polygon) {
279 220
            foreach ($polygon->coordinates as $ring) {
280 220
                foreach ($ring->coordinates as $vertex) {
281 73
                    if ($vertex->x >= 180) {
282
                        $this->longitudeExtendsFurtherThanPlus180 = true;
283 220
                    }
284 56
                    if ($vertex->x <= -180) {
285
                        $this->longitudeExtendsFurtherThanMinus180 = true;
286
                    }
287
                }
288
            }
289
        }
290
    }
291
}
292