Passed
Push — master ( 1c4ceb...50b2e6 )
by Doug
44:53
created

Point::performOperation()   B

Complexity

Conditions 9
Paths 21

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 10.4768

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 9
eloc 20
c 3
b 0
f 0
nc 21
nop 4
dl 0
loc 35
rs 8.0555
ccs 14
cts 19
cp 0.7368
crap 10.4768
1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord;
10
11
use function abs;
12
use function acos;
13
use function array_reverse;
14
use function array_values;
15
use function asin;
16
use function atan;
17
use function atan2;
18
use function cos;
19
use DateTimeImmutable;
20
use function in_array;
21
use function lcfirst;
22
use const M_PI;
23
use const PHP_MAJOR_VERSION;
24
use PHPCoord\CoordinateOperation\ConvertiblePoint;
25
use PHPCoord\CoordinateOperation\CoordinateOperationMethods;
26
use PHPCoord\CoordinateOperation\CoordinateOperationParams;
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...
27
use PHPCoord\CoordinateOperation\CoordinateOperations;
28
use PHPCoord\CoordinateOperation\GeographicValue;
29
use PHPCoord\CoordinateOperation\GridProvider;
30
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
31
use PHPCoord\CoordinateSystem\Axis;
32
use PHPCoord\Datum\Ellipsoid;
33
use PHPCoord\UnitOfMeasure\Angle\Angle;
34
use PHPCoord\UnitOfMeasure\Length\Length;
35
use PHPCoord\UnitOfMeasure\Length\Metre;
36
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
37
use PHPCoord\UnitOfMeasure\Scale\Scale;
38
use PHPCoord\UnitOfMeasure\UnitOfMeasure;
39
use PHPCoord\UnitOfMeasure\UnitOfMeasureFactory;
40
use function preg_match;
41
use function sin;
42
use function sqrt;
43
use function sscanf;
44
use function str_replace;
45
use Stringable;
46
use function tan;
47
use function ucwords;
48
49
abstract class Point implements Stringable
50
{
51
    protected const ITERATION_CONVERGENCE_FORMULA = 1e-12;
52
    protected const ITERATION_CONVERGENCE_GRID = 0.0001;
53
54
    /**
55
     * @internal
56
     */
57 234
    public function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse, array $additionalParams = []): self
58
    {
59 234
        $operations = self::resolveConcatenatedOperations($srid, $inReverse);
60
61 234
        $point = $this;
62 234
        foreach ($operations as $operationSrid => $operation) {
63 234
            $method = CoordinateOperationMethods::getFunctionName($operation['method']);
64 234
            if (isset($operation['source_crs'])) {
65
                $sourceCRS = $inReverse ? $operation['target_crs'] : $operation['source_crs'];
66
                $destCRS = CoordinateReferenceSystem::fromSRID($inReverse ? $operation['source_crs'] : $operation['target_crs']);
67
68
                // occasionally the CRS domain switches part way through the sequence (e.g. NAD86 geog to NAD86 geocen)
69
                if ($point instanceof ConvertiblePoint && $point->getCRS()->getSRID() !== $sourceCRS) {
70
                    $point = $point->convert(CoordinateReferenceSystem::fromSRID($sourceCRS), true);
71
                }
72
            } else {
73 234
                $destCRS = $to;
74
            }
75
76 234
            $params = self::resolveParamsByOperation($operationSrid, $operation['method'], $inReverse);
77
78 234
            if ($operation['method'] === CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_AND_SLOPE) {
79 9
                $params['horizontalPoint'] = $additionalParams['horizontalPoint'];
80
            }
81
82 234
            if (PHP_MAJOR_VERSION >= 8) {
83
                $point = $point->$method($destCRS, ...$params);
84
            } else {
85 234
                $point = $point->$method($destCRS, ...array_values($params));
86
            }
87
        }
88
89 234
        $point->crs = $to; //some operations are reused across CRSses (e.g. ETRS89 and WGS84), so the $destCRS of the final suboperation might not be the intended target
90
91 234
        return $point;
92
    }
93
94 234
    protected static function camelCase(string $string): string
95
    {
96 234
        $string = str_replace([' ', '-'], '', ucwords($string, ' -'));
97 234
        if (!preg_match('/^(EPSG|[ABC][uv\d])/', $string)) {
98 234
            $string = lcfirst($string);
99
        }
100
101 234
        return $string;
102
    }
