Passed
Push — master ( 6ef80c...2068f7 )
by Doug
23:26
created

AutoConversion   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Test Coverage

Coverage 97.16%

Importance

Changes 16
Bugs 0 Features 0
Metric Value
eloc 150
c 16
b 0
f 0
dl 0
loc 288
ccs 137
cts 141
cp 0.9716
rs 6.96
wmc 53

8 Methods

Rating   Name   Duplication   Size   Complexity  
C validatePath() 0 39 13
A getPointForBoundaryCheck() 0 33 6
A buildSupportedTransformationsByCRSPair() 0 30 4
A findOperationPath() 0 15 5
B buildTransformationPathsToCRS() 0 45 9
B buildSupportedTransformationsByCRS() 0 30 6
A convert() 0 15 4
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_shift;
14
use function array_sum;
15
use function array_unique;
16
use function assert;
17
use function class_exists;
18
use function count;
19
use DateTimeImmutable;
20
use function in_array;
21
use PHPCoord\CompoundPoint;
22
use PHPCoord\CoordinateReferenceSystem\Compound;
23
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
24
use PHPCoord\CoordinateReferenceSystem\Geocentric;
25
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
26
use PHPCoord\CoordinateReferenceSystem\Geographic3D;
27
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...
28
use PHPCoord\CoordinateReferenceSystem\Vertical;
29
use PHPCoord\Exception\UnknownConversionException;
30
use PHPCoord\GeocentricPoint;
31
use PHPCoord\GeographicPoint;
32
use PHPCoord\Geometry\BoundingArea;
33
use PHPCoord\Geometry\Extents\RegionMap;
0 ignored issues
show
Bug introduced by
The type PHPCoord\Geometry\Extents\RegionMap 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...
34
use PHPCoord\Point;
35
use PHPCoord\ProjectedPoint;
36
use PHPCoord\UnitOfMeasure\Time\Year;
37
use function usort;
38
39
/**
40
 * @internal
41
 */
