Passed
Push — master ( 140fc1...a234d9 )
by Doug
12:58
created

Point::generalPolynomialUnitless()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 41
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 19
c 0
b 0
f 0
dl 0
loc 41
ccs 20
cts 20
cp 1
rs 9.3222
cc 5
nc 9
nop 11
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\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 243
    public function performOperation(string $srid, CoordinateReferenceSystem $to, bool $inReverse, array $additionalParams = []): self
58
    {
59 243
        $operations = self::resolveConcatenatedOperations($srid, $inReverse);
60
61 243
        $point = $this;
62 243
        foreach ($operations as $operationSrid => $operation) {
63 243
            $method = CoordinateOperationMethods::getFunctionName($operation['method']);
64 243
            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 243
                $destCRS = $to;
74
            }
75
76 243
            $params = self::resolveParamsByOperation($operationSrid, $operation['method'], $inReverse);
77
78 243
            if (in_array($operation['method'], [CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_AND_SLOPE, CoordinateOperationMethods::EPSG_ZERO_TIDE_HEIGHT_TO_MEAN_TIDE_HEIGHT_EVRF2019], true)) {
79 9
                $params['horizontalPoint'] = $additionalParams['horizontalPoint'];
80
            }
81
82 243
            if (PHP_MAJOR_VERSION >= 8) {
83
                $point = $point->$method($destCRS, ...$params);
84
            } else {
85 243
                $point = $point->$method($destCRS, ...array_values($params));
86
            }
87
        }
88
89
        // 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 243
        if ($point instanceof ConvertiblePoint && !$point->getCRS() instanceof $to) {// occasionally the CRS domain switches part way through the sequence (e.g. NAD86 geog to NAD86 geocen)
0 ignored issues
show
Bug introduced by
The method getCRS() does not exist on PHPCoord\CoordinateOperation\ConvertiblePoint. Since it exists in all sub-types, consider adding an abstract or default implementation to PHPCoord\CoordinateOperation\ConvertiblePoint. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

90
        if ($point instanceof ConvertiblePoint && !$point->/** @scrutinizer ignore-call */ getCRS() instanceof $to) {// occasionally the CRS domain switches part way through the sequence (e.g. NAD86 geog to NAD86 geocen)
Loading history...
91
            $point = $point->convert($to, true);
92
        }
93 243
        $point->crs = $to;
94
95 243
        return $point;
96
    }
97
98 243
    protected static function camelCase(string $string): string
99
    {
100 243
        $string = str_replace([' ', '-', '(', ')'], '', ucwords($string, ' -()'));
101 243
        if (!preg_match('/^(EPSG|[ABC][uv\d])/', $string)) {
102 243
            $string = lcfirst($string);
103
        }
104
105 243
        return $string;
106
    }
107
108 252
    protected static function resolveConcatenatedOperations(string $operationSrid, bool $inReverse): array
109
    {
110 252
        $operations = [];
111 252
        $operation = CoordinateOperations::getOperationData($operationSrid);
112 252
        if (isset($operation['operations'])) {
113 16
            foreach ($operation['operations'] as $subOperation) {
114 16
                $subOperationData = CoordinateOperations::getOperationData($subOperation['operation']);
115 16
                $subOperationData['source_crs'] = $subOperation['source_crs'];
116 16
                $subOperationData['target_crs'] = $subOperation['target_crs'];
117 16
                $operations[$subOperation['operation']] = $subOperationData;
118
            }
119
        } else {
120 252
            $operations[$operationSrid] = $operation;
121
        }
122
123 252
        if ($inReverse) {
124 113
            $operations = array_reverse($operations, true);
125
        }
126
127 252
        return $operations;
128
    }
129
130 243
    protected static function resolveParamsByOperation(string $operationSrid, string $methodSrid, bool $inReverse): array