103
104 234
    protected static function resolveConcatenatedOperations(string $operationSrid, bool $inReverse): array
105
    {
106 234
        $operations = [];
107 234
        $operation = CoordinateOperations::getOperationData($operationSrid);
108 234
        if (isset($operation['operations'])) {
109 8
            foreach ($operation['operations'] as $subOperation) {
110 8
                $subOperationData = CoordinateOperations::getOperationData($subOperation['operation']);
111 8
                $subOperationData['source_crs'] = $subOperation['source_crs'];
112 8
                $subOperationData['target_crs'] = $subOperation['target_crs'];
113 8
                $operations[$subOperation['operation']] = $subOperationData;
114
            }
115
        } else {
116 234
            $operations[$operationSrid] = $operation;
117
        }
118
119 234
        if ($inReverse) {
120 117
            $operations = array_reverse($operations, true);
121
        }
122
123 234
        return $operations;
124
    }
125
126 234
    protected static function resolveParamsByOperation(string $operationSrid, string $methodSrid, bool $inReverse): array
127
    {
128 234
        $params = [];
129 234
        $powerCoefficients = [];
130 234
        foreach (CoordinateOperationParams::getParamData($operationSrid) as $paramName => $paramData) {
131 234
            if (isset($paramData['fileProvider'])) {
132
                $paramName = static::camelCase($paramName);
133
                /** @var GridProvider $provider */
134
                $provider = new $paramData['fileProvider']();
135
                $params[$paramName] = $provider->provideGrid();
136
            } else {
137 234
                $value = $paramData['value'];
138 234
                if ($inReverse && $paramData['reverses']) {
139 54
                    $value *= -1;
140
                }
141 234
                if ($paramData['uom']) {
142 234
                    $param = UnitOfMeasureFactory::makeUnit($value, $paramData['uom']);
143
                } else {
144 9
                    $param = $paramData['value'];
145
                }
146 234
                $paramName = static::camelCase($paramName);
147 234
                if (preg_match('/^(Au|Bu)/', $paramName)) {
148 27
                    $powerCoefficients[$paramName] = $param;
149
                } else {
150 234
                    $params[$paramName] = $param;
151
                }
152
            }
153
        }
154 234
        if ($powerCoefficients) {
155 27
            $params['powerCoefficients'] = $powerCoefficients;
156
        }
157 234
        if (in_array($methodSrid, [
158 234
            CoordinateOperationMethods::EPSG_SIMILARITY_TRANSFORMATION,
159 234
            CoordinateOperationMethods::EPSG_AFFINE_PARAMETRIC_TRANSFORMATION,
160 234
            CoordinateOperationMethods::EPSG_NADCON5_2D,
161 234
            CoordinateOperationMethods::EPSG_NADCON5_3D,
162 234
        ], true)) {
163
            $params['inReverse'] = $inReverse;
164
        }
165
166 234
        return $params;
167
    }
168
169 2052
    protected function getAxisByName(string $name): ?Axis
170
    {
171 2052
        foreach ($this->getCRS()->getCoordinateSystem()->getAxes() as $axis) {
172 2052
            if ($axis->getName() === $name) {
173 2052
                return $axis;
174
            }
175
        }
176
177 1314
        return null;
178
    }
179
180 90
    protected static function sign(float $number): int
181
    {
182 90
        if ($number < 0) {
183
            return -1;
184
        }
185
186 90
        return 1;
187
    }
188
189
    /**
190
     * Calculate surface distance between two points.
191
     */
192 153
    protected static function vincenty(GeographicValue $from, GeographicValue $to, Ellipsoid $ellipsoid): Length