42
trait AutoConversion
43
{
44
    private int $maxChainDepth = 6; // if traits could have constants...
45
46
    protected static array $methodsThatRequireCoordinateEpoch = [ // if traits could have constants...
47
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN,
48
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D,
49
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D,
50
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC,
51
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D,
52
        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D => CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D,
53
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
54
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
55
    ];
56
57
    protected static array $methodsThatRequireASpecificEpoch = [ // if traits could have constants...
58
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
59
        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN => CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
60
    ];
61
62
    private static array $completePathCache = [];
63
64 2308
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
65
    {
66 2308
        if ($this->getCRS() == $to) {
67 225
            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...
68
        }
69
70 2128
        $point = $this;
71 2128
        $path = $this->findOperationPath($this->getCRS(), $to, $ignoreBoundaryRestrictions);
72
73 2092
        foreach ($path as $step) {
74 2092
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
75 2092
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
76
        }
77
78 2092
        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...
79
    }
80
81 2128
    protected function findOperationPath(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target, bool $ignoreBoundaryRestrictions): array
82
    {
83 2128
        $boundaryCheckPoint = $ignoreBoundaryRestrictions ? null : $this->getPointForBoundaryCheck();
84
85
        // Iteratively calculate permutations of intermediate CRSs
86 2128
        $candidatePaths = $this->buildTransformationPathsToCRS($source, $target);
87 2128
        usort($candidatePaths, static fn (array $a, array $b) => $a['accuracy'] <=> $b['accuracy'] ?: count($a['path']) <=> count($b['path']));
88
89 2128
        foreach ($candidatePaths as $candidatePath) {
90 2110
            if ($this->validatePath($candidatePath['path'], $boundaryCheckPoint)) {
91 2092
                return $candidatePath['path'];
92
            }
93
        }
94
95 1926
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
96
    }
97
98 2110
    protected function validatePath(array $candidatePath, ?GeographicValue $boundaryCheckPoint): bool
99
    {
100 2110
        foreach ($candidatePath as $pathStep) {
101 2110
            $operation = CoordinateOperations::getOperationData($pathStep['operation']);
102 2110
            if ($boundaryCheckPoint) {
103
                // filter out operations that only operate outside this point
104 2074
                $polygon = BoundingArea::createFromExtentCodes($operation['extent_code']);
105 2074
                if (!$polygon->containsPoint($boundaryCheckPoint)) {
106 108
                    return false;
107
                }
108
            }
109
110 2101
            $operation = CoordinateOperations::getOperationData($pathStep['operation']);
111
112
            // filter out operations that require an epoch if we don't have one
113 2101
            if (isset(self::$methodsThatRequireCoordinateEpoch[$operation['method']]) && !$this->getCoordinateEpoch()) {
114 85
                return false;
115
            }
116
117 2092
            $params = CoordinateOperations::getParamData($pathStep['operation']);
118
119
            // filter out operations that require a specific epoch
120 2092
            if (isset(self::$methodsThatRequireASpecificEpoch[$operation['method']]) && $this->getCoordinateEpoch()) {
121 9
                $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
122 9
                if (!(abs($pointEpoch->getValue() - $params['transformationReferenceEpoch']['value']) <= 0.001)) {
123
                    return false;
124
                }
125
            }
126
127
            // filter out operations that require a grid file that we don't have, or where boundaries are not being
128
            // checked (a formula-based conversion will always return *a* result, outside a grid boundary does not...
129 2092
            foreach ($params as $param) {
130 2092
                if (isset($param['fileProvider']) && (!$boundaryCheckPoint || !class_exists($param['fileProvider']))) {
131 153
                    return false;
132
                }
133
            }
134
        }
135
136 2092
        return true;
137
    }
138
139
    /**
140
     * Build the set of possible paths that lead from the current CRS to the target CRS.
141
     */
142 2128
    protected function buildTransformationPathsToCRS(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target): array
143
    {
144 2128
        $iterations = 0;
145 2128
        $sourceSRID = $source->getSRID();
146 2128
        $targetSRID = $target->getSRID();
147 2128
        $previousSimplePaths = [[$sourceSRID]];
148 2128
        $cacheKey = $sourceSRID . '|' . $targetSRID;
149
150 2128
        if (!isset(self::$completePathCache[$cacheKey])) {
151 292
            $transformationsByCRS = self::buildSupportedTransformationsByCRS($source, $target);
152 292
            $transformationsByCRSPair = self::buildSupportedTransformationsByCRSPair($source, $target);
153 292
            self::$completePathCache[$cacheKey] = [];
154
155 292
            while ($iterations <= $this->maxChainDepth) {
156 292
                $completePaths = [];
157 292
                $simplePaths = [];
158
159 292
                foreach ($previousSimplePaths as $simplePath) {
160 292
                    $current = $simplePath[$iterations];
161 292
                    if ($current === $targetSRID) {
162 265
                        $completePaths[] = $simplePath;
163 292
                    } elseif (isset($transformationsByCRS[$current])) {
164 283
                        foreach ($transformationsByCRS[$current] as $next) {
165 283
                            if (!in_array($next, $simplePath, true)) {
166 283
                                $simplePaths[] = [...$simplePath, $next];
167
                            }
168
                        }
169
                    }
170
                }
171
172
                // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
173 292
                $fullPaths = $this->expandSimplePaths($transformationsByCRSPair, $completePaths, $sourceSRID, $targetSRID);
174
175 292
                $paths = [];
176 292
                foreach ($fullPaths as $fullPath) {
177 265
                    $paths[] = ['path' => $fullPath, 'accuracy' => array_sum(array_column($fullPath, 'accuracy'))];
178
                }
179
180 292
                $previousSimplePaths = $simplePaths;
181 292
                self::$completePathCache[$cacheKey] = [...self::$completePathCache[$cacheKey], ...$paths];
182 292
                ++$iterations;
183
            }
184
        }
185
186 2128
        return self::$completePathCache[$cacheKey];
187
    }
188
189 292
    protected function expandSimplePaths(array $transformationsByCRSPair, array $simplePaths, string $fromSRID, string $toSRID): array
190
    {
191 292
        $fullPaths = [];
192 292
        foreach ($simplePaths as $simplePath) {
193 265
            $transformationsToMakePath = [[]];
194 265
            $from = array_shift($simplePath);
195 265
            assert($from === $fromSRID);
196
            do {
197 265
                $to = array_shift($simplePath);
198 265
                $wipTransformationsInPath = [];
199 265
                foreach ($transformationsByCRSPair[$from . '|' . $to] ?? [] as $transformation) {
200 265
                    foreach ($transformationsToMakePath as $transformationToMakePath) {
201 265
                        $wipTransformationsInPath[] = [...$transformationToMakePath, $transformation];
202
                    }
203
                }
204
205 265
                $transformationsToMakePath = $wipTransformationsInPath;
206 265
                $from = $to;
207 265
            } while (count($simplePath) > 0);
208 265
            assert($to === $toSRID);
209
210 265
            foreach ($transformationsToMakePath as $transformationToMakePath) {
211 265
                $fullPaths[] = $transformationToMakePath;
212
            }
213
        }
214
215 292
        return $fullPaths;
216
    }
217
218
    /**
219
     * Boundary polygons are defined as WGS84, so theoretically all that needs to happen is
220
     * to conversion to WGS84 by calling ->convert(). However, that leads quickly to either circularity
221
     * when a conversion is possible, or an exception because not every CRS has a WGS84 transformation
222
     * available to it even when chaining.
223
     */
224 2110
    protected function getPointForBoundaryCheck(): ?GeographicValue
225
    {
226 2110
        if ($this instanceof CompoundPoint) {
227 46
            $point = $this->getHorizontalPoint();
228
        } else {
229 2073
            $point = $this;
230
        }
231
232
        try {
233
            // try converting to WGS84 if possible...
234 2110
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->asGeographicValue();
0 ignored issues
show
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

234
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ 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

234
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
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

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