Passed
Push — master ( e5f776...37af97 )
by Doug
63:28
created

Point::generalPolynomialUnitlessReverse()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 37
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 20
nc 2
nop 11
dl 0
loc 37
rs 9.6
c 0
b 0
f 0

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\Point;
10
11
use DateTimeImmutable;
12
use PHPCoord\CoordinateOperation\CoordinateOperationMethods;
13
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...
14
use PHPCoord\CoordinateOperation\Grid;
15
use PHPCoord\CoordinateOperation\GridProvider;
16
use PHPCoord\CoordinateReferenceSystem\Compound;
17
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
18
use PHPCoord\CoordinateReferenceSystem\Geocentric;
19
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
20
use PHPCoord\CoordinateReferenceSystem\Geographic3D;
21
use PHPCoord\CoordinateReferenceSystem\Projected;
22
use PHPCoord\CoordinateReferenceSystem\Vertical;
23
use PHPCoord\UnitOfMeasure\Angle\Angle;
24
use PHPCoord\UnitOfMeasure\Length\Length;
25
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
26
use PHPCoord\UnitOfMeasure\Scale\Scale;
27
use PHPCoord\UnitOfMeasure\UnitOfMeasure;
28
use Stringable;
29
30
use function acos;
31
use function asin;
32
use function assert;
33
use function atan;
34
use function atan2;
35
use function class_exists;
36
use function is_string;
37
use function property_exists;
38
use function sscanf;
39
use function str_ends_with;
40
use function str_starts_with;
41
42
abstract class Point implements Stringable
43
{
44
    protected const ITERATION_CONVERGENCE_FORMULA = 1e-10;
45
    protected const ITERATION_CONVERGENCE_GRID = 0.0001;
46
    protected const METHODS_REQUIRING_HORIZONTAL_POINT = [
47
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_AND_SLOPE => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_AND_SLOPE,
48
        CoordinateOperationMethods::EPSG_ZERO_TIDE_HEIGHT_TO_MEAN_TIDE_HEIGHT_EVRF2019 => CoordinateOperationMethods::EPSG_ZERO_TIDE_HEIGHT_TO_MEAN_TIDE_HEIGHT_EVRF2019,
49
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_GTX => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_GTX,
50
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_PL_TXT => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_PL_TXT,
51
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_BEV_AT => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_BEV_AT,
52
        CoordinateOperationMethods::EPSG_VERTICAL_CHANGE_BY_GEOID_GRID_DIFFERENCE_NRCAN => CoordinateOperationMethods::EPSG_VERTICAL_CHANGE_BY_GEOID_GRID_DIFFERENCE_NRCAN,
53
    ];
54
    protected const METHODS_THAT_REQUIRE_DIRECTION = [
55
        CoordinateOperationMethods::EPSG_SIMILARITY_TRANSFORMATION => CoordinateOperationMethods::EPSG_SIMILARITY_TRANSFORMATION,
56
        CoordinateOperationMethods::EPSG_AFFINE_PARAMETRIC_TRANSFORMATION => CoordinateOperationMethods::EPSG_AFFINE_PARAMETRIC_TRANSFORMATION,
57
        CoordinateOperationMethods::EPSG_NADCON5_2D => CoordinateOperationMethods::EPSG_NADCON5_2D,
58
        CoordinateOperationMethods::EPSG_NADCON5_3D => CoordinateOperationMethods::EPSG_NADCON5_3D,
59
        CoordinateOperationMethods::EPSG_NTV2 => CoordinateOperationMethods::EPSG_NTV2,
60
        CoordinateOperationMethods::EPSG_ZERO_TIDE_HEIGHT_TO_MEAN_TIDE_HEIGHT_EVRF2019 => CoordinateOperationMethods::EPSG_ZERO_TIDE_HEIGHT_TO_MEAN_TIDE_HEIGHT_EVRF2019,
61
        CoordinateOperationMethods::EPSG_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN => CoordinateOperationMethods::EPSG_GEOCENTRIC_TRANSLATION_BY_GRID_INTERPOLATION_IGN,
62
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_GTX => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_GTX,
63
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_PL_TXT => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_PL_TXT,
64
        CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_BEV_AT => CoordinateOperationMethods::EPSG_VERTICAL_OFFSET_BY_GRID_INTERPOLATION_BEV_AT,
65
        CoordinateOperationMethods::EPSG_VERTICAL_CHANGE_BY_GEOID_GRID_DIFFERENCE_NRCAN => CoordinateOperationMethods::EPSG_VERTICAL_CHANGE_BY_GEOID_GRID_DIFFERENCE_NRCAN,
66
        CoordinateOperationMethods::EPSG_GENERAL_POLYNOMIAL_OF_DEGREE_2 => CoordinateOperationMethods::EPSG_GENERAL_POLYNOMIAL_OF_DEGREE_2,
67
        CoordinateOperationMethods::EPSG_GENERAL_POLYNOMIAL_OF_DEGREE_6 => CoordinateOperationMethods::EPSG_GENERAL_POLYNOMIAL_OF_DEGREE_6,
68
    ];
69
70
    /**
71
     * @var array<class-string, Grid>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, Grid> at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, Grid>.
Loading history...
72
     */
73
    protected static array $gridCache = [];
74
75
    /**
76
     * @internal
77
     * @param array{horizontalPoint?: Point} $additionalParams
78
     */
79
    public function performOperation(string $srid, Compound|Geocentric|Geographic2D|Geographic3D|Projected|Vertical $to, bool $inReverse, array $additionalParams = []): self
80
    {
81
        $operation = CoordinateOperations::getOperationData($srid);
82
83
        if ($operation['method'] === CoordinateOperationMethods::EPSG_ALIAS) {
84
            $point = clone $this;
85
            assert(property_exists($point, 'crs'));
86
            $point->crs = $to;
0 ignored issues
show
Bug Best Practice introduced by
The property crs does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
87
88
            return $point;
89
        } else {
90
            $method = CoordinateOperationMethods::getFunctionName($operation['method']);
91
            $params = self::resolveParamsByOperation($srid, $operation['method'], $inReverse);
92
93
            if (isset(self::METHODS_REQUIRING_HORIZONTAL_POINT[$operation['method']]) && isset($additionalParams['horizontalPoint'])) {
94
                $params['horizontalPoint'] = $additionalParams['horizontalPoint'];
95
            }
96
97
            return $this->$method($to, ...$params);
98
        }
99
    }
100
101
    /**
102
     * @return array<string, mixed>
103
     */
104
    protected static function resolveParamsByOperation(string $operationSrid, string $methodSrid, bool $inReverse): array
105
    {
106
        $methodData = CoordinateOperationMethods::getMethodData($methodSrid);
107
108
        $params = [];
109
        $powerCoefficients = [];
110
        foreach (CoordinateOperations::getParamData($operationSrid) as $paramName => $paramValue) {
111
            if (str_ends_with($paramName, 'File') && is_string($paramValue) && class_exists($paramValue) && new $paramValue() instanceof GridProvider) {
112
                $params[$paramName] = static::$gridCache[$paramValue] ??= (new $paramValue())->provideGrid();
113
            } else {
114
                if ($inReverse && isset($methodData['paramData'][$paramName]) && $methodData['paramData'][$paramName]['reverses']) {
115
                    $paramValue = $paramValue->multiply(-1);
116
                }
117
                if (str_starts_with($paramName, 'Au') || str_starts_with($paramName, 'Bu')) {
118
                    $powerCoefficients[$paramName] = $paramValue;
119
                } else {
120
                    $params[$paramName] = $paramValue;
121
                }
122
            }
123
        }
124
        if ($powerCoefficients) {
125
            $params['powerCoefficients'] = $powerCoefficients;
126
        }
127
        if (isset(self::METHODS_THAT_REQUIRE_DIRECTION[$methodSrid])) {
128
            if ($methodSrid === CoordinateOperationMethods::EPSG_VERTICAL_CHANGE_BY_GEOID_GRID_DIFFERENCE_NRCAN) {
129
                $inReverse = !$inReverse; // has a reversed grid
130
            }
131
            $params['inReverse'] = $inReverse;
132
        }
133
134
        return $params;
135
    }
136
137
    protected static function sign(float $number): int
138
    {
139
        if ($number < 0) {
140
            return -1;
141
        }
142
143
        return 1;
144
    }
145
146
    /**
147
     * General polynomial.
148
     * @param  array<string, Coefficient>  $powerCoefficients
149
     * @return array{xt: float, yt: float}
150
     */
151
    protected function generalPolynomialUnitless(
152
        float $xs,
153
        float $ys,
154
        UnitOfMeasure $ordinate1OfEvaluationPointInSourceCRS,
155
        UnitOfMeasure $ordinate2OfEvaluationPointInSourceCRS,
156
        UnitOfMeasure $ordinate1OfEvaluationPointInTargetCRS,
157
        UnitOfMeasure $ordinate2OfEvaluationPointInTargetCRS,
158
        Scale $scalingFactorForSourceCRSCoordDifferences,
159
        Scale $scalingFactorForTargetCRSCoordDifferences,
160
        Scale $A0,
161
        Scale $B0,
162
        array $powerCoefficients,
163
        bool $inReverse
164
    ): array {
165
        if (!$inReverse) {
166
            return $this->generalPolynomialUnitlessForward(
167
                $xs,
168
                $ys,
169
                $ordinate1OfEvaluationPointInSourceCRS,
170
                $ordinate2OfEvaluationPointInSourceCRS,
171
                $ordinate1OfEvaluationPointInTargetCRS,
172
                $ordinate2OfEvaluationPointInTargetCRS,
173
                $scalingFactorForSourceCRSCoordDifferences,
174
                $scalingFactorForTargetCRSCoordDifferences,
175
                $A0,
176
                $B0,
177
                $powerCoefficients,
178
            );
179
        } else {
180
            return $this->generalPolynomialUnitlessReverse(
181
                $xs,
182
                $ys,
183
                $ordinate1OfEvaluationPointInSourceCRS,
184
                $ordinate2OfEvaluationPointInSourceCRS,
185
                $ordinate1OfEvaluationPointInTargetCRS,
186
                $ordinate2OfEvaluationPointInTargetCRS,
187
                $scalingFactorForSourceCRSCoordDifferences,
188
                $scalingFactorForTargetCRSCoordDifferences,
189
                $A0,
190
                $B0,
191
                $powerCoefficients,
192
            );
193
        }
194
    }
195
196
    protected function generalPolynomialUnitlessForward(
197
        float $xs,
198
        float $ys,
199
        UnitOfMeasure $ordinate1OfEvaluationPointInSourceCRS,
200
        UnitOfMeasure $ordinate2OfEvaluationPointInSourceCRS,
201
        UnitOfMeasure $ordinate1OfEvaluationPointInTargetCRS,
202
        UnitOfMeasure $ordinate2OfEvaluationPointInTargetCRS,
203
        Scale $scalingFactorForSourceCRSCoordDifferences,
204
        Scale $scalingFactorForTargetCRSCoordDifferences,
205
        Scale $A0,
206
        Scale $B0,
207
        array $powerCoefficients
208
    ): array {
209
        $xso = $ordinate1OfEvaluationPointInSourceCRS->getValue();
210
        $yso = $ordinate2OfEvaluationPointInSourceCRS->getValue();
211
        $xto = $ordinate1OfEvaluationPointInTargetCRS->getValue();
212
        $yto = $ordinate2OfEvaluationPointInTargetCRS->getValue();
213
214
        $U = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($xs - $xso);
215
        $V = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($ys - $yso);
216
217
        $mTdX = $A0->getValue();
218
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
219
            if ($coefficientName[0] === 'A') {
220
                sscanf($coefficientName, 'Au%dv%d', $uPower, $vPower);
221
                $mTdX += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
222
            }
223
        }
224
225
        $mTdY = $B0->getValue();
226
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
227
            if ($coefficientName[0] === 'B') {
228
                sscanf($coefficientName, 'Bu%dv%d', $uPower, $vPower);
229
                $mTdY += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
230
            }
231
        }
