Passed
Push — master ( 705a37...2b8bad )
by Doug
17:25 queued 45s
created

buildSupportedTransformationsByCRS()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 6.0033

Importance

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

243
            return $point->convert(Geographic2D::fromSRID(Geographic2D::EPSG_WGS_84), true)->/** @scrutinizer ignore-call */ asGeographicValue();
Loading history...
244 9
        } catch (UnknownConversionException) {
245
            /*
246
             * If Projected then either the point is inside the boundary by definition
247
             * or the user is deliberately exceeding the safe zone so safe to make a no-op either way.
248
             */
249 9
            if ($point instanceof ProjectedPoint) {
250
                return null;
251
            }
252
253
            /*
254
             * Otherwise, compensate for non-Greenwich Prime Meridian, but otherwise assume that coordinates
255
             * are interchangeable between the actual CRS and WGS84. Boundaries are only defined to the nearest
256
             * ≈1km so the error bound should be acceptable within the area of interest
257
             */
258 9
            if ($point instanceof GeographicPoint) {
259
                return new GeographicValue($point->getLatitude(), $point->getLongitude()->subtract($point->getCRS()->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $point->getCRS()->getDatum());
260
            }
261
262 9
            if ($point instanceof GeocentricPoint) {
263 9
                $asGeographic = $point->asGeographicValue();
264
265 9
                return new GeographicValue($asGeographic->getLatitude(), $asGeographic->getLongitude()->subtract($asGeographic->getDatum()->getPrimeMeridian()->getGreenwichLongitude()), null, $asGeographic->getDatum());
266
            }
267
        }
268
    }
269
270 201
    protected static function buildSupportedTransformationsByCRS(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target): array