131
    {
132 243
        $params = [];
133 243
        $powerCoefficients = [];
134 243
        foreach (CoordinateOperationParams::getParamData($operationSrid) as $paramName => $paramData) {
135 243
            if (isset($paramData['fileProvider'])) {
136 9
                $paramName = static::camelCase($paramName);
137
                /** @var GridProvider $provider */
138 9
                $provider = new $paramData['fileProvider']();
139 9
                $params[$paramName] = $provider->provideGrid();
140
            } else {
141 243
                $value = $paramData['value'];
142 243
                if ($inReverse && $paramData['reverses']) {
143 50
                    $value *= -1;
144
                }
145 243
                if ($paramData['uom']) {
146 243
                    $param = UnitOfMeasureFactory::makeUnit($value, $paramData['uom']);
147
                } else {
148 10
                    $param = $paramData['value'];
149
                }
150 243
                $paramName = static::camelCase($paramName);
151 243
                if (preg_match('/^(Au|Bu)/', $paramName)) {
152 27
                    $powerCoefficients[$paramName] = $param;
153
                } else {
154 243
                    $params[$paramName] = $param;
155
                }
156
            }
157
        }
158 243
        if ($powerCoefficients) {
159 27
            $params['powerCoefficients'] = $powerCoefficients;
160
        }
161 243
        if (in_array($methodSrid, [
162 243
            CoordinateOperationMethods::EPSG_SIMILARITY_TRANSFORMATION,
163 243
            CoordinateOperationMethods::EPSG_AFFINE_PARAMETRIC_TRANSFORMATION,
164 243
            CoordinateOperationMethods::EPSG_NADCON5_2D,
165 243
            CoordinateOperationMethods::EPSG_NADCON5_3D,
166 243
            CoordinateOperationMethods::EPSG_NTV2,
167 243
            CoordinateOperationMethods::EPSG_ZERO_TIDE_HEIGHT_TO_MEAN_TIDE_HEIGHT_EVRF2019,
168 243
            CoordinateOperationMethods::EPSG_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN,
169 243
        ], true)) {
170 3
            $params['inReverse'] = $inReverse;
171
        }
172
173 243
        return $params;
174
    }
175
176 2105
    protected function getAxisByName(string $name): ?Axis
177
    {
178 2105
        foreach ($this->getCRS()->getCoordinateSystem()->getAxes() as $axis) {
179 2105
            if ($axis->getName() === $name) {
180 2105
                return $axis;
181
            }
182
        }
183
184 1319
        return null;
185
    }
186
187 90
    protected static function sign(float $number): int
188
    {
189 90
        if ($number < 0) {
190
            return -1;
191
        }
192
193 90
        return 1;
194
    }
195
196
    /**
197
     * Calculate surface distance between two points.
198
     */
199 153
    protected static function vincenty(GeographicValue $from, GeographicValue $to, Ellipsoid $ellipsoid): Length
200
    {
201 153
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
202 153
        $b = $ellipsoid->getSemiMinorAxis()->asMetres()->getValue();
203 153
        $f = $ellipsoid->getInverseFlattening();
204 153
        $U1 = atan((1 - $f) * tan($from->getLatitude()->asRadians()->getValue()));
205 153
        $U2 = atan((1 - $f) * tan($to->getLatitude()->asRadians()->getValue()));
206 153
        $L = $to->getLongitude()->subtract($from->getLongitude())->asRadians()->getValue();
207
208 153
        $lambda = $L;
209
        do {
210 153
            $lambdaN = $lambda;
211
212 153
            $sinSigma = sqrt((cos($U2) * sin($lambda)) ** 2 + (cos($U1) * sin($U2) - sin($U1) * cos($U2) * cos($lambda)) ** 2);
213 153
            $cosSigma = sin($U1) * sin($U2) + cos($U1) * cos($U2) * cos($lambda);
214 153
            $sigma = atan2($sinSigma, $cosSigma);
215
216 153
            $sinAlpha = cos($U1) * cos($U2) * sin($lambda) / $sinSigma;
217 153
            $cosSqAlpha = (1 - $sinAlpha ** 2);
218 153
            $cos2SigmaM = $cosSqAlpha ? $cosSigma - (2 * sin($U1) * sin($U2) / $cosSqAlpha) : 0;
219 153
            $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
220 153
            $lambda = $L + (1 - $C) * $f * $sinAlpha * ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * $cosSigma * (-1 + 2 * $cos2SigmaM ** 2)));
