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

AutoConversion::validatePath()   C

Complexity

Conditions 13
Paths 20

Size

Total Lines 52
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 13.0056

Importance

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

285
            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

285
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
286
        } catch (UnknownConversionException $e) {
287
            /*
288
             * If Projected then either the point is inside the boundary by definition
289
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
290
             */
291
            if ($point instanceof ProjectedPoint) {
292
                return null;
293
            }
294
295
            /*
296
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
297
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
298
             * ≈1km so the error bound should be acceptable within the area of interest
299
             */
300
            if ($point instanceof GeographicPoint) {
301
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
302
            }
303
304 252
            if ($point instanceof GeocentricPoint) {
305
                $asGeographic = $point->asGeographicValue();
306 252
307 27
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
308 27
            }
309 27
        }
310
    }
311 27
312 27
    protected static function buildSupportedTransformationsByCRS(): void
313
    {
314
        if (!static::$transformationsByCRS) {
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...
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...
315
            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...
316 252
                if (!isset(static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']])) {
317
                    static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']] = $transformation['target_crs'];
318 252
                }
319
                if ($transformation['reversible'] && !isset(static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']])) {
320 252
                    static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']] = $transformation['source_crs'];
321 27
                }
322 27
            }
323 27
        }
324 27
    }
325
326 27
    protected static function buildSupportedTransformationsByCRSPair(): void
327 27
    {
328 27
        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...
329
            foreach (CRSTransformations::getSupportedTransformations() as $key => $transformation) {
330
                if (!isset(static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key])) {
331
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key] = $transformation;
332 252
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key]['in_reverse'] = false;
333
                }
334
                if ($transformation['reversible'] && !isset(static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key])) {
335
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key] = $transformation;
336
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key]['in_reverse'] = true;
337
                }
338
            }
339
        }
340
    }
341
342
    abstract public function getCRS(): CoordinateReferenceSystem;
343
344
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
345
346
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
347
}
348