232
233
        $xt = $xs - $xso + $xto + $mTdX / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
234
        $yt = $ys - $yso + $yto + $mTdY / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
235
236
        return ['xt' => $xt, 'yt' => $yt];
237
    }
238
239
    protected function generalPolynomialUnitlessReverse(
240
        float $xs,
241
        float $ys,
242
        UnitOfMeasure $ordinate1OfEvaluationPointInSourceCRS,
243
        UnitOfMeasure $ordinate2OfEvaluationPointInSourceCRS,
244
        UnitOfMeasure $ordinate1OfEvaluationPointInTargetCRS,
245
        UnitOfMeasure $ordinate2OfEvaluationPointInTargetCRS,
246
        Scale $scalingFactorForSourceCRSCoordDifferences,
247
        Scale $scalingFactorForTargetCRSCoordDifferences,
248
        Scale $A0,
249
        Scale $B0,
250
        array $powerCoefficients
251
    ): array {
252
        $result = ['xt' => $xs, 'yt' => $ys];
253
        for ($i = 0; $i <= 15; ++$i) {
254
            $forwardShiftedCoordinates = $this->generalPolynomialUnitlessForward(
255
                $result['xt'],
256
                $result['yt'],
257
                $ordinate1OfEvaluationPointInSourceCRS,
258
                $ordinate2OfEvaluationPointInSourceCRS,
259
                $ordinate1OfEvaluationPointInTargetCRS,
260
                $ordinate2OfEvaluationPointInTargetCRS,
261
                $scalingFactorForSourceCRSCoordDifferences,
262
                $scalingFactorForTargetCRSCoordDifferences,
263
                $A0,
264
                $B0,
265
                $powerCoefficients
266
            );
267
            $deltaError = [
268
                'xt' => $forwardShiftedCoordinates['xt'] - $xs,
269
                'yt' => $forwardShiftedCoordinates['yt'] - $ys,
270
            ];
271
            $result['xt'] -= $deltaError['xt'];
272
            $result['yt'] -= $deltaError['yt'];
273
        }
274
275
        return $result;
276
    }
