Passed
Push — master ( be3192...a784d9 )
by Doug
25:13
created

Point::vincenty()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 62
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 43
CRAP Score 7.0005

Importance

Changes 0
Metric Value
cc 7
eloc 46
c 0
b 0
f 0
nc 6
nop 3
dl 0
loc 62
ccs 43
cts 44
cp 0.9773
crap 7.0005
rs 8.2448

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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