Passed
Branch master (b57997)
by Doug
12:41
created

AutoConversion   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 207
Duplicated Lines 0 %

Test Coverage

Coverage 91.35%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 103
c 5
b 0
f 0
dl 0
loc 207
ccs 95
cts 104
cp 0.9135
rs 9.2
wmc 40

5 Methods

Rating   Name   Duplication   Size   Complexity  
A getPointForBoundaryCheck() 0 33 6
C findOperationPath() 0 56 14
B buildTransformationPathsToCRS() 0 49 9
A convert() 0 19 6
A DFS() 0 18 5

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 count;
19
use DateTimeImmutable;
20
use function in_array;
21
use PHPCoord\CompoundPoint;
22
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
23
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
24
use PHPCoord\Exception\UnknownConversionException;
25
use PHPCoord\GeocentricPoint;
26
use PHPCoord\GeographicPoint;
27
use PHPCoord\Geometry\BoundingArea;
28
use PHPCoord\Point;
29
use PHPCoord\ProjectedPoint;
30
use PHPCoord\UnitOfMeasure\Time\Year;
31
use function strpos;
32
use function usort;
33
34
/**
35
 * @internal
36
 */
37
trait AutoConversion
38
{
39
    private int $maxChainDepth = 4; // if traits could have constants...
40
41
    private static array $pathCache = [];
42
43 414
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
44
    {
45 414
        if ($this->getCRS() == $to) {
46 216
            return $this;
47
        }
48
49 234
        if (strpos($this->getCRS()->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0 || strpos($to->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0) {
50 18
            throw new UnknownConversionException('Automatic conversions are only supported for EPSG CRSs');
51
        }
52
53 225
        $point = $this;
54 225
        $path = $this->findOperationPath($this->getCRS(), $to, $ignoreBoundaryRestrictions);
55
56 216
        foreach ($path as $step) {
57 216
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
58 216
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
59
        }
60
61 216
        return $point;
62
    }
63
64 225
    protected function findOperationPath(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target, bool $ignoreBoundaryRestrictions): array
65
    {
66 225
        $candidatePaths = $this->buildTransformationPathsToCRS($source, $target);
67
68 225
        usort($candidatePaths, static function (array $a, array $b) {
69 144
            return count($a['path']) <=> count($b['path']) ?: $a['accuracy'] <=> $b['accuracy'];
70 225
        });
71
72 225
        $boundaryCheckPoint = $ignoreBoundaryRestrictions ? null : $this->getPointForBoundaryCheck();
73
74 225
        foreach ($candidatePaths as $candidatePath) {
75 225
            $ok = true;
76
77 225
            foreach ($candidatePath['path'] as $pathStep) {
78 225
                $operation = CoordinateOperations::getOperationData($pathStep['operation']);
79 225
                if ($boundaryCheckPoint) {
80
                    //filter out operations that only operate outside this point
81 180
                    $polygon = BoundingArea::createFromExtentCodes($operation['extent_code']);
82 180
                    $ok = $ok && $polygon->containsPoint($boundaryCheckPoint);
83
                }
84
85 225
                $operations = static::resolveConcatenatedOperations($pathStep['operation'], false);
86
87 225
                foreach ($operations as $operation) {
88
                    //filter out operations that require an epoch if we don't have one
89 225
                    if (!$this->getCoordinateEpoch() && in_array($operation['method'], [
90 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN,
91 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D,
92 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D,
93 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC,
94 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D,
95 216
                        CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D,
96 216
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
97 216
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
98 225
                    ], true)) {
99 9
                        $ok = false;
100
                    }
101
102
                    //filter out operations that require a specific epoch
103 225
                    if ($this->getCoordinateEpoch() && in_array($operation['method'], [
104 9
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
105 9
                        CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
106 225
                    ], true)) {
107 9
                        $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...
108 9
                        $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
109 9
                        $ok = $ok && (abs($pointEpoch->getValue() - $params['Transformation reference epoch']['value']) <= 0.001);
110
                    }
111
                }
112
            }
113
114 225
            if ($ok) {
115 216
                return $candidatePath['path'];
116
            }
117
        }
118
119 63
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
120
    }
121
122
    /**
123
     * Build the set of *all* possible paths that lead from the current CRS to the target CRS.
124
     */
125 225
    protected function buildTransformationPathsToCRS(CoordinateReferenceSystem $source, CoordinateReferenceSystem $target): array
126
    {
127 225
        $cacheKey = $source->getSRID() . '|' . $target->getSRID();
128 225
        if (!isset(self::$pathCache[$cacheKey])) {
129 189
            $simplePaths = [];
130
131
            // Try a simple direct match before doing anything more complex!
132 189
            if (CRSTransformations::getSupportedTransformationsForCRSPair($source->getSRID(), $target->getSRID())) {
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...
133 144
                $simplePaths[] = [$source->getSRID(), $target->getSRID()];
134
            } else { // Otherwise, recursively calculate permutations of intermediate CRSs
135 99
                $visited = [];
136 99
                foreach (CoordinateReferenceSystem::getSupportedSRIDs() as $crs => $name) {
137 99
                    $visited[$crs] = false;
138
                }
139 99
                $currentPath = [];
140 99
                $this->DFS($source->getSRID(), $target->getSRID(), $visited, $currentPath, $simplePaths);
141
            }
142
143
            // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
144 189
            $candidatePaths = [];
145 189
            foreach ($simplePaths as $simplePath) {
146 180
                $transformationsToMakePath = [[]];
147 180
                $from = array_shift($simplePath);
148 180
                assert($from === $source->getSRID());
149
                do {
150 180
                    $to = array_shift($simplePath);
151 180
                    $wipTransformationsInPath = [];
152 180
                    foreach (CRSTransformations::getSupportedTransformationsForCRSPair($from, $to) as $transformation) {
153 180
                        foreach ($transformationsToMakePath as $transformationToMakePath) {
154 180
                            $wipTransformationsInPath[] = array_merge($transformationToMakePath, [$transformation]);
155
                        }
156
                    }
157
158 180
                    $transformationsToMakePath = $wipTransformationsInPath;
159 180
                    $from = $to;
160 180
                } while (count($simplePath) > 0);
161 180
                assert($to === $target->getSRID());
162 180
                $candidatePaths = array_merge($candidatePaths, $transformationsToMakePath);
163
            }
164
165 189
            $candidates = [];
166 189
            foreach ($candidatePaths as $candidatePath) {
167 180
                $candidates[] = ['path' => $candidatePath, 'accuracy' => array_sum(array_column($candidatePath, 'accuracy'))];
168
            }
169
170 189
            self::$pathCache[$cacheKey] = $candidates;
171
        }
172
173 225
        return self::$pathCache[$cacheKey];
174
    }
175
176 99
    protected function DFS(string $u, string $v, array &$visited, array &$currentPath, array &$simplePaths): void
177
    {
178 99
        $currentPath[] = $u;
179 99
        if ($u === $v) {
180 81
            $simplePaths[] = $currentPath;
181
        } else {
182 99
            $visited[$u] = true;
183 99
            if (count($currentPath) <= $this->maxChainDepth) {
184 99
                foreach (CRSTransformations::getSupportedTransformationsForCRS($u) as $nextU) {
185 99
                    if (!$visited[$nextU]) {
186 99
                        $this->DFS($nextU, $v, $visited, $currentPath, $simplePaths);
187
                    }
188
                }
189
            }
190
        }
191
192 99
        array_pop($currentPath);
193 99
        $visited[$u] = false;
194 99
    }
195
196
    /**
197
     * Boundary polygons are defined as WGS84, so theoretically all that needs to happen is
198
     * to conversion to WGS84 by calling ->convert(). However, that leads quickly to either circularity
199
     * when a conversion is possible, or an exception because not every CRS has a WGS84 transformation
200
     * available to it even when chaining.
201
     */
202 207
    protected function getPointForBoundaryCheck(): ?GeographicValue
203
    {
204 207
        if ($this instanceof CompoundPoint) {
205 36
            $point = $this->getHorizontalPoint();
206
        } else {
207 171
            $point = $this;
208
        }
209
210
        try {
211
            // try converting to WGS84 if possible...
212 207
            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

212
            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

212
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
213
        } catch (UnknownConversionException $e) {
214
            /*
215
             * If Projected then either the point is inside the boundary by definition
216
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
217
             */
218
            if ($point instanceof ProjectedPoint) {
219
                return null;
220
            }
221
222
            /*
223
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
224
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
225
             * ≈1km so the error bound should be acceptable within the area of interest
226
             */
227
            if ($point instanceof GeographicPoint) {
228
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
229
            }
230
231
            if ($point instanceof GeocentricPoint) {
232
                $asGeographic = $point->asGeographicValue();
233
234
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
235
            }
236
        }
237
    }
238
239
    abstract public function getCRS(): CoordinateReferenceSystem;
240
241
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
242
243
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
244
}
245