277
278
    /**
279
     * Reversible polynomial.
280
     * @param  array<string, Coefficient>  $powerCoefficients
281
     * @return array{xt: float, yt: float}
282
     */
283
    protected function reversiblePolynomialUnitless(
284
        float $xs,
285
        float $ys,
286
        Angle $ordinate1OfEvaluationPoint,
287
        Angle $ordinate2OfEvaluationPoint,
288
        Scale $scalingFactorForCoordDifferences,
289
        Scale $A0,
290
        Scale $B0,
291
        array $powerCoefficients
292
    ): array {
293
        $xo = $ordinate1OfEvaluationPoint->getValue();
294
        $yo = $ordinate2OfEvaluationPoint->getValue();
295
296
        $U = $scalingFactorForCoordDifferences->asUnity()->getValue() * ($xs - $xo);
297
        $V = $scalingFactorForCoordDifferences->asUnity()->getValue() * ($ys - $yo);
298
299
        $mTdX = $A0->getValue();
300
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
301
            if ($coefficientName[0] === 'A') {
302
                sscanf($coefficientName, 'Au%dv%d', $uPower, $vPower);
303
                $mTdX += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
304
            }
305
        }
306
307
        $mTdY = $B0->getValue();
308
        foreach ($powerCoefficients as $coefficientName => $coefficientValue) {
309
            if ($coefficientName[0] === 'B') {
310
                sscanf($coefficientName, 'Bu%dv%d', $uPower, $vPower);
311
                $mTdY += $coefficientValue->getValue() * $U ** $uPower * $V ** $vPower;
312
            }
313
        }
