Passed
Push — master ( aca036...b09582 )
by Doug
83:35 queued 68:07
created

AutoConversion::validatePath()   C

Complexity

Conditions 13
Paths 20

Size

Total Lines 52
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 13.0051

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 13
eloc 31
c 2
b 0
f 0
nc 20
nop 2
dl 0
loc 52
ccs 31
cts 32
cp 0.9688
crap 13.0051
rs 6.6166

How to fix   Long Method    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\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 class_exists;
19
use function count;
20
use DateTimeImmutable;
21
use function in_array;
22
use PHPCoord\CompoundPoint;
23
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
24
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
25
use PHPCoord\Exception\UnknownConversionException;
26
use PHPCoord\GeocentricPoint;
27
use PHPCoord\GeographicPoint;
28
use PHPCoord\Geometry\BoundingArea;
29
use PHPCoord\Point;
30
use PHPCoord\ProjectedPoint;
31
use PHPCoord\UnitOfMeasure\Time\Year;
32
use function strpos;
33
use function usort;
34
35
/**
36
 * @internal
37
 */
38
trait AutoConversion
39
{
40
    private int $maxChainDepth = 4; // if traits could have constants...
41
42
    private static array $directTransformationPathCache = [];
43
44
    private static array $indirectTransformationPathCache = [];
45
46
    private static array $transformationsByCRS = [];
47
48
    private static array $transformationsByCRSPair = [];
49
50 423
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
51
    {
52 423
        if ($this->getCRS() == $to) {
53 207
            return $this;
54
        }
55
56 243
        if (strpos($this->getCRS()->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0 || strpos($to->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0) {
57 18
            throw new UnknownConversionException('Automatic conversions are only supported for EPSG CRSs');
58
        }
59
60 234
        $point = $this;
61 234
        $path = $this->findOperationPath($this->getCRS(), $to, $ignoreBoundaryRestrictions);
62
63 225
        foreach ($path as $step) {
64 225
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
65 225
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
66
        }
67
68 225
        return $point;
69
    }
70
71 234
    protected function findOperationPath(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target, bool $ignoreBoundaryRestrictions): array
72
    {
73 234
        self::buildSupportedTransformationsByCRS();
74 234
        self::buildSupportedTransformationsByCRSPair();
75 234
        $boundaryCheckPoint = $ignoreBoundaryRestrictions ? null : $this->getPointForBoundaryCheck();
76
77
        // Try simple direct match before doing anything more complex!
78 234
        $candidatePaths = $this->buildDirectTransformationPathsToCRS($source, $target);
79
80 234
        usort($candidatePaths, static function (array $a, array $b) {
81 72
            return count($a['path']) <=> count($b['path']) ?: $a['accuracy'] <=> $b['accuracy'];
82 234
        });
83
84 234
        foreach ($candidatePaths as $candidatePath) {
85 189
            if ($this->validatePath($candidatePath['path'], $boundaryCheckPoint)) {
86 171
                return $candidatePath['path'];
87
            }
88
        }
89
90
        // Otherwise, recursively calculate permutations of intermediate CRSs
91 126
        $candidatePaths = $this->buildIndirectTransformationPathsToCRS($source, $target);
92
93 117
        usort($candidatePaths, static function (array $a, array $b) {
94 99
            return count($a['path']) <=> count($b['path']) ?: $a['accuracy'] <=> $b['accuracy'];
95 117
        });
96
97 117
        foreach ($candidatePaths as $candidatePath) {
98 117
            if ($this->validatePath($candidatePath['path'], $boundaryCheckPoint)) {
99 108
                return $candidatePath['path'];
100
            }
101
        }
102
103 54
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
104
    }
105
106 234
    protected function validatePath(array $candidatePath, ?GeographicValue $boundaryCheckPoint): bool
107
    {
108 234
        foreach ($candidatePath as $pathStep) {
109 234
            $operation = CoordinateOperations::getOperationData($pathStep['operation']);
110 234
            if ($boundaryCheckPoint) {
111
                //filter out operations that only operate outside this point
112 189
                $polygon = BoundingArea::createFromExtentCodes($operation['extent_code']);
113 189
                if (!$polygon->containsPoint($boundaryCheckPoint)) {
114 81
                    return false;
115
                }
116
            }
117
118 225
            $operations = static::resolveConcatenatedOperations($pathStep['operation'], false);
119
120 225
            foreach ($operations as $operation) {
121
                //filter out operations that require an epoch if we don't have one
122 225
                if (!$this->getCoordinateEpoch() && in_array($operation['method'], [
123 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN,
124 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D,
125 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D,
126 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC,
127 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D,
128 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D,
129 216
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
130 216
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
131 225
                    ], true)) {
132 9
                    return false;
133
                }
134
135 225
                $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...
136
137
                //filter out operations that require a specific epoch
138 225
                if ($this->getCoordinateEpoch() && in_array($operation['method'], [
139 9
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
140 9
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
141 225
                    ], true)) {
142 9
                    $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
143 9
                    if (!(abs($pointEpoch->getValue() - $params['Transformation reference epoch']['value']) <= 0.001)) {
144
                        return false;
145
                    }
146
                }
147
148
                //filter out operations that require a grid file that we don't have
149 225
                foreach ($params as $param) {
150 225
                    if (isset($param['fileProvider']) && !class_exists($param['fileProvider'])) {
151 54
                        return false;
152
                    }
153
                }
154
            }
155
        }
156
157 225
        return true;
158
    }
159
160
    /**
161
     * Build the set of possible direct paths that lead from the current CRS to the target CRS.
162
     */
163 234
    protected function buildDirectTransformationPathsToCRS(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target): array
164
    {
165 234
        $cacheKey = $source->getSRID() . '|' . $target->getSRID();
166 234
        if (!isset(self::$directTransformationPathCache[$cacheKey])) {
167 198
            $simplePaths = [[$source->getSRID(), $target->getSRID()]];
168
169
            // Expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
170 198
            $fullPaths = $this->expandSimplePaths($simplePaths, $source->getSRID(), $target->getSRID());
171
172 198
            $paths = [];
173 198
            foreach ($fullPaths as $fullPath) {
174 153
                $paths[] = ['path' => $fullPath, 'accuracy' => array_sum(array_column($fullPath, 'accuracy'))];
175
            }
176
177 198
            self::$directTransformationPathCache[$cacheKey] = $paths;
178
        }
179
180 234
        return self::$directTransformationPathCache[$cacheKey];
181
    }
182
183
    /**
184
     * Build the set of possible indirect paths that lead from the current CRS to the target CRS.
185
     */
186 126
    protected function buildIndirectTransformationPathsToCRS(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target): array
187
    {
188 126
        $cacheKey = $source->getSRID() . '|' . $target->getSRID();
189 126
        if (!isset(self::$indirectTransformationPathCache[$cacheKey])) {
190 126
            $simplePaths = [];
191 126
            $visited = [];
192 126
            foreach (CoordinateReferenceSystem::getSupportedSRIDs() as $crs => $name) {
193 126
                $visited[$crs] = false;
194
            }
195 126
            $currentPath = [];
196 126
            $this->DFS($source->getSRID(), $target->getSRID(), $visited, $currentPath, $simplePaths);
197
198
            // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
199 117
            $fullPaths = $this->expandSimplePaths($simplePaths, $source->getSRID(), $target->getSRID());
200
201 117
            $paths = [];
202 117
            foreach ($fullPaths as $fullPath) {
203 108
                $paths[] = ['path' => $fullPath, 'accuracy' => array_sum(array_column($fullPath, 'accuracy'))];
204
            }
205
206 117
            self::$indirectTransformationPathCache[$cacheKey] = $paths;
207
        }
208
209 117
        return self::$indirectTransformationPathCache[$cacheKey];
210
    }
211
212 126
    protected function DFS(string $u, string $v, array &$visited, array &$currentPath, array &$simplePaths): void
213
    {
214 126
        $currentPath[] = $u;
215 126
        if ($u === $v) {
216 108
            $simplePaths[] = $currentPath;
217
        } else {
218 126
            $visited[$u] = true;
219 126
            if (count($currentPath) <= $this->maxChainDepth) {
220 126
                foreach (static::$transformationsByCRS[$u] as $nextU) {
0 ignored issues
show
Bug introduced by
Since $transformationsByCRS is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $transformationsByCRS to at least protected.
Loading history...
221 117
                    if (!$visited[$nextU]) {
222 117
                        $this->DFS($nextU, $v, $visited, $currentPath, $simplePaths);
223
                    }
224
                }
225
            }
226
        }
227
228 117
        array_pop($currentPath);
229 117
        $visited[$u] = false;
230 117
    }
231
232 207
    protected function expandSimplePaths(array $simplePaths, string $fromSRID, string $toSRID): array
233
    {
234 207
        $fullPaths = [];
235 207
        foreach ($simplePaths as $simplePath) {
236 207
            $transformationsToMakePath = [[]];
237 207
            $from = array_shift($simplePath);
238 207
            assert($from === $fromSRID);
239
            do {
240 207
                $to = array_shift($simplePath);
241 207
                $wipTransformationsInPath = [];
242 207
                foreach (static::$transformationsByCRSPair[$from . '|' . $to] ?? [] as $transformation) {
0 ignored issues
show
Bug introduced by
Since $transformationsByCRSPair is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $transformationsByCRSPair to at least protected.
Loading history...
243 198
                    foreach ($transformationsToMakePath as $transformationToMakePath) {
244 198
                        $wipTransformationsInPath[] = array_merge($transformationToMakePath, [$transformation]);
245
                    }
246
                }
247
248 207
                $transformationsToMakePath = $wipTransformationsInPath;
249 207
                $from = $to;
250 207
            } while (count($simplePath) > 0);
251 207
            assert($to === $toSRID);
252
253 207
            foreach ($transformationsToMakePath as $transformationToMakePath) {
254 198
                $fullPaths[] = $transformationToMakePath;
255
            }
256
        }
257
258 207
        return $fullPaths;
259
    }
260
261
    /**
262
     * Boundary polygons are defined as WGS84, so theoretically all that needs to happen is
263
     * to conversion to WGS84 by calling ->convert(). However, that leads quickly to either circularity
264
     * when a conversion is possible, or an exception because not every CRS has a WGS84 transformation
265
     * available to it even when chaining.
266
     */
267 216
    protected function getPointForBoundaryCheck(): ?GeographicValue
268
    {
269 216
        if ($this instanceof CompoundPoint) {
270 36
            $point = $this->getHorizontalPoint();
271
        } else {
272 180
            $point = $this;
273
        }
274
275
        try {
276
            // try converting to WGS84 if possible...
277 216
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->asGeographicValue();
0 ignored issues
show
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

277
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
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

277
            return $point->/** @scrutinizer ignore-call */ convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->asGeographicValue();
Loading history...
278
        } catch (UnknownConversionException $e) {
279
            /*
280
             * If Projected then either the point is inside the boundary by definition
281
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
282
             */
283
            if ($point instanceof ProjectedPoint) {
284
                return null;
285
            }
286
287
            /*
288
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
289
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
290
             * ≈1km so the error bound should be acceptable within the area of interest
291
             */
292
            if ($point instanceof GeographicPoint) {
293
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
294
            }
295
296
            if ($point instanceof GeocentricPoint) {
297
                $asGeographic = $point->asGeographicValue();
298
299
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
300
            }
301
        }
302
    }
303
304 234
    protected static function buildSupportedTransformationsByCRS(): void
305
    {
306 234
        if (!static::$transformationsByCRS) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::transformationsByCRS 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...
Bug introduced by
Since $transformationsByCRS is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $transformationsByCRS to at least protected.
Loading history...
307 27
            foreach (CRSTransformations::getSupportedTransformations() as $transformation) {
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateOperation\CRSTransformations 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...
308 27
                if (!isset(static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']])) {
309 27
                    static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']] = $transformation['target_crs'];
310
                }
311 27
                if ($transformation['reversible'] && !isset(static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']])) {
312 27
                    static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']] = $transformation['source_crs'];
313
                }
314
            }
315
        }
316 234
    }
317
318 234
    protected static function buildSupportedTransformationsByCRSPair(): void
319
    {
320 234
        if (!static::$transformationsByCRSPair) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::transformationsByCRSPair 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...
Bug introduced by
Since $transformationsByCRSPair is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $transformationsByCRSPair to at least protected.
Loading history...
321 27
            foreach (CRSTransformations::getSupportedTransformations() as $key => $transformation) {
322 27
                if (!isset(static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key])) {
323 27
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key] = $transformation;
324 27
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key]['in_reverse'] = false;
325
                }
326 27
                if ($transformation['reversible'] && !isset(static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key])) {
327 27
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key] = $transformation;
328 27
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key]['in_reverse'] = true;
329
                }
330
            }
331
        }
332 234
    }
333
334
    abstract public function getCRS(): CoordinateReferenceSystem;
335
336
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
337
338
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
339
}
340