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

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