Passed
Push — master ( 383181...140fc1 )
by Doug
23:45
created

oCRS()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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

237
            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

237
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
238 18
        } catch (UnknownConversionException $e) {
239
            /*
240
             * If Projected then either the point is inside the boundary by definition
241
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
242
             */
243 18
            if ($point instanceof ProjectedPoint) {
244
                return null;
245
            }
246
247
            /*
248
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
249
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
250
             * ≈1km so the error bound should be acceptable within the area of interest
251
             */
252 18
            if ($point instanceof GeographicPoint) {
253
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
254
            }
255
256 18
            if ($point instanceof GeocentricPoint) {
257 18
                $asGeographic = $point->asGeographicValue();
258
259 18
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
260
            }
261
        }
262
    }
263
264 252
    protected static function buildSupportedTransformationsByCRS(): void
265
    {
266 252
        if (!static::$transformationsByCRS) {
0 ignored issues
show
Bug introduced by
Since $transformationsByCRS is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $transformationsByCRS to at least protected.
Loading history...
Bug Best Practice introduced by
The expression static::transformationsByCRS of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
267 27
            foreach (CRSTransformations::getSupportedTransformations() as $transformation) {
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...
268 27
                if (!isset(static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']])) {
269 27
                    static::$transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']] = $transformation['target_crs'];
270
                }
271 27
                if ($transformation['reversible'] && !isset(static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']])) {
272 27
                    static::$transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']] = $transformation['source_crs'];
273
                }
274
            }
275
        }
276 252
    }
277
278 252
    protected static function buildSupportedTransformationsByCRSPair(): void
279
    {
280 252
        if (!static::$transformationsByCRSPair) {
0 ignored issues
show
Bug Best Practice introduced by
The expression static::transformationsByCRSPair of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Bug introduced by
Since $transformationsByCRSPair is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $transformationsByCRSPair to at least protected.
Loading history...
281 27
            foreach (CRSTransformations::getSupportedTransformations() as $key => $transformation) {
282 27
                if (!isset(static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key])) {
283 27
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key] = $transformation;
284 27
                    static::$transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key]['in_reverse'] = false;
285
                }
286 27
                if ($transformation['reversible'] && !isset(static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key])) {
287 27
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key] = $transformation;
288 27
                    static::$transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key]['in_reverse'] = true;
289
                }
290
            }
291
        }
292 252
    }
293
294
    abstract public function getCRS(): CoordinateReferenceSystem;
295
296
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
297
298
    abstract protected function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse): Point;
299
}
300