271
    {
272 201
        $regions = array_unique([$source->getBoundingArea()->getRegion(), $target->getBoundingArea()->getRegion(), RegionMap::REGION_GLOBAL]);
273 201
        $relevantRegionData = [];
274 201
        foreach ($regions as $region) {
275 201
            $regionData = match ($region) {
276 201
                RegionMap::REGION_GLOBAL => CRSTransformationsGlobal::getSupportedTransformations(),
277 147
                RegionMap::REGION_AFRICA => CRSTransformationsAfrica::getSupportedTransformations(),
278 147
                RegionMap::REGION_ANTARCTIC => CRSTransformationsAntarctic::getSupportedTransformations(),
279 147
                RegionMap::REGION_ARCTIC => CRSTransformationsArctic::getSupportedTransformations(),
280 147
                RegionMap::REGION_ASIA => CRSTransformationsAsia::getSupportedTransformations(),
281 120
                RegionMap::REGION_EUROPE => CRSTransformationsEurope::getSupportedTransformations(),
282 20
                RegionMap::REGION_NORTHAMERICA => CRSTransformationsNorthAmerica::getSupportedTransformations(),
283 10
                RegionMap::REGION_OCEANIA => CRSTransformationsOceania::getSupportedTransformations(),
284
                RegionMap::REGION_SOUTHAMERICA => CRSTransformationsSouthAmerica::getSupportedTransformations(),
285
            };
286 201
            $relevantRegionData = [...$relevantRegionData, ...$regionData];
287
        }
288
289 201
        $transformationsByCRS = [];
290 201
        foreach ($relevantRegionData as $transformation) {
291 201
            if (!isset($transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']])) {
292 201
                $transformationsByCRS[$transformation['source_crs']][$transformation['target_crs']] = $transformation['target_crs'];
293
            }
294 201
            if ($transformation['reversible'] && !isset($transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']])) {
295 201
                $transformationsByCRS[$transformation['target_crs']][$transformation['source_crs']] = $transformation['source_crs'];
296
            }
297
        }
298
299 201
        return $transformationsByCRS;
300
    }
301
302 201
    protected static function buildSupportedTransformationsByCRSPair(Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $source, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $target): array
303
    {
304 201
        $regions = array_unique([$source->getBoundingArea()->getRegion(), $target->getBoundingArea()->getRegion(), RegionMap::REGION_GLOBAL]);
305 201
        $relevantRegionData = [];
306 201
        foreach ($regions as $region) {
307 201
            $regionData = match ($region) {
308 201
                RegionMap::REGION_GLOBAL => CRSTransformationsGlobal::getSupportedTransformations(),
309 147
                RegionMap::REGION_AFRICA => CRSTransformationsAfrica::getSupportedTransformations(),
310 147
                RegionMap::REGION_ANTARCTIC => CRSTransformationsAntarctic::getSupportedTransformations(),
311 147
                RegionMap::REGION_ARCTIC => CRSTransformationsArctic::getSupportedTransformations(),
312 147
                RegionMap::REGION_ASIA => CRSTransformationsAsia::getSupportedTransformations(),
313 120
                RegionMap::REGION_EUROPE => CRSTransformationsEurope::getSupportedTransformations(),
314 20
                RegionMap::REGION_NORTHAMERICA => CRSTransformationsNorthAmerica::getSupportedTransformations(),
315 10
                RegionMap::REGION_OCEANIA => CRSTransformationsOceania::getSupportedTransformations(),
316
                RegionMap::REGION_SOUTHAMERICA => CRSTransformationsSouthAmerica::getSupportedTransformations(),
317
            };
318 201
            $relevantRegionData = [...$relevantRegionData, ...$regionData];
319
        }
320
321 201
        $transformationsByCRSPair = [];
322 201
        foreach ($relevantRegionData as $key => $transformation) {
323 201
            $transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key] = $transformation;
324 201
            $transformationsByCRSPair[$transformation['source_crs'] . '|' . $transformation['target_crs']][$key]['in_reverse'] = false;
325 201
            if ($transformation['reversible']) {
326 201
                $transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key] = $transformation;
327 201
                $transformationsByCRSPair[$transformation['target_crs'] . '|' . $transformation['source_crs']][$key]['in_reverse'] = true;
328
            }
329
        }
330
331 201
        return $transformationsByCRSPair;
332
        //additional help for ETRS89->WGS84
333
            /*if (in_array($from, [Geocentric::EPSG_WGS_84, Geographic2D::EPSG_WGS_84, Geographic3D::EPSG_WGS_84], true) && in_array($to, [Geocentric::EPSG_ETRS89, Geographic2D::EPSG_ETRS89, Geographic3D::EPSG_ETRS89], true)) {
334
                $inserts = self::$OPERATIONS_ETRF2014_TO_WGS84_G2139;
335
            } elseif (in_array($to, [Geocentric::EPSG_WGS_84, Geographic2D::EPSG_WGS_84, Geographic3D::EPSG_WGS_84], true) && in_array($from, [Geocentric::EPSG_ETRS89, Geographic2D::EPSG_ETRS89, Geographic3D::EPSG_ETRS89], true)) {
336
                $inserts = array_reverse(self::$OPERATIONS_ETRF2014_TO_WGS84_G2139);
337
            }
338
            if (in_array($from, [Geographic2D::EPSG_WGS_84, Geographic3D::EPSG_WGS_84], true) && in_array($to, [Geographic2D::EPSG_ETRS89, Geographic3D::EPSG_ETRS89], true)) {
339
                $inserts = [self::$OPERATION_GEOCENTRIC_TO_GEOGRAPHIC3D, ...$inserts, self::$OPERATION_GEOCENTRIC_TO_GEOGRAPHIC3D];
340
            } elseif (in_array($to, [Geographic2D::EPSG_WGS_84, Geographic3D::EPSG_WGS_84], true) && in_array($from, [Geographic2D::EPSG_ETRS89, Geographic3D::EPSG_ETRS89], true)) {
341
                $inserts = [self::$OPERATION_GEOCENTRIC_TO_GEOGRAPHIC3D, ...$inserts, self::$OPERATION_GEOCENTRIC_TO_GEOGRAPHIC3D];
342
            }
343
            if (in_array($from, [Geographic2D::EPSG_WGS_84], true) && in_array($to, [Geographic2D::EPSG_ETRS89], true)) {
344
                $inserts = [self::$OPERATION_GEOGRAPHIC3D_TO_GEOGRAPHIC2D, ...$inserts, self::$OPERATION_GEOGRAPHIC3D_TO_GEOGRAPHIC2D];
345
            } elseif (in_array($to, [Geographic2D::EPSG_WGS_84], true) && in_array($from, [Geographic2D::EPSG_ETRS89], true)) {
346
                $inserts = [self::$OPERATION_GEOGRAPHIC3D_TO_GEOGRAPHIC2D, ...$inserts, self::$OPERATION_GEOGRAPHIC3D_TO_GEOGRAPHIC2D];
347
            }*/
348
349
            /*
350
ITRF2000 to NAD83(CORS96) (1)
351
ITRF2000 to NAD83(CSRS)v4 (1)
352
ITRF2000 to NAD83(MARP00) (1)
353
ITRF2000 to NAD83(PACP00) (1)
354
ITRF2005 to NAD83(CSRS)v5 (1)
355
ITRF2008 to NAD83(2011) (1)
356
ITRF2008 to NAD83(CSRS)v6 (1)
357
ITRF2008 to NAD83(MA11) (1)
358
ITRF2008 to NAD83(PA11) (1)
359
ITRF2014 to GDA2020 (1)
360
ITRF2014 to ETRF2014 (1)
361
ITRF2014 to NAD83(2011) (1)
362
ITRF2014 to NAD83(CSRS)v7 (1)
363
364
*/
365
    }
366
367
    abstract public function getCRS(): CoordinateReferenceSystem;
368
369
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
370
371
    abstract protected function performOperation(string $srid, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $to, bool $inReverse): Point;
372
}
373