Passed
Push — master ( 2a4317...e180e7 )
by Doug
47:34
created

AutoConversion   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 290
Duplicated Lines 0 %

Test Coverage

Coverage 98.7%

Importance

Changes 19
Bugs 0 Features 0
Metric Value
eloc 149
dl 0
loc 290
ccs 152
cts 154
cp 0.987
rs 3.12
c 19
b 0
f 0
wmc 66

8 Methods

Rating   Name   Duplication   Size   Complexity  
A findOperationPath() 0 15 5
A convert() 0 15 4
D validatePath() 0 51 26
A getPointForBoundaryCheck() 0 33 6
A buildSupportedTransformationsByCRSPair() 0 33 4
B buildTransformationPathsToCRS() 0 45 9
B buildSupportedTransformationsByCRS() 0 33 6
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 DateTimeImmutable;
12
use PHPCoord\CompoundPoint;
13
use PHPCoord\CoordinateReferenceSystem\Compound;
14
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
15
use PHPCoord\CoordinateReferenceSystem\Geocentric;
16
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
17
use PHPCoord\CoordinateReferenceSystem\Geographic3D;
18
use PHPCoord\CoordinateReferenceSystem\Projected;
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateReferenceSystem\Projected 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...
19
use PHPCoord\CoordinateReferenceSystem\Vertical;
20
use PHPCoord\Exception\UnknownConversionException;
21
use PHPCoord\GeocentricPoint;
22
use PHPCoord\GeographicPoint;
23
use PHPCoord\Geometry\BoundingArea;
24
use PHPCoord\Geometry\RegionMap;
25
use PHPCoord\Point;
26
use PHPCoord\ProjectedPoint;
27
use PHPCoord\UnitOfMeasure\Time\Year;
28
29
use function abs;
30
use function array_column;
31
use function array_shift;
32
use function array_sum;
33
use function array_unique;
34
use function assert;
35
use function class_exists;
36
use function count;
37
use function in_array;
38
use function usort;
39
use function str_ends_with;
40
41
/**
42
 * @internal
43
 */
