Passed
Push — master ( 6a435f...dad6c0 )
by Doug
13:08
created

AutoConversion::validatePath()   C

Complexity

Conditions 13
Paths 20

Size

Total Lines 40
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 13.0211

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 13
eloc 19
c 4
b 0
f 0
nc 20
nop 2
dl 0
loc 40
ccs 19
cts 20
cp 0.95
crap 13.0211
rs 6.6166

How to fix   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_shift;
15
use function array_sum;
16
use function assert;
17
use function class_exists;
18
use function count;
19
use DateTimeImmutable;
20
use function end;
21
use Generator;
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 = 5; // if traits could have constants...
42
43
    protected static array $methodsThatRequireCoordinateEpoch = [ // if traits could have constants...
44
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN,
45
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D,
46
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D,
47
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC,
48
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D,
49
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D,
50
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
51
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
52
    ];
53
54
    protected static array $methodsThatRequireASpecificEpoch = [ // if traits could have constants...
55
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
56
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
57
    ];
58
59
    private static array $pathCache = [];
60
61
    private static array $transformationsByCRS = [];
62
63
    private static array $transformationsByCRSPair = [];
64
65 452
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
66
    {
67 452
        if ($this->getCRS() == $to) {
68 216
            return $this;
69
        }
70
71 272
        if (strpos($this->getCRS()->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0 || strpos($to->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0) {
72 18
            throw new UnknownConversionException('Automatic conversions are only supported for EPSG CRSs');
73
        }
74
75 263
        $point = $this;
76 263
        $path = $this->findOperationPath($this->getCRS(), $to, $ignoreBoundaryRestrictions);
77
78 245
        foreach ($path as $step) {
79 245
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
80 245
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
81
        }
82
83 245
        return $point;
84
    }
85
86 263
    protected function findOperationPath(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target, bool $ignoreBoundaryRestrictions): array
87
    {
88 263
        self::buildSupportedTransformationsByCRS();
89 263
        self::buildSupportedTransformationsByCRSPair();
90 263
        $boundaryCheckPoint = $ignoreBoundaryRestrictions ? null : $this->getPointForBoundaryCheck();
91
92
        // Iteratively calculate permutations of intermediate CRSs
93 263
        foreach ($this->buildTransformationPathsToCRS($source, $target) as $candidatePaths) {
94 263
            usort($candidatePaths, static function (array $a, array $b) {
95 161
                return $a['accuracy'] <=> $b['accuracy'];
96 263
            });
97
98 263
            foreach ($candidatePaths as $candidatePath) {
99 263
                if ($this->validatePath($candidatePath['path'], $boundaryCheckPoint)) {
100 245
                    return $candidatePath['path'];
101
                }
102
            }
103
        }
104
105 72
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
106
    }
107
108 263
    protected function validatePath(array $candidatePath, ?GeographicValue $boundaryCheckPoint): bool
109
    {
110 263
        foreach ($candidatePath as $pathStep) {
111 263
            $operation = CoordinateOperations::getOperationData($pathStep['operation']);
112 263
            if ($boundaryCheckPoint) {
113
                //filter out operations that only operate outside this point
114 218
                $polygon = BoundingArea::createFromExtentCodes($operation['extent_code']);
115 218
                if (!$polygon->containsPoint($boundaryCheckPoint)) {
116 81
                    return false;
117
                }
118
            }
119
120 254
            $operations = static::resolveConcatenatedOperations($pathStep['operation'], false);
121
122 254
            foreach ($operations as $operationId => $operation) {
123
                //filter out operations that require an epoch if we don't have one
124 254
                if (isset(self::$methodsThatRequireCoordinateEpoch[$operation['method']]) && !$this->getCoordinateEpoch()) {
125 9
                    return false;
126
                }
127
128 245
                $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...
129
130
                //filter out operations that require a specific epoch
131 245
                if (isset(self::$methodsThatRequireASpecificEpoch[$operation['method']]) && $this->getCoordinateEpoch()) {
132 9
                    $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
133 9
                    if (!(abs($pointEpoch->getValue() - $params['transformationReferenceEpoch']['value']) <= 0.001)) {
134
                        return false;
135
                    }
136
                }
137
138
                //filter out operations that require a grid file that we don't have
139 245
                foreach ($params as $param) {
140 245
                    if (isset($param['fileProvider']) && !class_exists($param['fileProvider'])) {
141 72
                        return false;
142
                    }
143
                }
144
            }
145
        }
146
147 245
        return true;
148
    }
149
150
    /**
151
     * Build the set of possible paths that lead from the current CRS to the target CRS.
152
     */
153 263
    protected function buildTransformationPathsToCRS(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target): Generator
154
    {
155 263
        $iterations = 1;
156 263
        $sourceSRID = $source->getSRID();
157 263
        $targetSRID = $target->getSRID();
158 263
        $simplePaths = [[$sourceSRID]];
159
160 263
        while ($iterations < $this->maxChainDepth) {
161 263
            $cacheKey = $sourceSRID . '|' . $targetSRID . '|' . $iterations;
162
163 263
            if (!isset(self::$pathCache[$cacheKey])) {
164 227
                $completePaths = [];
165
166 227
                foreach ($simplePaths as $key => $simplePath) {
167 227
                    $current = end($simplePath);
168 227
                    if ($current === $targetSRID) {
169 218
                        $completePaths[] = $simplePath;
170 227
                    } elseif (isset(static::$transformationsByCRS[$current])) {
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...
171 227
                        foreach (static::$transformationsByCRS[$current] as $next) {
172 227
                            if (!in_array($next, $simplePath, true)) {
173 227
                                $simplePaths[] = array_merge($simplePath, [$next]);
174
                            }
175
                        }
176
                    }
177 227
                    unset($simplePaths[$key]);
178
                }
179
180
                // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
181 227
                $fullPaths = $this->expandSimplePaths($completePaths, $sourceSRID, $targetSRID);
182
183 227
                $paths = [];
184 227
                foreach ($fullPaths as $fullPath) {
185 218
                    $paths[] = ['path' => $fullPath, 'accuracy' => array_sum(array_column($fullPath, 'accuracy'))];
186
                }
187
188 227
                self::$pathCache[$cacheKey] = $paths;
189
            }
190
191 263
            ++$iterations;
192 263
            yield self::$pathCache[$cacheKey];
193
        }
194 72
    }
195
196 227
    protected function expandSimplePaths(array $simplePaths, string $fromSRID, string $toSRID): array
197
    {
198 227
        $fullPaths = [];
199 227
        foreach ($simplePaths as $simplePath) {
200 218
            $transformationsToMakePath = [[]];
201 218
            $from = array_shift($simplePath);
202 218
            assert($from === $fromSRID);
203
            do {
204 218
                $to = array_shift($simplePath);
205 218
                $wipTransformationsInPath = [];
206 218
                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...
207 218
                    foreach ($transformationsToMakePath as $transformationToMakePath) {
208 218
                        $wipTransformationsInPath[] = array_merge($transformationToMakePath, [$transformation]);
209
                    }
210
                }
211
212 218
                $transformationsToMakePath = $wipTransformationsInPath;
213 218
                $from = $to;
214 218
            } while (count($simplePath) > 0);
215 218
            assert($to === $toSRID);
216
217 218
            foreach ($transformationsToMakePath as $transformationToMakePath) {
218 218
                $fullPaths[] = $transformationToMakePath;
219
            }
220
        }
221
222 227
        return $fullPaths;
223
    }
224
225
    /**
226
     * Boundary polygons are defined as WGS84, so theoretically all that needs to happen is
227
     * to conversion to WGS84 by calling ->convert(). However, that leads quickly to either circularity
228
     * when a conversion is possible, or an exception because not every CRS has a WGS84 transformation
229
     * available to it even when chaining.
230
     */
231 245
    protected function getPointForBoundaryCheck(): ?GeographicValue
232
    {
233 245
        if ($this instanceof CompoundPoint) {
234 37
            $point = $this->getHorizontalPoint();
235
        } else {
236 208
            $point = $this;
237
        }
238
239
        try {
240
            // try converting to WGS84 if possible...
241 245
            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

241
            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

241
            return $point->/** @scrutinizer ignore-call */ convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->asGeographicValue();
Loading history...
242 18
        } catch (UnknownConversionException $e) {
243
            /*
244
             * If Projected then either the point is inside the boundary by definition
245
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
246
             */
247 18
            if ($point instanceof ProjectedPoint) {
248
                return null;
249
            }
250
251
            /*
252
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
253
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
254
             * ≈1km so the error bound should be acceptable within the area of interest
255
             */
256 18
            if ($point instanceof GeographicPoint) {
257
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
258
            }
259
260 18
            if ($point instanceof GeocentricPoint) {
261 18
                $asGeographic = $point->asGeographicValue();
262
263 18
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
264
            }
265
        }
266
    }
267
268 263
    protected static function buildSupportedTransformationsByCRS(): void
269
    {
270 263
        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...
271 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...
272 27
                if (!isset(static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']])) {
273 27
                    static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']] = $transformation['target_crs'];
274
                }
275 27
                if ($transformation['reversible'] && !isset(static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']])) {
276 27
                    static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']] = $transformation['source_crs'];
277
                }
278
            }
279
        }
280 263
    }
281
282 263
    protected static function buildSupportedTransformationsByCRSPair(): void
283
    {
284 263
        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...
285 27
            foreach (CRSTransformations::getSupportedTransformations() as $key => $transformation) {
286 27
                if (!isset(static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key])) {
287 27
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key] = $transformation;
288 27
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key]['in_reverse'] = false;
289
                }
290 27
                if ($transformation['reversible'] && !isset(static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key])) {
291 27
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key] = $transformation;
292 27
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key]['in_reverse'] = true;
293
                }
294
            }
295
        }
296 263
    }
297
298
    abstract public function getCRS(): CoordinateReferenceSystem;
299
300
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
301
302
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
303
}
304