221 153
        } while (abs($lambda - $lambdaN) >= static::ITERATION_CONVERGENCE_FORMULA && abs($lambda) < M_PI);
222
223
        // Antipodal case
224 153
        if (abs($lambda) >= M_PI) {
225 27
            if ($L >= 0) {
226 27
                $LPrime = M_PI - $L;
227
            } else {
228
                $LPrime = -M_PI - $L;
229
            }
230
231 27
            $lambdaPrime = 0;
232 27
            $sigma = M_PI - abs($U1 + $U2);
233 27
            $sinSigma = sin($sigma);
234 27
            $cosSqAlpha = 0.5;
235 27
            $sinAlpha = 0;
236
237
            do {
238 27
                $sinAlphaN = $sinAlpha;
239
240 27
                $C = $f / 16 * $cosSqAlpha * (4 + $f * (4 - 3 * $cosSqAlpha));
241 27
                $cos2SigmaM = cos($sigma) - 2 * sin($U1) * sin($U2) / $cosSqAlpha;
242 27
                $D = (1 - $C) * $f * ($sigma + $C * $sinSigma * ($cos2SigmaM + $C * cos($sigma) * (-1 + 2 * $cos2SigmaM ** 2)));
243 27
                $sinAlpha = ($LPrime - $lambdaPrime) / $D;
244 27
                $cosSqAlpha = (1 - $sinAlpha ** 2);
245 27
                $sinLambdaPrime = ($sinAlpha * $sinSigma) / (cos($U1) * cos($U2));
246 27
                $lambdaPrime = self::asin($sinLambdaPrime);
247 27
                $sinSqSigma = (cos($U2) * $sinLambdaPrime) ** 2 + (cos($U1) * sin($U2) + sin($U1) * cos($U2) * cos($lambdaPrime)) ** 2;
248 27
                $sinSigma = sqrt($sinSqSigma);
249 27
            } while (abs($sinAlpha - $sinAlphaN) >= static::ITERATION_CONVERGENCE_FORMULA);
250
        }
251
252 153
        $E = sqrt(1 + (($a ** 2 - $b ** 2) / $b ** 2) * $cosSqAlpha);
253 153
        $F = ($E - 1) / ($E + 1);
254
255 153
        $A = (1 + $F ** 2 / 4) / (1 - $F);
256 153
        $B = $F * (1 - 3 / 8 * $F ** 2);
257
258 153
        $deltaSigma = $B * $sinSigma * ($cos2SigmaM + $B / 4 * ($cosSigma * (-1 + 2 * $cos2SigmaM ** 2) - $B / 6 * $cos2SigmaM * (-3 + 4 * $sinSigma ** 2) * (-3 + 4 * $cos2SigmaM ** 2)));
259
260 153
        return new Metre($b * $A * ($sigma - $deltaSigma));
261
    }
262
263
    /**
264
     * General polynomial.
265
     * @param Coefficient[] $powerCoefficients
266
     */
267 18
    protected function generalPolynomialUnitless(
268
        float $xs,
269
        float $ys,
270
        UnitOfMeasure $ordinate1OfEvaluationPointInSourceCRS,
271
        UnitOfMeasure $ordinate2OfEvaluationPointInSourceCRS,
272
        UnitOfMeasure $ordinate1OfEvaluationPointInTargetCRS,
273
        UnitOfMeasure $ordinate2OfEvaluationPointInTargetCRS,
274
        Scale $scalingFactorForSourceCRSCoordDifferences,
275
        Scale $scalingFactorForTargetCRSCoordDifferences,
276
        Scale $A0,
277
        Scale $B0,
278
        array $powerCoefficients
279
    ): array {
280 18
        $xso = $ordinate1OfEvaluationPointInSourceCRS->getValue();
281 18
        $yso = $ordinate2OfEvaluationPointInSourceCRS->getValue();
282 18
        $xto = $ordinate1OfEvaluationPointInTargetCRS->getValue();
283 18
        $yto = $ordinate2OfEvaluationPointInTargetCRS->getValue();
284
285 18
        $U = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($xs - $xso);
286 18
        $V = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($ys - $yso);
287
288 18
        $mTdX = $A0->getValue();
289 18
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
290 18
            if ($coefficientName[0] === 'A') {
291 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...
292 18
                $mTdX += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
293
            }
294
        }
