Passed
Push — master ( 9abb61...fb6dbd )
by Doug
08:29
created

AutoConversion::buildTransformationPathsToCRS()   B

Complexity

Conditions 9
Paths 3

Size

Total Lines 49
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 9

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 9
eloc 32
nc 3
nop 1
dl 0
loc 49
ccs 31
cts 31
cp 1
crap 9
rs 8.0555
c 2
b 0
f 0
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\GeographicPoint;
26
use PHPCoord\Geometry\GeographicPolygon;
27
use PHPCoord\Point;
28
use PHPCoord\UnitOfMeasure\Time\Year;
29
use function strpos;
30
use function usort;
31
32
trait AutoConversion
33
{
34
    private int $maxChainDepth = 4; // if traits could have constants...
35
36
    private static array $pathCache = [];
37
38 39
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
39
    {
40 39
        if ($this->getCRS() == $to) {
41 21
            return $this;
42
        }
43
44 20
        if (strpos($this->getCRS()->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0 || strpos($to->getSRID(), CoordinateReferenceSystem::CRS_SRID_PREFIX_EPSG) !== 0) {
45
            throw new UnknownConversionException('Automatic conversions are only supported for EPSG CRSs');
46
        }
47
48 20
        $point = $this;
49 20
        $path = $this->findOperationPath($to, $ignoreBoundaryRestrictions);
50
51 19
        foreach ($path as $step) {
52 19
            $target = CoordinateReferenceSystem::fromSRID($step['in_reverse'] ? $step['source_crs'] : $step['target_crs']);
53 19
            $point = $point->performOperation($step['operation'], $target, $step['in_reverse']);
54
        }
55
56 19
        return $point;
57
    }
58
59 20
    protected function findOperationPath(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions): array
60
    {
61 20
        $candidatePaths = $this->buildTransformationPathsToCRS($to);
62
63 20
        usort($candidatePaths, static function (array $a, array $b) {
64 14
            return count($a['path']) <=> count($b['path']) ?: $a['accuracy'] <=> $b['accuracy'];
65 20
        });
66
67 20
        $asWGS84Value = $ignoreBoundaryRestrictions ? null : $this->asWGS84()->asGeographicValue();
68
69 20
        foreach ($candidatePaths as $candidatePath) {
70 20
            $ok = true;
71
72 20
            foreach ($candidatePath['path'] as $pathStep) {
73 20
                $operation = CoordinateOperations::getOperationData($pathStep['operation']);
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateOperation\CoordinateOperations 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...
74 20
                if (!$ignoreBoundaryRestrictions) {
75
                    //filter out operations that only operate outside this point
76 16
                    $polygon = GeographicPolygon::createFromArray($operation['bounding_box'], $operation['bounding_box_crosses_antimeridian']);
77 16
                    $ok = $ok && $polygon->containsPoint($asWGS84Value);
0 ignored issues
show
Bug introduced by
It seems like $asWGS84Value can also be of type null; however, parameter $point of PHPCoord\Geometry\Geogra...olygon::containsPoint() does only seem to accept PHPCoord\CoordinateOperation\GeographicValue, 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

77
                    $ok = $ok && $polygon->containsPoint(/** @scrutinizer ignore-type */ $asWGS84Value);
Loading history...
78
                }
79
80 20
                $operations = static::resolveConcatenatedOperations($pathStep['operation'], false);
81
82 20
                foreach ($operations as $operation) {
83
                    //filter out operations that require an epoch if we don't have one
84 20
                    if (!$this->getCoordinateEpoch() && in_array($operation['method'], [
85
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOCEN,
86
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG2D,
87
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_COORDINATE_FRAME_ROTATION_GEOG3D,
88
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOCENTRIC,
89
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG2D,
90
                            CoordinateOperationMethods::EPSG_TIME_DEPENDENT_POSITION_VECTOR_TFM_GEOG3D,
91
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
92
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
93
                        ], true)) {
94 1
                        $ok = false;
95
                    }
96
97
                    //filter out operations that require a specific epoch
98 20
                    if ($this->getCoordinateEpoch() && in_array($operation['method'], [
99
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_COORDINATE_FRAME_ROTATION_GEOCEN,
100
                            CoordinateOperationMethods::EPSG_TIME_SPECIFIC_POSITION_VECTOR_TRANSFORM_GEOCEN,
101
                        ], true)) {
102 1
                        $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...
103 1
                        $pointEpoch = Year::fromDateTime($this->getCoordinateEpoch());
104 1
                        $ok = $ok && (abs($pointEpoch->getValue() - $params['Transformation reference epoch']['value']) <= 0.001);
105
                    }
106
                }
107
            }
108
109 20
            if ($ok) {
110 19
                return $candidatePath['path'];
111
            }
112
        }
113
114 7
        throw new UnknownConversionException('Unable to perform conversion, please file a bug if you think this is incorrect');
115
    }
116
117
    /**
118
     * Build the set of *all* possible paths that lead from the current CRS to the target CRS.
119
     */
120 20
    protected function buildTransformationPathsToCRS(CoordinateReferenceSystem $target): array
121
    {
122 20
        $cacheKey = $this->getCRS()->getSRID() . '|' . $target->getSRID();
123 20
        if (!isset(self::$pathCache[$cacheKey])) {
124 18
            $simplePaths = [];
125
126
            // Try a simple direct match before doing anything more complex!
127 18
            if (CRSTransformations::getSupportedTransformationsForCRSPair($this->getCRS()->getSRID(), $target->getSRID())) {
128 14
                $simplePaths[] = [$this->getCRS()->getSRID(), $target->getSRID()];
129
            } else { // Otherwise, recursively calculate permutations of intermediate CRSs
130 9
                $visited = [];
131 9
                foreach (CoordinateReferenceSystem::getSupportedSRIDs() as $crs => $name) {
132 9
                    $visited[$crs] = false;
133
                }
134 9
                $currentPath = [];
135 9
                $this->DFS($this->getCRS()->getSRID(), $target->getSRID(), $visited, $currentPath, $simplePaths);
136
            }
137
138
            // Then expand each CRS->CRS permutation with the various ways of achieving that (can be lots :/)
139 18
            $candidatePaths = [];
140 18
            foreach ($simplePaths as $simplePath) {
141 17
                $transformationsToMakePath = [[]];
142 17
                $from = array_shift($simplePath);
143 17
                assert($from === $this->getCRS()->getSRID());
144
                do {
145 17
                    $to = array_shift($simplePath);
146 17
                    $wipTransformationsInPath = [];
147 17
                    foreach (CRSTransformations::getSupportedTransformationsForCRSPair($from, $to) as $transformation) {
148 17
                        foreach ($transformationsToMakePath as $transformationToMakePath) {
149 17
                            $wipTransformationsInPath[] = array_merge($transformationToMakePath, [$transformation]);
150
                        }
151
                    }
152
153 17
                    $transformationsToMakePath = $wipTransformationsInPath;
154 17
                    $from = $to;
155 17
                } while (count($simplePath) > 0);
156 17
                assert($to === $target->getSRID());
157 17
                $candidatePaths = array_merge($candidatePaths, $transformationsToMakePath);
158
            }
159
160 18
            $candidates = [];
161 18
            foreach ($candidatePaths as $candidatePath) {
162 17
                $candidates[] = ['path' => $candidatePath, 'accuracy' => array_sum(array_column($candidatePath, 'accuracy'))];
163
            }
164
165 18
            self::$pathCache[$cacheKey] = $candidates;
166
        }
167
168 20
        return self::$pathCache[$cacheKey];
169
    }
170
171 9
    protected function DFS(string $u, string $v, array &$visited, array &$currentPath, array &$simplePaths): void
172
    {
173 9
        $currentPath[] = $u;
174 9
        if ($u === $v) {
175 7
            $simplePaths[] = $currentPath;
176
        } else {
177 9
            $visited[$u] = true;
178 9
            if (count($currentPath) <= $this->maxChainDepth) {
179 9
                foreach (CRSTransformations::getSupportedTransformationsForCRS($u) as $nextU) {
180 8
                    if (!$visited[$nextU]) {
181 8
                        $this->DFS($nextU, $v, $visited, $currentPath, $simplePaths);
182
                    }
183
                }
184
            }
185
        }
186
187 9
        array_pop($currentPath);
188 9
        $visited[$u] = false;
189 9
    }
190
191 19
    protected function asWGS84(): GeographicPoint
192
    {
193 19
        if ($this instanceof CompoundPoint) {
194 2
            $point = $this->getHorizontalPoint();
195
        } else {
196 17
            $point = $this;
197
        }
198
199 19
        return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true);
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

199
        return $point->/** @scrutinizer ignore-call */ convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true);
Loading history...
200
    }
201
202
    abstract public function getCRS(): CoordinateReferenceSystem;
203
204
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
205
206
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
207
}
208