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

buildIndirectTransformationPathsToCRS()   B

Complexity

Conditions 11
Paths 3

Size

Total Lines 51
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 11
eloc 29
c 2
b 0
f 0
nc 3
nop 2
dl 0
loc 51
ccs 31
cts 31
cp 1
crap 11
rs 7.3166

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