314
315
        $xt = $xs + $mTdX * $scalingFactorForCoordDifferences->asUnity()->getValue();
316
        $yt = $ys + $mTdY * $scalingFactorForCoordDifferences->asUnity()->getValue();
317
318
        return ['xt' => $xt, 'yt' => $yt];
319
    }
320
321
    /**
322
     * Floating point vagaries mean that it's possible for inputs to be e.g. 1.00000000000001 which makes PHP give a
323
     * silent NaN as output so inputs need to be capped. atan/atan2 are not affected, they seem to cap internally.
324
     */
325
    protected static function acos(float $num): float
326
    {
327
        if ($num > 1.0) {
328
            $num = 1.0;
329
        } elseif ($num < -1) {
330
            $num = -1.0;
331
        }
332
333
        return acos($num);
334
    }
335
336
    /**
337
     * Floating point vagaries mean that it's possible for inputs to be e.g. 1.00000000000001 which makes PHP give a
338
     * silent NaN as output so inputs need to be capped. atan/atan2 are not affected, they seem to cap internally.
339
     */
340
    protected static function asin(float $num): float
341
    {
342
        if ($num > 1.0) {
343
            $num = 1.0;
344
        } elseif ($num < -1.0) {
345
            $num = -1.0;
346
        }
347
348
        return asin($num);
349
    }
350
351
    abstract public function getCRS(): CoordinateReferenceSystem;
352
353
    abstract public function getCoordinateEpoch(): ?DateTimeImmutable;
354
355
    abstract public function calculateDistance(self $to): Length;
356
}
357