44
trait AutoConversion
45
{
46
    private int $maxChainDepth = 6; // if traits could have constants...
47
48
    private static array $completePathCache = [];
49
50 2391
    public function convert(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $to, bool $ignoreBoundaryRestrictions = false): Point
51
    {
52 2391
        if ($this->getCRS() == $to) {
53 226
            return $this;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this returns the type PHPCoord\CoordinateOperation\AutoConversion which is incompatible with the type-hinted return PHPCoord\Point.
Loading history...
54
        }
55
56 2211
        $point = $this;
57 2211
        $path = $this->findOperationPath($this->getCRS(), $to, $ignoreBoundaryRestrictions);
58
59 2175
        foreach ($path as $step) {
60 2175
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
61 2175
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
62
        }
63
64 2175
        return $point;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $point could return the type PHPCoord\CoordinateOperation\AutoConversion which is incompatible with the type-hinted return PHPCoord\Point. Consider adding an additional type-check to rule them out.
Loading history...
65
    }
66
67 2211
    protected function findOperationPath(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target, bool $ignoreBoundaryRestrictions): array
68
    {
69 2211
        $boundaryCheckPoint = $ignoreBoundaryRestrictions ? null : $this->getPointForBoundaryCheck();
70
71
        // Iteratively calculate permutations of intermediate CRSs
72 2211
        $candidatePaths = $this->buildTransformationPathsToCRS($source, $target);
73 2211
        usort($candidatePaths, static fn (array $a, array $b) => $a['accuracy'] <=> $b['accuracy'] ?: count($a['path']) <=> count($b['path']));
74
75 2211
        foreach ($candidatePaths as $candidatePath) {
76 2193
            if ($this->validatePath($candidatePath['path'], $target, $boundaryCheckPoint)) {
77 2175
                return $candidatePath['path'];
78
            }
79
        }
80
81 2000
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
82
    }
83
84 2193
    protected function validatePath(array $candidatePath, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target, ?GeographicValue $boundaryCheckPoint): bool
85
    {
86 2193
        foreach ($candidatePath as $pathStep) {
87 2193
            $operation = CoordinateOperations::getOperationData($pathStep['operation']);
88 2193
            if ($boundaryCheckPoint) {
89
                // filter out operations that only operate outside this point
90 2157
                $polygon = $operation['extent'] = $operation['extent'] instanceof BoundingArea ? $operation['extent'] : BoundingArea::createFromExtentCodes($operation['extent']);
91 2157
                if (!$polygon->containsPoint($boundaryCheckPoint)) {
92 109
                    return false;
93
                }
94
            }
95
96 2184
            $operation = CoordinateOperations::getOperationData($pathStep['operation']);
97 2184
            $methodParams = CoordinateOperationMethods::getMethodData($operation['method'])['paramData'];
98
99
            // filter out operations that use a 2D CRS as intermediate where this is a 3D point
100 2184
            $currentCRS = $this->getCRS();
101 2184
            if ($currentCRS instanceof Compound || $currentCRS instanceof Geocentric || $currentCRS instanceof Geographic3D || $currentCRS instanceof Vertical) {
102 59
                if ($target instanceof Compound || $target instanceof Geocentric || $target instanceof Geographic3D || $target instanceof Vertical) {
103 41
                    $intermediateTarget = CoordinateReferenceSystem::fromSRID($pathStep['in_reverse'] ? $pathStep['source_crs'] : $pathStep['target_crs']);
104 41
                    if ($intermediateTarget instanceof Geographic2D || $intermediateTarget instanceof Projected) {
105 1
                        return false;
106
                    }
107
                }
108
            }
109
110
            // filter out operations that require an epoch if we don't have one
111 2184
            if ((isset($methodParams['transformationReferenceEpoch']) || isset($methodParams['parameterReferenceEpoch'])) && !$this->getCoordinateEpoch()) {
112 96
                return false;
113
            }
114
115 2175
            $operationParams = CoordinateOperations::getParamData($pathStep['operation']);
116
117
            // filter out operations that require a specific epoch
118 2175
            if (isset($methodParams['transformationReferenceEpoch'])) {
119 9
                $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
0 ignored issues
show
Bug introduced by
It seems like $this->getCoordinateEpoch() can also be of type null; however, parameter $dateTime of PHPCoord\UnitOfMeasure\Time\Year::fromDateTime() does only seem to accept DateTimeInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
                $pointEpoch = Year::fromDateTime(/** @scrutinizer ignore-type */ $this->getCoordinateEpoch());
Loading history...
120 9
                if (!(abs($pointEpoch->subtract($operationParams['transformationReferenceEpoch'])->getValue()) <= 0.001)) {
121
                    return false;
122
                }
123
            }
124
125
            // filter out operations that require a grid file that we don't have, or where boundaries are not being
126
            // checked (a formula-based conversion will always return *a* result, outside a grid boundary does not...)
127 2175
            foreach ($operationParams as $paramName => $paramValue) {
128 2175
                if (str_ends_with($paramName, 'File') && $paramValue !== null && (!$boundaryCheckPoint || !class_exists($paramValue))) {
129 164
                    return false;
130
                }
131
            }
132
        }
133
134 2175
        return true;
135
    }
136
137
    /**
138
     * Build the set of possible paths that lead from the current CRS to the target CRS.
139
     */
140 2211
    protected function buildTransformationPathsToCRS(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target): array
141
    {
142 2211
        $iterations = 0;
143 2211
        $sourceSRID = $source->getSRID();
144 2211
        $targetSRID = $target->getSRID();
145 2211
        $previousSimplePaths = [[$sourceSRID]];
146 2211
        $cacheKey = $sourceSRID . '|' . $targetSRID;
147
148 2211
        if (!isset(self::$completePathCache[$cacheKey])) {
149 411
            $transformationsByCRS = self::buildSupportedTransformationsByCRS($source, $target);
150 411
            $transformationsByCRSPair = self::buildSupportedTransformationsByCRSPair($source, $target);
151 411
            self::$completePathCache[$cacheKey] = [];
152
153 411
            while ($iterations <= $this->maxChainDepth) {
154 411
                $completePaths = [];
155 411
                $simplePaths = [];
156
157 411
                foreach ($previousSimplePaths as $simplePath) {
158 411
                    $current = $simplePath[$iterations];
159 411
                    if ($current === $targetSRID) {
160 384
                        $completePaths[] = $simplePath;
161 411
                    } elseif (isset($transformationsByCRS[$current])) {
162 402
                        foreach ($transformationsByCRS[$current] as $next) {
163 402
                            if (!in_array($next, $simplePath, true)) {
164 402
                                $simplePaths[] = [...$simplePath, $next];
165
                            }
166
                        }
167
                    }
168
                }
169
170
                // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
171 411
                $fullPaths = $this->expandSimplePaths($transformationsByCRSPair, $completePaths, $sourceSRID, $targetSRID);
172
173 411
                $paths = [];
174 411
                foreach ($fullPaths as $fullPath) {
175 384
                    $paths[] = ['path' => $fullPath, 'accuracy' => array_sum(array_column($fullPath, 'accuracy'))];
176
                }
177
178 411
                $previousSimplePaths = $simplePaths;
179 411
                self::$completePathCache[$cacheKey] = [...self::$completePathCache[$cacheKey], ...$paths];
180 411
                ++$iterations;
181
            }
182
        }
183
184 2211
        return self::$completePathCache[$cacheKey];
185
    }
186
187 411
    protected function expandSimplePaths(array $transformationsByCRSPair, array $simplePaths, string $fromSRID, string $toSRID): array
188
    {
189 411
        $fullPaths = [];
190 411
        foreach ($simplePaths as $simplePath) {
191 384
            $transformationsToMakePath = [[]];
192 384
            $from = array_shift($simplePath);
193 384
            assert($from === $fromSRID);
194
            do {
195 384
                $to = array_shift($simplePath);
196 384
                $wipTransformationsInPath = [];
197 384
                foreach ($transformationsByCRSPair[$from . '|' . $to] ?? [] as $transformation) {
198 384
                    foreach ($transformationsToMakePath as $transformationToMakePath) {
199 384
                        $wipTransformationsInPath[] = [...$transformationToMakePath, $transformation];
200
                    }
201
                }
202
203 384
                $transformationsToMakePath = $wipTransformationsInPath;
204 384
                $from = $to;
205 384
            } while (count($simplePath) > 0);
206 384
            assert($to === $toSRID);
207
208 384
            foreach ($transformationsToMakePath as $transformationToMakePath) {
209 384
                $fullPaths[] = $transformationToMakePath;
210
            }
211
        }
212
213 411
        return $fullPaths;
214
    }
215
216
    /**
217
     * Boundary polygons are defined as WGS84, so theoretically all that needs to happen is
218
     * to conversion to WGS84 by calling ->convert(). However, that leads quickly to either circularity
219
     * when a conversion is possible, or an exception because not every CRS has a WGS84 transformation
220
     * available to it even when chaining.
221
     */
222 2193
    protected function getPointForBoundaryCheck(): ?GeographicValue
223
    {
224 2193
        if ($this instanceof CompoundPoint) {
225 48
            $point = $this->getHorizontalPoint();
226
        } else {
227 2156
            $point = $this;
228
        }
229
230
        try {
231
            // try converting to WGS84 if possible...
232 2193
            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\ProjectedPoint. Did you maybe mean asGeographicPoint()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

232
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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

232
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
Bug introduced by
It seems like asGeographicValue() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

232
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
233 1935
        } catch (UnknownConversionException) {
234
            /*
235
             * If Projected then either the point is inside the boundary by definition
236
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
237
             */
238 1935
            if ($point instanceof ProjectedPoint) {
239
                return null;
240
            }
241
242
            /*
243
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
244
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
245
             * ≈1km so the error bound should be acceptable within the area of interest
246
             */
247 1935
            if ($point instanceof GeographicPoint) {
248 1926
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
249
            }
250
251 9
            if ($point instanceof GeocentricPoint) {
252 9
                $asGeographic = $point->asGeographicValue();
253
254 9
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
255
            }
256
        }
257
    }
258
259 411
    protected static function buildSupportedTransformationsByCRS(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target): array
260
    {
261 411
        $regions = array_unique([$source->getBoundingArea()->getRegion(), $target->getBoundingArea()->getRegion(), RegionMap::REGION_GLOBAL]);
262 411
        $relevantRegionData = CoordinateOperations::getCustomTransformations();
263 411
        foreach ($regions as $region) {
264 411
            $regionData = match ($region) {
265 411
                RegionMap::REGION_GLOBAL => CRSTransformationsGlobal::getSupportedTransformations(),
266 411
                RegionMap::REGION_AFRICA => CRSTransformationsAfrica::getSupportedTransformations(),
267 411
                RegionMap::REGION_ANTARCTIC => CRSTransformationsAntarctic::getSupportedTransformations(),
268 411
                RegionMap::REGION_ARCTIC => CRSTransformationsArctic::getSupportedTransformations(),
269 411
                RegionMap::REGION_ASIA => CRSTransformationsAsia::getSupportedTransformations(),
270 411
                RegionMap::REGION_EUROPE => CRSTransformationsEurope::getSupportedTransformations(),
271 411
                RegionMap::REGION_NORTHAMERICA => CRSTransformationsNorthAmerica::getSupportedTransformations(),
272 411
                RegionMap::REGION_OCEANIA => CRSTransformationsOceania::getSupportedTransformations(),
273 411
                RegionMap::REGION_SOUTHAMERICA => CRSTransformationsSouthAmerica::getSupportedTransformations(),
274 411
            };
275 411
            $relevantRegionData = [...$relevantRegionData, ...$regionData];
276
        }
277
278 411
        $transformationsByCRS = [];
279 411
        foreach ($relevantRegionData as $transformation) {
280 411
            $operation = CoordinateOperations::getOperationData($transformation['operation']);
281 411
            $method = CoordinateOperationMethods::getMethodData($operation['method']);
282
283 411
            if (!isset($transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']])) {
284 411
                $transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']] = $transformation['target_crs'];
285
            }
286 411
            if ($method['reversible'] && !isset($transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']])) {
287 411
                $transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']] = $transformation['source_crs'];
288
            }
289
        }
290
291 411
        return $transformationsByCRS;
292
    }
293
294 411
    protected static function buildSupportedTransformationsByCRSPair(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target): array
295
    {
296 411
        $regions = array_unique([$source->getBoundingArea()->getRegion(), $target->getBoundingArea()->getRegion(), RegionMap::REGION_GLOBAL]);
297 411
        $relevantRegionData = [];
298 411
        foreach ($regions as $region) {
299 411
            $regionData = match ($region) {
300 411
                RegionMap::REGION_GLOBAL => [...CRSTransformationsGlobal::getSupportedTransformations(), ...CoordinateOperations::getCustomTransformations()],
301 411
                RegionMap::REGION_AFRICA => CRSTransformationsAfrica::getSupportedTransformations(),
302 411
                RegionMap::REGION_ANTARCTIC => CRSTransformationsAntarctic::getSupportedTransformations(),
303 411
                RegionMap::REGION_ARCTIC => CRSTransformationsArctic::getSupportedTransformations(),
304 411
                RegionMap::REGION_ASIA => CRSTransformationsAsia::getSupportedTransformations(),
305 411
                RegionMap::REGION_EUROPE => CRSTransformationsEurope::getSupportedTransformations(),
306 411
                RegionMap::REGION_NORTHAMERICA => CRSTransformationsNorthAmerica::getSupportedTransformations(),
307 411
                RegionMap::REGION_OCEANIA => CRSTransformationsOceania::getSupportedTransformations(),
308 411
                RegionMap::REGION_SOUTHAMERICA => CRSTransformationsSouthAmerica::getSupportedTransformations(),
309 411
            };
310 411
            $relevantRegionData = [...$relevantRegionData, ...$regionData];
311
        }
312
313 411
        $transformationsByCRSPair = [];
314 411
        foreach ($relevantRegionData as $key => $transformation) {
315 411
            $operation = CoordinateOperations::getOperationData($transformation['operation']);
316 411
            $method = CoordinateOperationMethods::getMethodData($operation['method']);
317
318 411
            $transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key] = $transformation;
319 411
            $transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key]['in_reverse'] = false;
320 411
            if ($method['reversible']) {
321 411
                $transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key] = $transformation;
322 411
                $transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key]['in_reverse'] = true;
323
            }
324
        }
325
326 411
        return $transformationsByCRSPair;
327
    }
328
329
    abstract public function getCRS(): CoordinateReferenceSystem;
330
331
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
332
333
    abstract protected function performOperation(string $srid, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $to, bool $inReverse): Point;
334
}
335