Passed
Push — master ( 0e0534...3c9843 )
by Doug
02:42
created

AutoConversion::getPointForBoundaryCheck()   A

Complexity

Conditions 6
Paths 10

Size

Total Lines 33
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 14.3902

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 14
c 1
b 0
f 0
nc 10
nop 0
dl 0
loc 33
ccs 5
cts 13
cp 0.3846
crap 14.3902
rs 9.2222
1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord\CoordinateOperation;
10
11
use function abs;
12
use function array_column;
13
use function array_merge;
14
use function array_pop;
15
use function array_shift;
16
use function array_sum;
17
use function assert;
18
use function count;
19
use DateTimeImmutable;
20
use function in_array;
21
use PHPCoord\CompoundPoint;
22
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
23
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
24
use PHPCoord\Exception\UnknownConversionException;
25
use PHPCoord\GeocentricPoint;
26
use PHPCoord\GeographicPoint;
27
use PHPCoord\Geometry\GeographicPolygon;
28
use PHPCoord\Point;
29
use PHPCoord\ProjectedPoint;
30
use PHPCoord\UnitOfMeasure\Time\Year;
31
use function strpos;
32
use function usort;
33
34
/**
35
 * @internal
36
 */
37
trait AutoConversion
38
{
39
    private int $maxChainDepth = 4; // if traits could have constants...
40
41
    private static array $pathCache = [];
42
43 42
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
44
    {
45 42
        if ($this->getCRS() == $to) {
46 23
            return $this;
47
        }
48
49 23
        if (strpos($this->getCRS()->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0 || strpos($to->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0) {
50 1
            throw new UnknownConversionException('Automatic conversions are only supported for EPSG CRSs');
51
        }
52
53 23
        $point = $this;
54 23
        $path = $this->findOperationPath($this->getCRS(), $to, $ignoreBoundaryRestrictions);
55
56 22
        foreach ($path as $step) {
57 22
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
58 22
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
59
        }
60
61 22
        return $point;
62
    }
63
64 23
    protected function findOperationPath(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target, bool $ignoreBoundaryRestrictions): array
65
    {
66 23
        $candidatePaths = $this->buildTransformationPathsToCRS($source, $target);
67
68 23
        usort($candidatePaths, static function (array $a, array $b) {
69 16
            return count($a['path']) <=> count($b['path']) ?: $a['accuracy'] <=> $b['accuracy'];
70 23
        });
71
72 23
        $boundaryCheckPoint = $ignoreBoundaryRestrictions ? null : $this->getPointForBoundaryCheck();
73
74 23
        foreach ($candidatePaths as $candidatePath) {
75 23
            $ok = true;
76
77 23
            foreach ($candidatePath['path'] as $pathStep) {
78 23
                $operation = CoordinateOperations::getOperationData($pathStep['operation']);
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateOperation\CoordinateOperations was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
79 23
                if ($boundaryCheckPoint) {
80
                    //filter out operations that only operate outside this point
81 19
                    $polygon = GeographicPolygon::createFromArray($operation['bounding_box'], $operation['bounding_box_crosses_antimeridian']);
82 19
                    $ok = $ok && $polygon->containsPoint($boundaryCheckPoint);
83
                }
84
85 23
                $operations = static::resolveConcatenatedOperations($pathStep['operation'], false);
86
87 23
                foreach ($operations as $operation) {
88
                    //filter out operations that require an epoch if we don't have one
89 23
                    if (!$this->getCoordinateEpoch() && in_array($operation['method'], [
90 22
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN,
91 22
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D,
92 22
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D,
93 22
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC,
94 22
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D,
95 22
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D,
96 22
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
97 22
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
98 23
                        ], true)) {
99 1
                        $ok = false;
100
                    }
101
102
                    //filter out operations that require a specific epoch
103 23
                    if ($this->getCoordinateEpoch() && in_array($operation['method'], [
104 1
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
105 1
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
106 23
                        ], true)) {
107 1
                        $params = CoordinateOperationParams::getParamData($pathStep['operation']);
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateOpera...ordinateOperationParams was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
108 1
                        $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
109 1
                        $ok = $ok && (abs($pointEpoch->getValue() - $params['Transformation reference epoch']['value']) <= 0.001);
110
                    }
111
                }
112
            }
113
114 23
            if ($ok) {
115 22
                return $candidatePath['path'];
116
            }
117
        }
118
119 7
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
120
    }
121
122
    /**
123
     * Build the set of *all* possible paths that lead from the current CRS to the target CRS.
124
     */
125 23
    protected function buildTransformationPathsToCRS(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target): array
126
    {
127 23
        $cacheKey = $source->getSRID() . '|' . $target->getSRID();
128 23
        if (!isset(self::$pathCache[$cacheKey])) {
129 21
            $simplePaths = [];
130
131
            // Try a simple direct match before doing anything more complex!
132 21
            if (CRSTransformations::getSupportedTransformationsForCRSPair($source->getSRID(), $target->getSRID())) {
133 16
                $simplePaths[] = [$source->getSRID(), $target->getSRID()];
134
            } else { // Otherwise, recursively calculate permutations of intermediate CRSs
135 11
                $visited = [];
136 11
                foreach (CoordinateReferenceSystem::getSupportedSRIDs() as $crs => $name) {
137 11
                    $visited[$crs] = false;
138
                }
139 11
                $currentPath = [];
140 11
                $this->DFS($source->getSRID(), $target->getSRID(), $visited, $currentPath, $simplePaths);
141
            }
142
143
            // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
144 21
            $candidatePaths = [];
145 21
            foreach ($simplePaths as $simplePath) {
146 20
                $transformationsToMakePath = [[]];
147 20
                $from = array_shift($simplePath);
148 20
                assert($from === $source->getSRID());
149
                do {
150 20
                    $to = array_shift($simplePath);
151 20
                    $wipTransformationsInPath = [];
152 20
                    foreach (CRSTransformations::getSupportedTransformationsForCRSPair($from, $to) as $transformation) {
153 20
                        foreach ($transformationsToMakePath as $transformationToMakePath) {
154 20
                            $wipTransformationsInPath[] = array_merge($transformationToMakePath, [$transformation]);
155
                        }
156
                    }
157
158 20
                    $transformationsToMakePath = $wipTransformationsInPath;
159 20
                    $from = $to;
160 20
                } while (count($simplePath) > 0);
161 20
                assert($to === $target->getSRID());
162 20
                $candidatePaths = array_merge($candidatePaths, $transformationsToMakePath);
163
            }
164
165 21
            $candidates = [];
166 21
            foreach ($candidatePaths as $candidatePath) {
167 20
                $candidates[] = ['path' => $candidatePath, 'accuracy' => array_sum(array_column($candidatePath, 'accuracy'))];
168
            }
169
170 21
            self::$pathCache[$cacheKey] = $candidates;
171
        }
172
173 23
        return self::$pathCache[$cacheKey];
174
    }
175
176 11
    protected function DFS(string $u, string $v, array &$visited, array &$currentPath, array &$simplePaths): void
177
    {
178 11
        $currentPath[] = $u;
179 11
        if ($u === $v) {
180 9
            $simplePaths[] = $currentPath;
181
        } else {
182 11
            $visited[$u] = true;
183 11
            if (count($currentPath) <= $this->maxChainDepth) {
184 11
                foreach (CRSTransformations::getSupportedTransformationsForCRS($u) as $nextU) {
185 11
                    if (!$visited[$nextU]) {
186 11
                        $this->DFS($nextU, $v, $visited, $currentPath, $simplePaths);
187
                    }
188
                }
189
            }
190
        }
191
192 11
        array_pop($currentPath);
193 11
        $visited[$u] = false;
194 11
    }
195
196
    /**
197
     * Boundary polygons are defined as WGS84, so theoretically all that needs to happen is
198
     * to conversion to WGS84 by calling ->convert(). However, that leads quickly to either circularity
199
     * when a conversion is possible, or an exception because not every CRS has a WGS84 transformation
200
     * available to it even when chaining.
201
     */
202 22
    protected function getPointForBoundaryCheck(): ?GeographicValue
203
    {
204 22
        if ($this instanceof CompoundPoint) {
205 4
            $point = $this->getHorizontalPoint();
206
        } else {
207 18
            $point = $this;
208
        }
209
210
        try {
211
            // try converting to WGS84 if possible...
212 22
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->asGeographicValue();
0 ignored issues
show
Bug introduced by
The method convert() does not exist on PHPCoord\Point. It seems like you code against a sub-type of said class. However, the method does not exist in PHPCoord\VerticalPoint. Are you sure you never get one of those? ( Ignorable by Annotation )

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

212
            return $point->/** @scrutinizer ignore-call */ convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->asGeographicValue();
Loading history...
Bug introduced by
The method asGeographicValue() does not exist on PHPCoord\Point. It seems like you code against a sub-type of PHPCoord\Point such as PHPCoord\GeographicPoint or PHPCoord\GeocentricPoint. ( Ignorable by Annotation )

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

212
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
213
        } catch (UnknownConversionException $e) {
214
            /*
215
             * If Projected then either the point is inside the boundary by definition
216
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
217
             */
218
            if ($point instanceof ProjectedPoint) {
219
                return null;
220
            }
221
222
            /*
223
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
224
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
225
             * ≈1km so the error bound should be acceptable within the area of interest
226
             */
227
            if ($point instanceof GeographicPoint) {
228
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
229
            }
230
231
            if ($point instanceof GeocentricPoint) {
232
                $asGeographic = $point->asGeographicValue();
233
234
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
235
            }
236
        }
237
    }
238
239
    abstract public function getCRS(): CoordinateReferenceSystem;
240
241
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
242
243
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
244
}
245