Passed
Push — master ( 0e0534...3c9843 )
by Doug
02:42
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 22
    public function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse, array $additionalParams = []): self
55
    {
56 22
        $operations = self::resolveConcatenatedOperations($srid, $inReverse);
57
58 22
        $point = $this;
59 22
        foreach ($operations as $operationSrid => $operation) {
60 22
            $method = CoordinateOperationMethods::getFunctionName($operation['method']);
61 22
            if (isset($operation['source_crs'])) {
62
                $destCRS = CoordinateReferenceSystem::fromSRID($inReverse ? $operation['source_crs'] : $operation['target_crs']);
63
            } else {
64 22
                $destCRS = $to;
65
            }
66
67 22
            $params = self::resolveParamsByOperation($operationSrid, $operation['method'], $inReverse);
68
69 22
            if ($operation['method'] === CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_AND_SLOPE) {
70 1
                $params['horizontalPoint'] = $additionalParams['horizontalPoint'];
71
            }
72
73 22
            if (PHP_MAJOR_VERSION >= 8) {
74
                $point = $point->$method($destCRS, ...$params);
75
            } else {
76 22
                $point = $point->$method($destCRS, ...array_values($params));
77
            }
78
        }
79
80 22
        $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 22
        return $point;
83
    }
84
85 22
    protected static function camelCase(string $string): string
86
    {
87 22
        $string = str_replace([' ', '-'], '', ucwords($string, ' -'));
88 22
        if (!preg_match('/^(EPSG|[ABC][uv\d])/', $string)) {
89 22
            $string = lcfirst($string);
90
        }
91
92 22
        return $string;
93
    }
94
95 23
    protected static function resolveConcatenatedOperations(string $operationSrid, bool $inReverse): array
96
    {
97 23
        $operations = [];
98 23
        $operation = CoordinateOperations::getOperationData($operationSrid);
99 23
        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 23
            $operations[$operationSrid] = $operation;
108
        }
109
110 23
        if ($inReverse) {
111 11
            $operations = array_reverse($operations, true);
112
        }
113
114 23
        return $operations;
115
    }
116
117 22
    protected static function resolveParamsByOperation(string $operationSrid, string $methodSrid, bool $inReverse): array
118
    {
119 22
        $params = [];
120 22
        $powerCoefficients = [];
121 22
        foreach (CoordinateOperationParams::getParamData($operationSrid) as $paramName => $paramData) {
122 22
            $value = $paramData['value'];
123 22
            if ($inReverse && $paramData['reverses']) {
124 4
                $value *= -1;
125
            }
126 22
            if ($paramData['uom']) {
127 22
                $param = UnitOfMeasureFactory::makeUnit($value, $paramData['uom']);
128
            } else {
129 1
                $param = $paramData['value'];
130
            }
131 22
            $paramName = static::camelCase($paramName);
132 22
            if (preg_match('/^(Au|Bu)/', $paramName)) {
133 3
                $powerCoefficients[$paramName] = $param;
134
            } else {
135 22
                $params[$paramName] = $param;
136
            }
137
        }
138 22
        if ($powerCoefficients) {
139 3
            $params['powerCoefficients'] = $powerCoefficients;
140
        }
141 22
        if (in_array($methodSrid, [
142 22
            CoordinateOperationMethods::EPSG_SIMILARITY_TRANSFORMATION,
143 22
            CoordinateOperationMethods::EPSG_AFFINE_PARAMETRIC_TRANSFORMATION,
144 22
        ], true)) {
145
            $params['inReverse'] = $inReverse;
146
        }
147
148 22
        return $params;
149
    }
150
151 218
    protected function getAxisByName(string $name): ?Axis
152
    {
153 218
        foreach ($this->getCRS()->getCoordinateSystem()->getAxes() as $axis) {
154 218
            if ($axis->getName() === $name) {
155 218
                return $axis;
156
            }
157
        }
158
159 137
        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 15
    protected static function vincenty(GeographicValue $from, GeographicValue $to, Ellipsoid $ellipsoid): Length
175
    {
176 15
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
177 15
        $b = $ellipsoid->getSemiMinorAxis()->asMetres()->getValue();
178 15
        $f = $ellipsoid->getInverseFlattening();
179 15
        $U1 = atan((1 - $f) * tan($from->getLatitude()->asRadians()->getValue()));
180 15
        $U2 = atan((1 - $f) * tan($to->getLatitude()->asRadians()->getValue()));
181 15
        $L = $to->getLongitude()->subtract($from->getLongitude())->asRadians()->getValue();
182
183 15
        $lambda = $L;
184
        do {
185 15
            $lambdaN = $lambda;
186
187 15
            $sinSigma = sqrt((cos($U2) * sin($lambda)) ** 2 + (cos($U1) * sin($U2) - sin($U1) * cos($U2) * cos($lambda)) ** 2);
188 15
            $cosSigma = sin($U1) * sin($U2) + cos($U1) * cos($U2) * cos($lambda);
189 15
            $sigma = atan2($sinSigma, $cosSigma);
190
191 15
            $sinAlpha = cos($U1) * cos($U2) * sin($lambda) / $sinSigma;
192 15
            $cosSqAlpha = (1 - $sinAlpha ** 2);
193 15
            $cos2SigmaM = $cosSqAlpha ? $cosSigma - (2 * sin($U1) * sin($U2) / $cosSqAlpha) : 0;
194 15
            $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
195 15
            $lambda = $L + (1 - $C) * $f * $sinAlpha * ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (-1 + 2 * $cos2SigmaM ** 2)));
196 15
        } while (abs($lambda - $lambdaN) >= static::ITERATION_CONVERGENCE && abs($lambda) < M_PI);
197
198
        // Antipodal case
199 15
        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 15
        $E = sqrt(1 + (($a ** 2 - $b ** 2) / $b ** 2) * $cosSqAlpha);
228 15
        $F = ($E - 1) / ($E + 1);
229
230 15
        $A = (1 + $F ** 2 / 4) / (1 - $F);
231 15
        $B = $F * (1 - 3 / 8 * $F ** 2);
232
233 15
        $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM ** 2) - $B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma ** 2) * (-3 + 4 * $cos2SigmaM ** 2)));
234
235 15
        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 46
    protected static function asin(float $num): float
346
    {
347 46
        if ($num > 1.0) {
348
            $num = 1.0;
349 46
        } elseif ($num < -1.0) {
350
            $num = -1.0;
351
        }
352
353 46
        return asin($num);
354
    }
355
356
    abstract public function getCRS(): CoordinateReferenceSystem;
357
358
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
359
360
    abstract public function calculateDistance(self $to): Length;
361
}
362