Passed
Push — 4.0.x ( 9ec284...cc1a77 )
by Doug
02:59
created

Point::reversiblePolynomialUnitless()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 17
c 0
b 0
f 0
dl 0
loc 36
ccs 18
cts 18
cp 1
rs 9.3888
cc 5
nc 9
nop 8
crap 5

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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