193
    {
194 153
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
195 153
        $b = $ellipsoid->getSemiMinorAxis()->asMetres()->getValue();
196 153
        $f = $ellipsoid->getInverseFlattening();
197 153
        $U1 = atan((1 - $f) * tan($from->getLatitude()->asRadians()->getValue()));
198 153
        $U2 = atan((1 - $f) * tan($to->getLatitude()->asRadians()->getValue()));
199 153
        $L = $to->getLongitude()->subtract($from->getLongitude())->asRadians()->getValue();
200
201 153
        $lambda = $L;
202
        do {
203 153
            $lambdaN = $lambda;
204
205 153
            $sinSigma = sqrt((cos($U2) * sin($lambda)) ** 2 + (cos($U1) * sin($U2) - sin($U1) * cos($U2) * cos($lambda)) ** 2);
206 153
            $cosSigma = sin($U1) * sin($U2) + cos($U1) * cos($U2) * cos($lambda);
207 153
            $sigma = atan2($sinSigma, $cosSigma);
208
209 153
            $sinAlpha = cos($U1) * cos($U2) * sin($lambda) / $sinSigma;
210 153
            $cosSqAlpha = (1 - $sinAlpha ** 2);
211 153
            $cos2SigmaM = $cosSqAlpha ? $cosSigma - (2 * sin($U1) * sin($U2) / $cosSqAlpha) : 0;
212 153
            $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
213 153
            $lambda = $L + (1 - $C) * $f * $sinAlpha * ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (-1 + 2 * $cos2SigmaM ** 2)));
214 153
        } while (abs($lambda - $lambdaN) >= static::ITERATION_CONVERGENCE_FORMULA && abs($lambda) < M_PI);
215
216
        // Antipodal case
217 153
        if (abs($lambda) >= M_PI) {
218 27
            if ($L >= 0) {
219 27
                $LPrime = M_PI - $L;
220
            } else {
221
                $LPrime = -M_PI - $L;
222
            }
223
224 27
            $lambdaPrime = 0;
225 27
            $sigma = M_PI - abs($U1 + $U2);
226 27
            $sinSigma = sin($sigma);
227 27
            $cosSqAlpha = 0.5;
228 27
            $sinAlpha = 0;
229
230
            do {
231 27
                $sinAlphaN = $sinAlpha;
232
233 27
                $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
234 27
                $cos2SigmaM = cos($sigma) - 2 * sin($U1) * sin($U2) / $cosSqAlpha;
235 27
                $D = (1 - $C) * $f * ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * cos($sigma) * (-1 + 2 * $cos2SigmaM ** 2)));
236 27
                $sinAlpha = ($LPrime - $lambdaPrime) / $D;
237 27
                $cosSqAlpha = (1 - $sinAlpha ** 2);
238 27
                $sinLambdaPrime = ($sinAlpha * $sinSigma) / (cos($U1) * cos($U2));
239 27
                $lambdaPrime = self::asin($sinLambdaPrime);
240 27
                $sinSqSigma = (cos($U2) * $sinLambdaPrime) ** 2 + (cos($U1) * sin($U2) + sin($U1) * cos($U2) * cos($lambdaPrime)) ** 2;
241 27
                $sinSigma = sqrt($sinSqSigma);
242 27
            } while (abs($sinAlpha - $sinAlphaN) >= static::ITERATION_CONVERGENCE_FORMULA);
243
        }
244
245 153
        $E = sqrt(1 + (($a ** 2 - $b ** 2) / $b ** 2) * $cosSqAlpha);
246 153
        $F = ($E - 1) / ($E + 1);
247
248 153
        $A = (1 + $F ** 2 / 4) / (1 - $F);
249 153
        $B = $F * (1 - 3 / 8 * $F ** 2);
250
251 153
        $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM ** 2) - $B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma ** 2) * (-3 + 4 * $cos2SigmaM ** 2)));
252
253 153
        return new Metre($b * $A * ($sigma - $deltaSigma));
254
    }
255
256
    /**
257
     * General polynomial.
258
     * @param Coefficient[] $powerCoefficients
259
     */
260 18
    protected function generalPolynomialUnitless(
261
        float $xs,
262
        float $ys,
263
        UnitOfMeasure $ordinate1OfEvaluationPointInSourceCRS,
264
        UnitOfMeasure $ordinate2OfEvaluationPointInSourceCRS,
265
        UnitOfMeasure $ordinate1OfEvaluationPointInTargetCRS,
266
        UnitOfMeasure $ordinate2OfEvaluationPointInTargetCRS,
267
        Scale $scalingFactorForSourceCRSCoordDifferences,
268
        Scale $scalingFactorForTargetCRSCoordDifferences,
269
        Scale $A0,
270
        Scale $B0,
271
        array $powerCoefficients
272
    ): array {
273 18
        $xso = $ordinate1OfEvaluationPointInSourceCRS->getValue();
274 18
        $yso = $ordinate2OfEvaluationPointInSourceCRS->getValue();
275 18
        $xto = $ordinate1OfEvaluationPointInTargetCRS->getValue();
276 18
        $yto = $ordinate2OfEvaluationPointInTargetCRS->getValue();
277
278 18
        $U = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($xs - $xso);
279 18
        $V = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($ys - $yso);
280
281 18
        $mTdX = $A0->getValue();
282 18
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
283 18
            if ($coefficientName[0] === 'A') {
284 18
                sscanf($coefficientName, 'Au%dv%d', $uPower, $vPower);
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $vPower seems to be never defined.
Loading history...
285 18
                $mTdX += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
286
            }
287
        }
