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

AutoConversion   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Test Coverage

Coverage 93.29%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 142
c 8
b 0
f 0
dl 0
loc 301
ccs 139
cts 149
cp 0.9329
rs 3.36
wmc 63

10 Methods

Rating   Name   Duplication   Size   Complexity  
C validatePath() 0 52 13
A getPointForBoundaryCheck() 0 33 6
A buildSupportedTransformationsByCRSPair() 0 11 6
B findOperationPath() 0 33 8
A buildIndirectTransformationPathsToCRS() 0 24 4
A buildSupportedTransformationsByCRS() 0 9 6
A buildDirectTransformationPathsToCRS() 0 18 3
A convert() 0 19 6
A DFS() 0 18 5
A expandSimplePaths() 0 27 6

How to fix   Complexity   

Complex Class

Complex classes like AutoConversion often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AutoConversion, and based on these observations, apply Extract Interface, too.

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