295
296 18
        $mTdY = $B0->getValue();
297 18
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
298 18
            if ($coefficientName[0] === 'B') {
299 18
                sscanf($coefficientName, 'Bu%dv%d', $uPower, $vPower);
300 18
                $mTdY += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
301
            }
302
        }
303
304 18
        $xt = $xs - $xso + $xto + $mTdX / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
305 18
        $yt = $ys - $yso + $yto + $mTdY / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
306
307 18
        return ['xt' => $xt, 'yt' => $yt];
308
    }
309
310
    /**
311
     * Reversible polynomial.
312
     */
313 36
    protected function reversiblePolynomialUnitless(
314
        float $xs,
315
        float $ys,
316
        Angle $ordinate1OfEvaluationPoint,
317
        Angle $ordinate2OfEvaluationPoint,
318
        Scale $scalingFactorForCoordDifferences,
319
        Scale $A0,
320
        Scale $B0,
321
        array $powerCoefficients
322
    ): array {
323 36
        $xo = $ordinate1OfEvaluationPoint->getValue();
324 36
        $yo = $ordinate2OfEvaluationPoint->getValue();
325
326 36
        $U = $scalingFactorForCoordDifferences->asUnity()->getValue() * ($xs - $xo);
327 36
        $V = $scalingFactorForCoordDifferences->asUnity()->getValue() * ($ys - $yo);
328
329 36
        $mTdX = $A0->getValue();
330 36
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
331 36
            if ($coefficientName[0] === 'A') {
332 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...
333 36
                $mTdX += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
334
            }
335
        }
336
337 36
        $mTdY = $B0->getValue();
338 36
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
339 36
            if ($coefficientName[0] === 'B') {
340 36
                sscanf($coefficientName, 'Bu%dv%d', $uPower, $vPower);
341 36
                $mTdY += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
342
            }
343
        }
344
345 36
        $xt = $xs + $mTdX * $scalingFactorForCoordDifferences->asUnity()->getValue();
346 36
        $yt = $ys + $mTdY * $scalingFactorForCoordDifferences->asUnity()->getValue();
347
348 36
        return ['xt' => $xt, 'yt' => $yt];
349
    }
350
351
    /**
352
     * Floating point vagaries mean that it's possible for inputs to be e.g. 1.00000000000001 which makes PHP give a
353
     * silent NaN as output so inputs need to be capped. atan/atan2 are not affected, they seem to cap internally.
354
     */
355
    protected static function acos(float $num): float
356
    {
357
        if ($num > 1.0) {
358
            $num = 1.0;
359
        } elseif ($num < -1) {
360
            $num = -1.0;
361
        }
362
363
        return acos($num);
364
    }
365
366
    /**
367
     * Floating point vagaries mean that it's possible for inputs to be e.g. 1.00000000000001 which makes PHP give a
368
     * silent NaN as output so inputs need to be capped. atan/atan2 are not affected, they seem to cap internally.
369
     */
370 500
    protected static function asin(float $num): float
371
    {
372 500
        if ($num > 1.0) {
373
            $num = 1.0;
374 500
        } elseif ($num < -1.0) {
375
            $num = -1.0;
376
        }
377
378 500
        return asin($num);
379
    }
380
381
    abstract public function getCRS(): CoordinateReferenceSystem;
382
383
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
384
385
    abstract public function calculateDistance(self $to): Length;
386
}
387