288
289 18
        $mTdY = $B0->getValue();
290 18
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
291 18
            if ($coefficientName[0] === 'B') {
292 18
                sscanf($coefficientName, 'Bu%dv%d', $uPower, $vPower);
293 18
                $mTdY += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
294
            }
295
        }
296
297 18
        $xt = $xs - $xso + $xto + $mTdX / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
298 18
        $yt = $ys - $yso + $yto + $mTdY / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
299
300 18
        return ['xt' => $xt, 'yt' => $yt];
301
    }
302
303
    /**
304
     * Reversible polynomial.
305
     */
306 36
    protected function reversiblePolynomialUnitless(
307
        float $xs,
308
        float $ys,
309
        Angle $ordinate1OfEvaluationPoint,
310
        Angle $ordinate2OfEvaluationPoint,
311
        Scale $scalingFactorForCoordDifferences,
312
        Scale $A0,
313
        Scale $B0,
314
        array $powerCoefficients
315
    ): array {
316 36
        $xo = $ordinate1OfEvaluationPoint->getValue();
317 36
        $yo = $ordinate2OfEvaluationPoint->getValue();
318
319 36
        $U = $scalingFactorForCoordDifferences->asUnity()->getValue() * ($xs - $xo);
320 36
        $V = $scalingFactorForCoordDifferences->asUnity()->getValue() * ($ys - $yo);
321
322 36
        $mTdX = $A0->getValue();
323 36
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
324 36
            if ($coefficientName[0] === 'A') {
325 36
                sscanf($coefficientName, 'Au%dv%d', $uPower, $vPower);
1 ignored issue
show
Comprehensibility Best Practice introduced by
The variable $vPower seems to be never defined.
Loading history...
326 36
                $mTdX += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
327
            }
328
        }
329
330 36
        $mTdY = $B0->getValue();
331 36
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
332 36
            if ($coefficientName[0] === 'B') {
333 36
                sscanf($coefficientName, 'Bu%dv%d', $uPower, $vPower);
334 36
                $mTdY += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
335
            }
336
        }
337
338 36
        $xt = $xs + $mTdX * $scalingFactorForCoordDifferences->asUnity()->getValue();
339 36
        $yt = $ys + $mTdY * $scalingFactorForCoordDifferences->asUnity()->getValue();
340
341 36
        return ['xt' => $xt, 'yt' => $yt];
342
    }
343
344
    /**
345
     * Floating point vagaries mean that it's possible for inputs to be e.g. 1.00000000000001 which makes PHP give a
346
     * silent NaN as output so inputs need to be capped. atan/atan2 are not affected, they seem to cap internally.
347
     */
348
    protected static function acos(float $num): float
349
    {
350
        if ($num > 1.0) {
351
            $num = 1.0;
352
        } elseif ($num < -1) {
353
            $num = -1.0;
354
        }
355
356
        return acos($num);
357
    }
358
359
    /**
360
     * Floating point vagaries mean that it's possible for inputs to be e.g. 1.00000000000001 which makes PHP give a
361
     * silent NaN as output so inputs need to be capped. atan/atan2 are not affected, they seem to cap internally.
362
     */
363 495
    protected static function asin(float $num): float
364
    {
365 495
        if ($num > 1.0) {
366
            $num = 1.0;
367 495
        } elseif ($num < -1.0) {
368
            $num = -1.0;
369
        }
370
371 495
        return asin($num);
372
    }
373
374
    abstract public function getCRS(): CoordinateReferenceSystem;
375
376
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
377
378
    abstract public function calculateDistance(self $to): Length;
379
}
380