Passed
Push — master ( 62b2c1...aec388 )
by Doug
48:50 queued 07:26
created

ProjectedPoint::generalPolynomial()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 21
c 0
b 0
f 0
nc 1
nop 10
dl 0
loc 37
ccs 0
cts 10
cp 0
crap 2
rs 9.584

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 asinh;
13
use function atan;
14
use function atan2;
15
use function atanh;
16
use function cos;
17
use function cosh;
18
use function count;
19
use DateTime;
20
use DateTimeImmutable;
21
use DateTimeInterface;
22
use function hypot;
23
use function implode;
24
use function is_nan;
25
use function log;
26
use const M_E;
27
use const M_PI;
28
use const M_PI_2;
29
use function max;
30
use PHPCoord\CoordinateOperation\AutoConversion;
31
use PHPCoord\CoordinateOperation\ComplexNumber;
32
use PHPCoord\CoordinateOperation\ConvertiblePoint;
33
use PHPCoord\CoordinateOperation\OSTNOSGM15Grid;
34
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
35
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
36
use PHPCoord\CoordinateReferenceSystem\Geographic3D;
37
use PHPCoord\CoordinateReferenceSystem\Projected;
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateReferenceSystem\Projected 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...
38
use PHPCoord\CoordinateSystem\Axis;
39
use PHPCoord\CoordinateSystem\Cartesian;
40
use PHPCoord\Exception\InvalidAxesException;
41
use PHPCoord\Exception\InvalidCoordinateReferenceSystemException;
42
use PHPCoord\Exception\UnknownAxisException;
43
use PHPCoord\UnitOfMeasure\Angle\Angle;
44
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
45
use PHPCoord\UnitOfMeasure\Angle\Degree;
46
use PHPCoord\UnitOfMeasure\Angle\Radian;
47
use PHPCoord\UnitOfMeasure\Length\Length;
48
use PHPCoord\UnitOfMeasure\Length\Metre;
49
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
50
use PHPCoord\UnitOfMeasure\Scale\Scale;
51
use PHPCoord\UnitOfMeasure\Scale\Unity;
52
use function sin;
53
use function sinh;
54
use function sqrt;
55
use function substr;
56
use function tan;
57
use function tanh;
58
59
/**
60
 * Coordinate representing a point on a map projection.
61
 */
62
class ProjectedPoint extends Point implements ConvertiblePoint
63
{
64
    use AutoConversion {
65
        convert as protected autoConvert;
66
    }
67
68
    /**
69
     * Easting.
70
     */
71
    protected Length $easting;
72
73
    /**
74
     * Northing.
75
     */
76
    protected Length $northing;
77
78
    /**
79
     * Westing.
80
     */
81
    protected Length $westing;
82
83
    /**
84
     * Southing.
85
     */
86
    protected Length $southing;
87
88
    /**
89
     * Height.
90
     */
91
    protected ?Length $height;
92
93
    /**
94
     * Coordinate reference system.
95
     */
96
    protected Projected $crs;
97
98
    /**
99
     * Coordinate epoch (date for which the specified coordinates represented this point).
100
     */
101
    protected ?DateTimeImmutable $epoch;
102
103 1339
    protected function __construct(Projected $crs, ?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, ?DateTimeInterface $epoch, ?Length $height)
104
    {
105 1339
        if (count($crs->getCoordinateSystem()->getAxes()) === 2 && $height !== null) {
106
            throw new InvalidCoordinateReferenceSystemException('A 2D projected point must not include a height');
107
        }
108
109 1339
        if (count($crs->getCoordinateSystem()->getAxes()) === 3 && $height === null) {
110
            throw new InvalidCoordinateReferenceSystemException('A 3D projected point must include a height, none given');
111
        }
112
113 1339
        $this->crs = $crs;
114
115 1339
        $eastingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::EASTING);
116 1339
        $westingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::WESTING);
117 1339
        $northingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::NORTHING);
118 1339
        $southingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::SOUTHING);
119
120 1339
        if ($easting && $eastingAxis) {
121 1213
            $this->easting = $easting::convert($easting, $eastingAxis->getUnitOfMeasureId());
122 1213
            $this->westing = $this->easting->multiply(-1);
123 126
        } elseif ($westing && $westingAxis) {
124 117
            $this->westing = $westing::convert($westing, $westingAxis->getUnitOfMeasureId());
125 117
            $this->easting = $this->westing->multiply(-1);
126
        } else {
127 9
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
128
        }
129
130 1330
        if ($northing && $northingAxis) {
131 1240
            $this->northing = $northing::convert($northing, $northingAxis->getUnitOfMeasureId());
132 1240
            $this->southing = $this->northing->multiply(-1);
133 90
        } elseif ($southing && $southingAxis) {
134 81
            $this->southing = $southing::convert($southing, $southingAxis->getUnitOfMeasureId());
135 81
            $this->northing = $this->southing->multiply(-1);
136
        } else {
137 9
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
138
        }
139
140 1321
        if ($epoch instanceof DateTime) {
141 18
            $epoch = DateTimeImmutable::createFromMutable($epoch);
142
        }
143 1321
        $this->epoch = $epoch;
144
145 1321
        $this->height = $height;
146
    }
147
148 1114
    public static function create(Projected $crs, ?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, ?DateTimeInterface $epoch = null, ?Length $height = null): self
149
    {
150 1114
        return match ($crs->getSRID()) {
151 1114
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint($easting, $northing, $epoch),
0 ignored issues
show
Bug introduced by
It seems like $easting can also be of type null; however, parameter $easting of PHPCoord\BritishNationalGridPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

151
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\BritishNationalGridPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

151
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
152 1061
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($easting, $northing, $epoch),
0 ignored issues
show
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\IrishGridPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

152
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
Bug introduced by
It seems like $easting can also be of type null; however, parameter $easting of PHPCoord\IrishGridPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

152
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
153 1052
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint($easting, $northing, $epoch),
0 ignored issues
show
Bug introduced by
It seems like $easting can also be of type null; however, parameter $easting of PHPCoord\IrishTransverse...torPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

153
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\IrishTransverse...torPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

153
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
154 1114
            default => new static($crs, $easting, $northing, $westing, $southing, $epoch, $height),
155
        };
156
    }
157
158 500
    public static function createFromEastingNorthing(Projected $crs, Length $easting, Length $northing, ?DateTimeInterface $epoch = null, ?Length $height = null): self
159
    {
160 500
        return static::create($crs, $easting, $northing, null, null, $epoch, $height);
161
    }
162
163 27
    public static function createFromWestingNorthing(Projected $crs, Length $westing, Length $northing, ?DateTimeInterface $epoch = null, ?Length $height = null): self
164
    {
165 27
        return static::create($crs, null, $northing, $westing, null, $epoch, $height);
166
    }
167
168 54
    public static function createFromWestingSouthing(Projected $crs, Length $westing, Length $southing, ?DateTimeInterface $epoch = null, ?Length $height = null): self
169
    {
170 54
        return static::create($crs, null, null, $westing, $southing, $epoch, $height);
171
    }
172
173 592
    public function getEasting(): Length
174
    {
175 592
        return $this->easting;
176
    }
177
178 610
    public function getNorthing(): Length
179
    {
180 610
        return $this->northing;
181
    }
182
183 90
    public function getWesting(): Length
184
    {
185 90
        return $this->westing;
186
    }
187
188 72
    public function getSouthing(): Length
189
    {
190 72
        return $this->southing;
191
    }
192
193 9
    public function getHeight(): ?Length
194
    {
195 9
        return $this->height;
196
    }
197
198 280
    public function getCRS(): Projected
199
    {
200 280
        return $this->crs;
201
    }
202
203 51
    public function getCoordinateEpoch(): ?DateTimeImmutable
204
    {
205 51
        return $this->epoch;
206
    }
207
208
    /**
209
     * Calculate distance between two points.
210
     * Because this is a simple grid, we can use Pythagoras.
211
     */
212 63
    public function calculateDistance(Point $to): Length
213
    {
214
        try {
215 63
            if ($to instanceof ConvertiblePoint) {
216 63
                $to = $to->convert($this->crs);
217
            }
218
        } finally {
219 63
            if ($to->getCRS()->getSRID() !== $this->crs->getSRID()) {
220 9
                throw new InvalidCoordinateReferenceSystemException('Can only calculate distances between two points in the same CRS');
221
            }
222
223
            /* @var ProjectedPoint $to */
224 54
            return new Metre(
225 54
                sqrt(
226 54
                    ($to->getEasting()->getValue() - $this->getEasting()->getValue()) ** 2 +
227 54
                    ($to->getNorthing()->getValue() - $this->getNorthing()->getValue()) ** 2
228
                )
229
            );
230
        }
231
    }
232
233 54
    public function asGeographicPoint(): GeographicPoint
234
    {
235 54
        return $this->performOperation($this->crs->getDerivingConversion(), $this->crs->getBaseCRS(), true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->performOpe...rs->getBaseCRS(), true) returns the type PHPCoord\Point which includes types incompatible with the type-hinted return PHPCoord\GeographicPoint.
Loading history...
236
    }
237
238 100
    public function convert(CoordinateReferenceSystem $to, bool $ignoreBoundaryRestrictions = false): Point
239
    {
240 100
        if ($to->getSRID() === $this->crs->getBaseCRS()->getSRID()) {
241 27
            return $this->performOperation($this->crs->getDerivingConversion(), $this->crs->getBaseCRS(), true);
242
        }
243
244 91
        return $this->autoConvert($to, $ignoreBoundaryRestrictions);
245
    }
246
247 162
    public function __toString(): string
248
    {
249 162
        $values = [];
250 162
        foreach ($this->getCRS()->getCoordinateSystem()->getAxes() as $axis) {
251 162
            if ($axis->getName() === Axis::EASTING) {
252 144
                $values[] = $this->easting;
253 162
            } elseif ($axis->getName() === Axis::NORTHING) {
254 153
                $values[] = $this->northing;
255 18
            } elseif ($axis->getName() === Axis::WESTING) {
256 18
                $values[] = $this->westing;
257 9
            } elseif ($axis->getName() === Axis::SOUTHING) {
258 9
                $values[] = $this->southing;
259
            } elseif ($axis->getName() === Axis::ELLIPSOIDAL_HEIGHT) {
260
                $values[] = $this->height;
261
            } else {
262
                throw new UnknownAxisException(); // @codeCoverageIgnore
263
            }
264
        }
265
266 162
        return '(' . implode(', ', $values) . ')';
267
    }
268
269
    /**
270
     * Affine parametric transformation.
271
     */
272 9
    public function affineParametricTransform(
273
        Projected $to,
274
        Length $A0,
275
        Coefficient $A1,
276
        Coefficient $A2,
277
        Length $B0,
278
        Coefficient $B1,
279
        Coefficient $B2,
280
        bool $inReverse
281
    ): self {
282 9
        $xs = $this->easting->getValue(); // native unit to metre conversion already embedded in the scale factor
283 9
        $ys = $this->northing->getValue(); // native unit to metre conversion already embedded in the scale factor
284
285 9
        if ($inReverse) {
286
            $D = ($A1->getValue() * $B2->getValue()) - ($A2->getValue() * $B1->getValue());
287
            $a0 = (($A2->getValue() * $B0->asMetres()->getValue()) - ($B2->getValue() * $A0->asMetres()->getValue())) / $D;
288
            $b0 = (($B1->getValue() * $A0->asMetres()->getValue()) - ($A1->getValue() * $B0->asMetres()->getValue())) / $D;
289
            $a1 = $B2->getValue() / $D;
290
            $a2 = -$A2->getValue() / $D;
291
            $b1 = -$B1->getValue() / $D;
292
            $b2 = $A1->getValue() / $D;
293
        } else {
294 9
            $a0 = $A0->asMetres()->getValue();
295 9
            $a1 = $A1->getValue();
296 9
            $a2 = $A2->getValue();
297 9
            $b0 = $B0->asMetres()->getValue();
298 9
            $b1 = $B1->getValue();
299 9
            $b2 = $B2->getValue();
300
        }
301
302 9
        $xt = $a0 + ($a1 * $xs) + ($a2 * $ys);
303 9
        $yt = $b0 + ($b1 * $xs) + ($b2 * $ys);
304
305 9
        return static::create($to, new Metre($xt), new Metre($yt), new Metre(-$xt), new Metre(-$yt), $this->epoch);
306
    }
307
308
    /**
309
     * Albers Equal Area.
310
     */
311 18
    public function albersEqualArea(
312
        Geographic2D|Geographic3D $to,
313
        Angle $latitudeOfFalseOrigin,
314
        Angle $longitudeOfFalseOrigin,
315
        Angle $latitudeOf1stStandardParallel,
316
        Angle $latitudeOf2ndStandardParallel,
317
        Length $eastingAtFalseOrigin,
318
        Length $northingAtFalseOrigin
319
    ): GeographicPoint {
320 18
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
321 18
        $easting = $this->easting->asMetres()->getValue() - $eastingAtFalseOrigin->asMetres()->getValue();
322 18
        $northing = $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue();
323 18
        $phiOrigin = $latitudeOfFalseOrigin->asRadians()->getValue();
324 18
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
325 18
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
326 18
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
327 18
        $e = $ellipsoid->getEccentricity();
328 18
        $e2 = $ellipsoid->getEccentricitySquared();
329 18
        $e4 = $e ** 4;
330 18
        $e6 = $e ** 6;
331
332 18
        $centralMeridianFirstParallel = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
333 18
        $centralMeridianSecondParallel = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
334
335 18
        $alphaOrigin = (1 - $e2) * (sin($phiOrigin) / (1 - $e2 * sin($phiOrigin) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($phiOrigin)) / (1 + $e * sin($phiOrigin))));
336 18
        $alphaFirstParallel = (1 - $e2) * (sin($phi1) / (1 - $e2 * sin($phi1) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))));
337 18
        $alphaSecondParallel = (1 - $e2) * (sin($phi2) / (1 - $e2 * sin($phi2) ** 2) - (1 / 2 / $e) * log((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))));
338
339 18
        $n = ($centralMeridianFirstParallel ** 2 - $centralMeridianSecondParallel ** 2) / ($alphaSecondParallel - $alphaFirstParallel);
340 18
        $C = $centralMeridianFirstParallel ** 2 + $n * $alphaFirstParallel;
341 18
        $rhoOrigin = $a * sqrt($C - $n * $alphaOrigin) / $n;
342 18
        $rhoPrime = hypot($easting, $rhoOrigin - $northing);
343 18
        $alphaPrime = ($C - $rhoPrime ** 2 * $n ** 2 / $a ** 2) / $n;
344 18
        $betaPrime = self::asin($alphaPrime / (1 - (1 - $e2) / 2 / $e * log((1 - $e) / (1 + $e))));
345 18
        if ($n > 0) {
346 9
            $theta = atan2($easting, $rhoOrigin - $northing);
347
        } else {
348 9
            $theta = atan2(-$easting, $northing - $rhoOrigin);
349
        }
350
351 18
        $latitude = $betaPrime + (($e2 / 3 + 31 * $e4 / 180 + 517 * $e6 / 5040) * sin(2 * $betaPrime)) + ((23 * $e4 / 360 + 251 * $e6 / 3780) * sin(4 * $betaPrime)) + ((761 * $e6 / 45360) * sin(6 * $betaPrime));
352 18
        $longitude = $longitudeOfFalseOrigin->asRadians()->getValue() + ($theta / $n);
353
354 18
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
355
    }
356
357
    /**
358
     * American Polyconic.
359
     */
360 9
    public function americanPolyconic(
361
        Geographic2D|Geographic3D $to,
362
        Angle $latitudeOfNaturalOrigin,
363
        Angle $longitudeOfNaturalOrigin,
364
        Length $falseEasting,
365
        Length $falseNorthing
366
    ): GeographicPoint {
367 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
368 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
369 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
370 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
371 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
372 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
373 9
        $e = $ellipsoid->getEccentricity();
374 9
        $e2 = $ellipsoid->getEccentricitySquared();
375 9
        $e4 = $e ** 4;
376 9
        $e6 = $e ** 6;
377
378 9
        $i = (1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256);
379 9
        $ii = (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024);
380 9
        $iii = (15 * $e4 / 256 + 45 * $e6 / 1024);
381 9
        $iv = (35 * $e6 / 3072);
382
383 9
        $MO = $a * ($i * $latitudeOrigin - $ii * sin(2 * $latitudeOrigin) + $iii * sin(4 * $latitudeOrigin) - $iv * sin(6 * $latitudeOrigin));
384
385 9
        if ($MO === $northing) {
386
            $latitude = 0;
387
            $longitude = $longitudeOrigin + $easting / $a;
388
        } else {
389 9
            $A = ($MO + $northing) / $a;
390 9
            $B = $A ** 2 + $easting ** 2 / $a ** 2;
391
392 9
            $latitude = $A;
393 9
            $C = sqrt(1 - $e2 * sin($latitude) ** 2) * tan($latitude);
394
            do {
395 9
                $latitudeN = $latitude;
396 9
                $Ma = $i * $latitude - $ii * sin(2 * $latitude) + $iii * sin(4 * $latitude) - $iv * sin(6 * $latitude);
397 9
                $MnPrime = $i - 2 * $ii * cos(2 * $latitude) + 4 * $iii * cos(4 * $latitude) - 6 * $iv * cos(6 * $latitude);
398 9
                $latitude = $latitude - ($A * ($C * $Ma + 1) - $Ma - $C * ($Ma ** 2 + $B) / 2) / ($e2 * sin(2 * $latitude) * ($Ma ** 2 + $B - 2 * $A * $Ma) / 4 * $C + ($A - $Ma) * ($C * $MnPrime - (2 / sin(2 * $latitude))) - $MnPrime);
399 9
                $C = sqrt(1 - $e2 * sin($latitude) ** 2) * tan($latitude);
400 9
            } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
401
402 9
            $longitude = $longitudeOrigin + (self::asin($easting * $C / $a)) / sin($latitude);
403
        }
404
405 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
406
    }
407
408
    /**
409
     * Bonne.
410
     */
411 9
    public function bonne(
412
        Geographic2D|Geographic3D $to,
413
        Angle $latitudeOfNaturalOrigin,
414
        Angle $longitudeOfNaturalOrigin,
415
        Length $falseEasting,
416
        Length $falseNorthing
417
    ): GeographicPoint {
418 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
419 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
420 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
421 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
422 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
423 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
424 9
        $e = $ellipsoid->getEccentricity();
425 9
        $e2 = $ellipsoid->getEccentricitySquared();
426 9
        $e4 = $e ** 4;
427 9
        $e6 = $e ** 6;
428
429 9
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
430 9
        $MO = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitudeOrigin - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitudeOrigin) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitudeOrigin) - (35 * $e6 / 3072) * sin(6 * $latitudeOrigin));
431 9
        $rho = hypot($easting, $a * $mO / sin($latitudeOrigin) - $northing) * static::sign($latitudeOrigin);
432
433 9
        $M = $a * $mO / sin($latitudeOrigin) + $MO - $rho;
434 9
        $mu = $M / ($a * (1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256));
435 9
        $e1 = (1 - sqrt(1 - $e2)) / (1 + sqrt(1 - $e2));
436
437 9
        $latitude = $mu + ((3 * $e1 / 2) - (27 * $e1 ** 3 / 32)) * sin(2 * $mu) + ((21 * $e1 ** 2 / 16) - (55 * $e1 ** 4 / 32)) * sin(4 * $mu) + ((151 * $e1 ** 3 / 96)) * sin(6 * $mu) + ((1097 * $e1 ** 4 / 512)) * sin(8 * $mu);
438 9
        $m = cos($latitude) / sqrt(1 - $e2 * sin($latitude) ** 2);
439
440 9
        if ($m === 0.0) {
441
            $longitude = $longitudeOrigin; // pole
442 9
        } elseif ($latitudeOrigin >= 0) {
443 9
            $longitude = $longitudeOrigin + $rho * atan2($easting, $a * $mO / sin($latitudeOrigin) - $northing) / $a / $m;
444
        } else {
445
            $longitude = $longitudeOrigin + $rho * atan2(-$easting, -($a * $mO / sin($latitudeOrigin) - $northing)) / $a / $m;
446
        }
447
448 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
449
    }
450
451
    /**
452
     * Bonne South Orientated.
453
     */
454 9
    public function bonneSouthOrientated(
455
        Geographic2D|Geographic3D $to,
456
        Angle $latitudeOfNaturalOrigin,
457
        Angle $longitudeOfNaturalOrigin,
458
        Length $falseEasting,
459
        Length $falseNorthing
460
    ): GeographicPoint {
461 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
462 9
        $westing = $falseEasting->asMetres()->getValue() - $this->westing->asMetres()->getValue();
463 9
        $southing = $falseNorthing->asMetres()->getValue() - $this->southing->asMetres()->getValue();
464 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
465 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
466 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
467 9
        $e = $ellipsoid->getEccentricity();
468 9
        $e2 = $ellipsoid->getEccentricitySquared();
469 9
        $e4 = $e ** 4;
470 9
        $e6 = $e ** 6;
471
472 9
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
473 9
        $MO = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitudeOrigin - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitudeOrigin) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitudeOrigin) - (35 * $e6 / 3072) * sin(6 * $latitudeOrigin));
474 9
        $rho = hypot($westing, $a * $mO / sin($latitudeOrigin) - $southing) * static::sign($latitudeOrigin);
475
476 9
        $M = $a * $mO / sin($latitudeOrigin) + $MO - $rho;
477 9
        $mu = $M / ($a * (1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256));
478 9
        $e1 = (1 - sqrt(1 - $e2)) / (1 + sqrt(1 - $e2));
479
480 9
        $latitude = $mu + ((3 * $e1 / 2) - (27 * $e1 ** 3 / 32)) * sin(2 * $mu) + ((21 * $e1 ** 2 / 16) - (55 * $e1 ** 4 / 32)) * sin(4 * $mu) + ((151 * $e1 ** 3 / 96)) * sin(6 * $mu) + ((1097 * $e1 ** 4 / 512)) * sin(8 * $mu);
481 9
        $m = cos($latitude) / sqrt(1 - $e2 * sin($latitude) ** 2);
482
483 9
        if ($m === 0.0) {
484
            $longitude = $longitudeOrigin; // pole
485 9
        } elseif ($latitudeOrigin >= 0) {
486 9
            $longitude = $longitudeOrigin + $rho * atan2($westing, $a * $mO / sin($latitudeOrigin) - $southing) / $a / $m;
487
        } else {
488
            $longitude = $longitudeOrigin + $rho * atan2(-$westing, -($a * $mO / sin($latitudeOrigin) - $southing)) / $a / $m;
489
        }
490
491 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
492
    }
493
494
    /**
495
     * Cartesian Grid Offsets
496
     * This transformation allows calculation of coordinates in the target system by adding the parameter value to the
497
     * coordinate values of the point in the source system.
498
     */
499 9
    public function offsets(
500
        Projected $to,
501
        Length $eastingOffset,
502
        Length $northingOffset
503
    ): self {
504 9
        $easting = $this->easting->asMetres()->getValue() + $eastingOffset->asMetres()->getValue();
505 9
        $northing = $this->northing->asMetres()->getValue() + $northingOffset->asMetres()->getValue();
506
507 9
        return static::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
508
    }
509
510
    /**
511
     * Cassini-Soldner.
512
     */
513 9
    public function cassiniSoldner(
514
        Geographic2D|Geographic3D $to,
515
        Angle $latitudeOfNaturalOrigin,
516
        Angle $longitudeOfNaturalOrigin,
517
        Length $falseEasting,
518
        Length $falseNorthing
519
    ): GeographicPoint {
520 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
521 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
522 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
523 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
524 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
525 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
526 9
        $e = $ellipsoid->getEccentricity();
527 9
        $e2 = $ellipsoid->getEccentricitySquared();
528 9
        $e4 = $e ** 4;
529 9
        $e6 = $e ** 6;
530
531 9
        $MO = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitudeOrigin - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitudeOrigin) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitudeOrigin) - (35 * $e6 / 3072) * sin(6 * $latitudeOrigin));
532
533 9
        $e1 = (1 - sqrt(1 - $e2)) / (1 + sqrt(1 - $e2));
534 9
        $M = $MO + $northing;
535 9
        $mu = $M / ($a * (1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256));
536 9
        $latitudeCentralMeridian = $mu + (3 * $e1 / 2 - 27 * $e1 ** 3 / 32) * sin(2 * $mu) + (21 * $e1 ** 2 / 16 - 55 * $e1 ** 4 / 32) * sin(4 * $mu) + (151 * $e1 ** 3 / 96) * sin(6 * $mu) + (1097 * $e1 ** 4 / 512) * sin(8 * $mu);
537
538 9
        $nu = $a / sqrt((1 - $e2 * sin($latitudeCentralMeridian) ** 2));
539 9
        $rho = $a * (1 - $e2) / (1 - $e2 * sin($latitudeCentralMeridian) ** 2) ** 1.5;
540
541 9
        $T = tan($latitudeCentralMeridian) ** 2;
542 9
        $D = $easting / $nu;
543
544 9
        $latitude = $latitudeCentralMeridian - ($nu * tan($latitudeCentralMeridian) / $rho) * ($D ** 2 / 2 - (1 + 3 * $T) * $D ** 4 / 24);
545 9
        $longitude = $longitudeOrigin + ($D - $T * $D ** 3 / 3 + (1 + 3 * $T) * $T * $D ** 5 / 15) / cos($latitudeCentralMeridian);
546
547 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
548
    }
549
550
    /**
551
     * Hyperbolic Cassini-Soldner.
552
     */
553 9
    public function hyperbolicCassiniSoldner(
554
        Geographic2D|Geographic3D $to,
555
        Angle $latitudeOfNaturalOrigin,
556
        Angle $longitudeOfNaturalOrigin,
557
        Length $falseEasting,
558
        Length $falseNorthing
559
    ): GeographicPoint {
560 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
561 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
562 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
563 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
564 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
565 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
566 9
        $e = $ellipsoid->getEccentricity();
567 9
        $e2 = $ellipsoid->getEccentricitySquared();
568 9
        $e4 = $e ** 4;
569 9
        $e6 = $e ** 6;
570
571 9
        $MO = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitudeOrigin - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitudeOrigin) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitudeOrigin) - (35 * $e6 / 3072) * sin(6 * $latitudeOrigin));
572 9
        $e1 = (1 - sqrt(1 - $e2)) / (1 + sqrt(1 - $e2));
573
574 9
        $latitude1 = $latitudeOrigin + $northing / 1567446;
575
576 9
        $nu = $a / sqrt((1 - $e2 * sin($latitude1) ** 2));
577 9
        $rho = $a * (1 - $e2) / (1 - $e2 * sin($latitude1) ** 2) ** 1.5;
578
579 9
        $qPrime = $northing ** 3 / (6 * $rho * $nu);
580 9
        $q = ($northing + $qPrime) ** 3 / (6 * $rho * $nu);
581 9
        $M = $MO + $northing + $q;
582
583 9
        $mu = $M / ($a * (1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256));
584 9
        $latitudeCentralMeridian = $mu + (3 * $e1 / 2 - 27 * $e1 ** 3 / 32) * sin(2 * $mu) + (21 * $e1 ** 2 / 16 - 55 * $e1 ** 4 / 32) * sin(4 * $mu) + (151 * $e1 ** 3 / 96) * sin(6 * $mu) + (1097 * $e1 ** 4 / 512) * sin(8 * $mu);
585
586 9
        $T = tan($latitudeCentralMeridian) ** 2;
587 9
        $D = $easting / $nu;
588
589 9
        $latitude = $latitudeCentralMeridian - ($nu * tan($latitudeCentralMeridian) / $rho) * ($D ** 2 / 2 - (1 + 3 * $T) * $D ** 4 / 24);
590 9
        $longitude = $longitudeOrigin + ($D - $T * $D ** 3 / 3 + (1 + 3 * $T) * $T * $D ** 5 / 15) / cos($latitudeCentralMeridian);
591
592 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
593
    }
594
595
    /**
596
     * Colombia Urban.
597
     */
598 9
    public function columbiaUrban(
599
        Geographic2D|Geographic3D $to,
600
        Angle $latitudeOfNaturalOrigin,
601
        Angle $longitudeOfNaturalOrigin,
602
        Length $falseEasting,
603
        Length $falseNorthing,
604
        Length $projectionPlaneOriginHeight
605
    ): GeographicPoint {
606 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
607 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
608 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
609 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
610 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
611 9
        $heightOrigin = $projectionPlaneOriginHeight->asMetres()->getValue();
612 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
613 9
        $e2 = $ellipsoid->getEccentricitySquared();
614
615 9
        $rhoOrigin = $a * (1 - $e2) / (1 - $e2 * sin($latitudeOrigin) ** 2) ** 1.5;
616
617 9
        $nuOrigin = $a / sqrt(1 - $e2 * (sin($latitudeOrigin) ** 2));
618
619 9
        $B = tan($latitudeOrigin) / (2 * $rhoOrigin * $nuOrigin);
620 9
        $C = 1 + $heightOrigin / $a;
621 9
        $D = $rhoOrigin * (1 + $heightOrigin / ($a * (1 - $e2)));
622
623 9
        $latitude = $latitudeOrigin + ($northing / $D) - $B * ($easting / $C) ** 2;
624 9
        $nu = $a / sqrt(1 - $e2 * (sin($latitude) ** 2));
625 9
        $longitude = $longitudeOrigin + $easting / ($C * $nu * cos($latitude));
626
627 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
628
    }
629
630
    /**
631
     * Equal Earth.
632
     */
633 9
    public function equalEarth(
634
        Geographic2D|Geographic3D $to,
635
        Angle $longitudeOfNaturalOrigin,
636
        Length $falseEasting,
637
        Length $falseNorthing
638
    ): GeographicPoint {
639 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
640 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
641 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
642 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
643 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
644 9
        $e = $ellipsoid->getEccentricity();
645 9
        $e2 = $ellipsoid->getEccentricitySquared();
646 9
        $e4 = $e ** 4;
647 9
        $e6 = $e ** 6;
648
649 9
        $qP = (1 - $e2) * ((1 / (1 - $e2)) - (1 / (2 * $e) * log((1 - $e) / (1 + $e))));
650 9
        $Rq = $a * sqrt($qP / 2);
651
652 9
        $theta = $northing / $Rq;
653
        do {
654 9
            $thetaN = $theta;
655 9
            $correctionFactor = ($theta * (1.340264 - 0.081106 * $theta ** 2 + $theta ** 6 * (0.000893 + 0.003796 * $theta ** 2)) - $northing / $Rq) / (1.340264 - 0.243318 * $theta ** 2 + $theta ** 6 * (0.006251 + 0.034164 * $theta ** 2));
656 9
            $theta = $theta - $correctionFactor;
657 9
        } while (abs($theta - $thetaN) >= static::ITERATION_CONVERGENCE_FORMULA);
658
659 9
        $beta = self::asin(2 * sin($theta) / sqrt(3));
660
661 9
        $latitude = $beta + (($e2 / 3 + 31 * $e4 / 180 + 517 * $e6 / 5040) * sin(2 * $beta)) + ((23 * $e4 / 360 + 251 * $e6 / 3780) * sin(4 * $beta)) + ((761 * $e6 / 45360) * sin(6 * $beta));
662 9
        $longitude = $longitudeOrigin + sqrt(3) * $easting * (1.340264 - 0.243318 * $theta ** 2 + $theta ** 6 * (0.006251 + 0.034164 * $theta ** 2)) / (2 * $Rq * cos($theta));
663
664 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
665
    }
666
667
    /**
668
     * Equidistant Cylindrical
669
     * See method code 1029 for spherical development. See also Pseudo Plate Carree, method code 9825.
670
     */
671 9
    public function equidistantCylindrical(
672
        Geographic2D|Geographic3D $to,
673
        Angle $latitudeOf1stStandardParallel,
674
        Angle $longitudeOfNaturalOrigin,
675
        Length $falseEasting,
676
        Length $falseNorthing
677
    ): GeographicPoint {
678 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
679 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
680 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
681 9
        $latitudeFirstParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
682 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
683 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
684 9
        $e = $ellipsoid->getEccentricity();
685 9
        $e2 = $ellipsoid->getEccentricitySquared();
686 9
        $e4 = $e ** 4;
687 9
        $e6 = $e ** 6;
688 9
        $e8 = $e ** 8;
689 9
        $e10 = $e ** 10;
690 9
        $e12 = $e ** 12;
691 9
        $e14 = $e ** 14;
692
693 9
        $n = (1 - sqrt(1 - $e2)) / (1 + sqrt(1 - $e2));
694 9
        $n2 = $n ** 2;
695 9
        $n3 = $n ** 3;
696 9
        $n4 = $n ** 4;
697 9
        $n5 = $n ** 5;
698 9
        $n6 = $n ** 6;
699 9
        $n7 = $n ** 7;
700 9
        $mu = $northing / ($a * (1 - 1 / 4 * $e2 - 3 / 64 * $e4 - 5 / 256 * $e6 - 175 / 16384 * $e8 - 441 / 65536 * $e10 - 4851 / 1048576 * $e12 - 14157 / 4194304 * $e14));
701
702 9
        $latitude = $mu + (3 / 2 * $n - 27 / 32 * $n3 + 269 / 512 * $n5 - 6607 / 24576 * $n7) * sin(2 * $mu)
703 9
            + (21 / 16 * $n2 - 55 / 32 * $n4 + 6759 / 4096 * $n6) * sin(4 * $mu)
704 9
            + (151 / 96 * $n3 - 417 / 128 * $n5 + 87963 / 20480 * $n7) * sin(6 * $mu)
705 9
            + (1097 / 512 * $n4 - 15543 / 2560 * $n6) * sin(8 * $mu)
706 9
            + (8011 / 2560 * $n5 - 69119 / 6144 * $n7) * sin(10 * $mu)
707 9
            + (293393 / 61440 * $n6) * sin(12 * $mu)
708 9
            + (6845701 / 860160 * $n7) * sin(14 * $mu);
709
710 9
        $longitude = $longitudeOrigin + $easting * sqrt(1 - $e2 * sin($latitudeFirstParallel) ** 2) / ($a * cos($latitudeFirstParallel));
711
712 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
713
    }
714
715
    /**
716
     * Guam Projection
717
     * Simplified form of Oblique Azimuthal Equidistant projection method.
718
     */
719 9
    public function guamProjection(
720
        Geographic2D|Geographic3D $to,
721
        Angle $latitudeOfNaturalOrigin,
722
        Angle $longitudeOfNaturalOrigin,
723
        Length $falseEasting,
724
        Length $falseNorthing
725
    ): GeographicPoint {
726 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
727 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
728 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
729 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
730 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
731 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
732 9
        $e = $ellipsoid->getEccentricity();
733 9
        $e2 = $ellipsoid->getEccentricitySquared();
734 9
        $e4 = $e ** 4;
735 9
        $e6 = $e ** 6;
736
737 9
        $MO = $a * ((1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256) * $latitudeOrigin - (3 * $e2 / 8 + 3 * $e4 / 32 + 45 * $e6 / 1024) * sin(2 * $latitudeOrigin) + (15 * $e4 / 256 + 45 * $e6 / 1024) * sin(4 * $latitudeOrigin) - (35 * $e6 / 3072) * sin(6 * $latitudeOrigin));
738 9
        $e1 = (1 - sqrt(1 - $e2)) / (1 + sqrt(1 - $e2));
739 9
        $i = (1 - $e2 / 4 - 3 * $e4 / 64 - 5 * $e6 / 256);
740
741 9
        $latitude = $latitudeOrigin;
742
        do {
743 9
            $latitudeN = $latitude;
744 9
            $M = $MO + $northing - ($easting ** 2 * tan($latitude) * sqrt(1 - $e2 * sin($latitude) ** 2) / (2 * $a));
745 9
            $mu = $M / ($a * $i);
746 9
            $latitude = $mu + (3 * $e1 / 2 - 27 * $e1 ** 3 / 32) * sin(2 * $mu) + (21 * $e1 ** 2 / 16 - 55 * $e1 ** 4 / 32) * sin(4 * $mu) + (151 * $e1 ** 3 / 96) * sin(6 * $mu) + (1097 * $e1 ** 4 / 512) * sin(8 * $mu);
747 9
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
748
749 9
        $longitude = $longitudeOrigin + $easting * sqrt(1 - $e2 * sin($latitude) ** 2) / ($a * cos($latitude));
750
751 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
752
    }
753
754
    /**
755
     * Krovak.
756
     */
757 36
    public function krovak(
758
        Geographic2D|Geographic3D $to,
759
        Angle $latitudeOfProjectionCentre,
760
        Angle $longitudeOfOrigin,
761
        Angle $coLatitudeOfConeAxis,
762
        Angle $latitudeOfPseudoStandardParallel,
763
        Scale $scaleFactorOnPseudoStandardParallel,
764
        Length $falseEasting,
765
        Length $falseNorthing
766
    ): GeographicPoint {
767 36
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
768 36
        $longitudeOffset = $this->crs->getDatum()->getPrimeMeridian()->getGreenwichLongitude()->asRadians()->getValue() - $to->getDatum()->getPrimeMeridian()->getGreenwichLongitude()->asRadians()->getValue();
769 36
        $westing = $this->westing->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
770 36
        $southing = $this->southing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
771 36
        $latitudeC = $latitudeOfProjectionCentre->asRadians()->getValue();
772 36
        $longitudeO = $longitudeOfOrigin->asRadians()->getValue();
773 36
        $alphaC = $coLatitudeOfConeAxis->asRadians()->getValue();
774 36
        $latitudeP = $latitudeOfPseudoStandardParallel->asRadians()->getValue();
775 36
        $kP = $scaleFactorOnPseudoStandardParallel->asUnity()->getValue();
776 36
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
777 36
        $e = $ellipsoid->getEccentricity();
778 36
        $e2 = $ellipsoid->getEccentricitySquared();
779
780 36
        $A = $a * sqrt(1 - $e2) / (1 - $e2 * sin($latitudeC) ** 2);
781 36
        $B = sqrt(1 + $e2 * cos($latitudeC) ** 4 / (1 - $e2));
782 36
        $upsilonO = self::asin(sin($latitudeC) / $B);
783 36
        $tO = tan(M_PI / 4 + $upsilonO / 2) * ((1 + $e * sin($latitudeC)) / (1 - $e * sin($latitudeC))) ** ($e * $B / 2) / (tan(M_PI / 4 + $latitudeC / 2) ** $B);
784 36
        $n = sin($latitudeP);
785 36
        $rO = $kP * $A / tan($latitudeP);
786
787 36
        $r = hypot($southing, $westing) ?: 1;
788 36
        $theta = atan2($westing, $southing);
789 36
        $D = $theta / sin($latitudeP);
790 36
        $T = 2 * (atan(($rO / $r) ** (1 / $n) * tan(M_PI / 4 + $latitudeP / 2)) - M_PI / 4);
791 36
        $U = self::asin(cos($alphaC) * sin($T) - sin($alphaC) * cos($T) * cos($D));
792 36
        $V = self::asin(cos($T) * sin($D) / cos($U));
793
794 36
        $latitude = $U;
795
        do {
796 36
            $latitudeN = $latitude;
797 36
            $latitude = 2 * (atan($tO ** (-1 / $B) * tan($U / 2 + M_PI / 4) ** (1 / $B) * ((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2)) - M_PI / 4);
798 36
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
799
800 36
        $longitude = $longitudeO + $longitudeOffset - $V / $B;
801
802 36
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
803
    }
804
805
    /**
806
     * Krovak Modified
807
     * Incorporates a polynomial transformation which is defined to be exact and for practical purposes is considered
808
     * to be a map projection.
809
     */
810 18
    public function krovakModified(
811
        Geographic2D|Geographic3D $to,
812
        Angle $latitudeOfProjectionCentre,
813
        Angle $longitudeOfOrigin,
814
        Angle $coLatitudeOfConeAxis,
815
        Angle $latitudeOfPseudoStandardParallel,
816
        Scale $scaleFactorOnPseudoStandardParallel,
817
        Length $falseEasting,
818
        Length $falseNorthing,
819
        Length $ordinate1OfEvaluationPoint,
820
        Length $ordinate2OfEvaluationPoint,
821
        Coefficient $C1,
822
        Coefficient $C2,
823
        Coefficient $C3,
824
        Coefficient $C4,
825
        Coefficient $C5,
826
        Coefficient $C6,
827
        Coefficient $C7,
828
        Coefficient $C8,
829
        Coefficient $C9,
830
        Coefficient $C10
831
    ): GeographicPoint {
832 18
        $Xr = $this->getSouthing()->asMetres()->getValue() - $falseNorthing->asMetres()->getValue() - $ordinate1OfEvaluationPoint->asMetres()->getValue();
833 18
        $Yr = $this->getWesting()->asMetres()->getValue() - $falseEasting->asMetres()->getValue() - $ordinate2OfEvaluationPoint->asMetres()->getValue();
834 18
        $c1 = $C1->asUnity()->getValue();
835 18
        $c2 = $C2->asUnity()->getValue();
836 18
        $c3 = $C3->asUnity()->getValue();
837 18
        $c4 = $C4->asUnity()->getValue();
838 18
        $c5 = $C5->asUnity()->getValue();
839 18
        $c6 = $C6->asUnity()->getValue();
840 18
        $c7 = $C7->asUnity()->getValue();
841 18
        $c8 = $C8->asUnity()->getValue();
842 18
        $c9 = $C9->asUnity()->getValue();
843 18
        $c10 = $C10->asUnity()->getValue();
844
845 18
        $dX = $c1 + $c3 * $Xr - $c4 * $Yr - 2 * $c6 * $Xr * $Yr + $c5 * ($Xr ** 2 - $Yr ** 2) + $c7 * $Xr * ($Xr ** 2 - 3 * $Yr ** 2) - $c8 * $Yr * (3 * $Xr ** 2 - $Yr ** 2) + 4 * $c9 * $Xr * $Yr * ($Xr ** 2 - $Yr ** 2) + $c10 * ($Xr ** 4 + $Yr ** 4 - 6 * $Xr ** 2 * $Yr ** 2);
846 18
        $dY = $c2 + $c3 * $Yr + $c4 * $Xr + 2 * $c5 * $Xr * $Yr + $c6 * ($Xr ** 2 - $Yr ** 2) + $c8 * $Xr * ($Xr ** 2 - 3 * $Yr ** 2) + $c7 * $Yr * (3 * $Xr ** 2 - $Yr ** 2) - 4 * $c10 * $Xr * $Yr * ($Xr ** 2 - $Yr ** 2) + $c9 * ($Xr ** 4 + $Yr ** 4 - 6 * $Xr ** 2 * $Yr ** 2);
847
848 18
        $Xp = $this->getSouthing()->asMetres()->getValue() - $falseNorthing->asMetres()->getValue() + $dX;
849 18
        $Yp = $this->getWesting()->asMetres()->getValue() - $falseEasting->asMetres()->getValue() + $dY;
850
851 18
        $asKrovak = self::create($this->crs, new Metre(-$Yp), new Metre(-$Xp), new Metre($Yp), new Metre($Xp), $this->epoch);
852
853 18
        return $asKrovak->krovak($to, $latitudeOfProjectionCentre, $longitudeOfOrigin, $coLatitudeOfConeAxis, $latitudeOfPseudoStandardParallel, $scaleFactorOnPseudoStandardParallel, new Metre(0), new Metre(0));
854
    }
855
856
    /**
857
     * Lambert Azimuthal Equal Area
858
     * This is the ellipsoidal form of the projection.
859
     */
860 9
    public function lambertAzimuthalEqualArea(
861
        Geographic2D|Geographic3D $to,
862
        Angle $latitudeOfNaturalOrigin,
863
        Angle $longitudeOfNaturalOrigin,
864
        Length $falseEasting,
865
        Length $falseNorthing
866
    ): GeographicPoint {
867 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
868 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
869 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
870 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
871 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
872 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
873 9
        $e = $ellipsoid->getEccentricity();
874 9
        $e2 = $ellipsoid->getEccentricitySquared();
875 9
        $e4 = $e ** 4;
876 9
        $e6 = $e ** 6;
877
878 9
        $qO = (1 - $e2) * ((sin($latitudeOrigin) / (1 - $e2 * sin($latitudeOrigin) ** 2)) - ((1 / (2 * $e)) * log((1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin)))));
879 9
        $qP = (1 - $e2) * ((1 / (1 - $e2)) - ((1 / (2 * $e)) * log((1 - $e) / (1 + $e))));
880 9
        $betaO = self::asin($qO / $qP);
881 9
        $Rq = $a * sqrt($qP / 2);
882 9
        $D = $a * (cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2)) / ($Rq * cos($betaO));
883 9
        $rho = hypot($easting / $D, $D * $northing) ?: 1;
884 9
        $C = 2 * self::asin($rho / (2 * $Rq));
885 9
        $beta = self::asin(cos($C) * sin($betaO) + ($D * $northing * sin($C) * cos($betaO)) / $rho);
886
887 9
        $latitude = $beta + (($e2 / 3 + 31 * $e4 / 180 + 517 * $e6 / 5040) * sin(2 * $beta)) + ((23 * $e4 / 360 + 251 * $e6 / 3780) * sin(4 * $beta)) + ((761 * $e6 / 45360) * sin(6 * $beta));
888 9
        $longitude = $longitudeOrigin + atan2($easting * sin($C), $D * $rho * cos($betaO) * cos($C) - $D ** 2 * $northing * sin($betaO) * sin($C));
889
890 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
891
    }
892
893
    /**
894
     * Lambert Azimuthal Equal Area (Spherical)
895
     * This is the spherical form of the projection.  See coordinate operation method Lambert Azimuthal Equal Area
896
     * (code 9820) for ellipsoidal form.  Differences of several tens of metres result from comparison of the two
897
     * methods.
898
     */
899 9
    public function lambertAzimuthalEqualAreaSpherical(
900
        Geographic2D|Geographic3D $to,
901
        Angle $latitudeOfNaturalOrigin,
902
        Angle $longitudeOfNaturalOrigin,
903
        Length $falseEasting,
904
        Length $falseNorthing
905
    ): GeographicPoint {
906 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
907 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
908 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
909 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
910 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
911 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
912
913 9
        $rho = hypot($easting, $northing) ?: 1;
914 9
        $c = 2 * self::asin($rho / (2 * $a));
915
916 9
        $latitude = self::asin(cos($c) * sin($latitudeOrigin) + ($northing * sin($c) * cos($latitudeOrigin) / $rho));
917
918 9
        if ($latitudeOrigin === 90) {
0 ignored issues
show
introduced by
The condition $latitudeOrigin === 90 is always false.
Loading history...
919
            $longitude = $longitudeOrigin + atan($easting / -$northing);
920 9
        } elseif ($latitudeOrigin === -90) {
0 ignored issues
show
introduced by
The condition $latitudeOrigin === -90 is always false.
Loading history...
921
            $longitude = $longitudeOrigin + atan($easting / $northing);
922
        } else {
923 9
            $longitudeDenominator = ($rho * cos($latitudeOrigin) * cos($c) - $northing * sin($latitudeOrigin) * sin($c));
924 9
            $longitude = $longitudeOrigin + atan($easting * sin($c) / $longitudeDenominator);
925 9
            if ($longitudeDenominator < 0) {
926 9
                $longitude += M_PI;
927
            }
928
        }
929
930 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
931
    }
932
933
    /**
934
     * Lambert Conic Conformal (1SP).
935
     */
936 9
    public function lambertConicConformal1SP(
937
        Geographic2D|Geographic3D $to,
938
        Angle $latitudeOfNaturalOrigin,
939
        Angle $longitudeOfNaturalOrigin,
940
        Scale $scaleFactorAtNaturalOrigin,
941
        Length $falseEasting,
942
        Length $falseNorthing
943
    ): GeographicPoint {
944 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
945 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
946 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
947 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
948 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
949 9
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
950 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
951 9
        $e = $ellipsoid->getEccentricity();
952 9
        $e2 = $ellipsoid->getEccentricitySquared();
953
954 9
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
955 9
        $tO = tan(M_PI / 4 - $latitudeOrigin / 2) / ((1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin))) ** ($e / 2);
956 9
        $n = sin($latitudeOrigin);
957 9
        $F = $mO / ($n * $tO ** $n);
958 9
        $rO = $a * $F * $tO ** $n * $scaleFactorOrigin;
959 9
        $r = hypot($easting, $rO - $northing);
960 9
        if ($n >= 0) {
961 9
            $theta = atan2($easting, $rO - $northing);
962
        } else {
963
            $r = -$r;
964
            $theta = atan2(-$easting, -($rO - $northing));
965
        }
966
967 9
        $t = ($r / ($a * $scaleFactorOrigin * $F)) ** (1 / $n);
968
969 9
        $latitude = M_PI / (2 - 2 * atan($t));
970
        do {
971 9
            $latitudeN = $latitude;
972 9
            $latitude = M_PI / 2 - 2 * atan($t * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
973 9
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
974
975 9
        $longitude = $theta / $n + $longitudeOrigin;
976
977 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
978
    }
979
980
    /**
981
     * Lambert Conic Conformal (west orientated).
982
     */
983
    public function lambertConicConformalWestOrientated(
984
        Geographic2D|Geographic3D $to,
985
        Angle $latitudeOfNaturalOrigin,
986
        Angle $longitudeOfNaturalOrigin,
987
        Scale $scaleFactorAtNaturalOrigin,
988
        Length $falseEasting,
989
        Length $falseNorthing
990
    ): GeographicPoint {
991
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
992
        $westing = $falseEasting->asMetres()->getValue() - $this->westing->asMetres()->getValue();
993
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
994
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
995
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
996
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
997
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
998
        $e = $ellipsoid->getEccentricity();
999
        $e2 = $ellipsoid->getEccentricitySquared();
1000
1001
        $mO = cos($latitudeOrigin) / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
1002
        $tO = tan(M_PI / 4 - $latitudeOrigin / 2) / ((1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin))) ** ($e / 2);
1003
        $n = sin($latitudeOrigin);
1004
        $F = $mO / ($n * $tO ** $n);
1005
        $rO = $a * $F * $tO ** $n ** $scaleFactorOrigin;
1006
        $r = hypot($westing, $rO - $northing);
1007
        if ($n >= 0) {
1008
            $theta = atan2($westing, $rO - $northing);
1009
        } else {
1010
            $r = -$r;
1011
            $theta = atan2(-$westing, -($rO - $northing));
1012
        }
1013
1014
        $t = ($r / ($a * $scaleFactorOrigin * $F)) ** (1 / $n);
1015
1016
        $latitude = M_PI / (2 - 2 * atan($t));
1017
        do {
1018
            $latitudeN = $latitude;
1019
            $latitude = M_PI / 2 - 2 * atan($t * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1020
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1021
1022
        $longitude = $theta / $n + $longitudeOrigin;
1023
1024
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1025
    }
1026
1027
    /**
1028
     * Lambert Conic Conformal (1SP) Variant B.
1029
     */
1030
    public function lambertConicConformal1SPVariantB(
1031
        Geographic2D|Geographic3D $to,
1032
        Angle $latitudeOfNaturalOrigin,
1033
        Scale $scaleFactorAtNaturalOrigin,
1034
        Angle $latitudeOfFalseOrigin,
1035
        Angle $longitudeOfFalseOrigin,
1036
        Length $eastingAtFalseOrigin,
1037
        Length $northingAtFalseOrigin
1038
    ): GeographicPoint {
1039
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1040
        $easting = $this->easting->asMetres()->getValue() - $eastingAtFalseOrigin->asMetres()->getValue();
1041
        $northing = $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue();
1042
        $latitudeNaturalOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1043
        $latitudeFalseOrigin = $latitudeOfFalseOrigin->asRadians()->getValue();
1044
        $longitudeFalseOrigin = $longitudeOfFalseOrigin->asRadians()->getValue();
1045
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1046
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1047
        $e = $ellipsoid->getEccentricity();
1048
        $e2 = $ellipsoid->getEccentricitySquared();
1049
1050
        $mO = cos($latitudeNaturalOrigin) / sqrt(1 - $e2 * sin($latitudeNaturalOrigin) ** 2);
1051
        $tO = tan(M_PI / 4 - $latitudeNaturalOrigin / 2) / ((1 - $e * sin($latitudeNaturalOrigin)) / (1 + $e * sin($latitudeNaturalOrigin))) ** ($e / 2);
1052
        $tF = tan(M_PI / 4 - $latitudeFalseOrigin / 2) / ((1 - $e * sin($latitudeFalseOrigin)) / (1 + $e * sin($latitudeFalseOrigin))) ** ($e / 2);
1053
        $n = sin($latitudeNaturalOrigin);
1054
        $F = $mO / ($n * $tO ** $n);
1055
        $rF = $a * $F * $tF ** $n * $scaleFactorOrigin;
1056
        $r = hypot($easting, $rF - $northing);
1057
        if ($n >= 0) {
1058
            $theta = atan2($easting, $rF - $northing);
1059
        } else {
1060
            $r = -$r;
1061
            $theta = atan2(-$easting, -($rF - $northing));
1062
        }
1063
1064
        $t = ($r / ($a * $scaleFactorOrigin * $F)) ** (1 / $n);
1065
1066
        $latitude = M_PI / (2 - 2 * atan($t));
1067
        do {
1068
            $latitudeN = $latitude;
1069
            $latitude = M_PI / 2 - 2 * atan($t * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1070
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1071
1072
        $longitude = $theta / $n + $longitudeFalseOrigin;
1073
1074
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1075
    }
1076
1077
    /**
1078
     * Lambert Conic Conformal (2SP).
1079
     */
1080 10
    public function lambertConicConformal2SP(
1081
        Geographic2D|Geographic3D $to,
1082
        Angle $latitudeOfFalseOrigin,
1083
        Angle $longitudeOfFalseOrigin,
1084
        Angle $latitudeOf1stStandardParallel,
1085
        Angle $latitudeOf2ndStandardParallel,
1086
        Length $eastingAtFalseOrigin,
1087
        Length $northingAtFalseOrigin
1088
    ): GeographicPoint {
1089 10
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1090 10
        $easting = $this->easting->asMetres()->getValue() - $eastingAtFalseOrigin->asMetres()->getValue();
1091 10
        $northing = $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue();
1092 10
        $lambdaOrigin = $longitudeOfFalseOrigin->asRadians()->getValue();
1093 10
        $phiF = $latitudeOfFalseOrigin->asRadians()->getValue();
1094 10
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
1095 10
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
1096 10
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1097 10
        $e = $ellipsoid->getEccentricity();
1098 10
        $e2 = $ellipsoid->getEccentricitySquared();
1099
1100 10
        $m1 = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
1101 10
        $m2 = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
1102 10
        $t1 = tan(M_PI / 4 - $phi1 / 2) / ((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))) ** ($e / 2);
1103 10
        $t2 = tan(M_PI / 4 - $phi2 / 2) / ((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))) ** ($e / 2);
1104 10
        $tF = tan(M_PI / 4 - $phiF / 2) / ((1 - $e * sin($phiF)) / (1 + $e * sin($phiF))) ** ($e / 2);
1105 10
        $n = (log($m1) - log($m2)) / (log($t1) - log($t2));
1106 10
        $F = $m1 / ($n * $t1 ** $n);
1107 10
        $rF = $a * $F * $tF ** $n;
1108 10
        $r = hypot($easting, $rF - $northing) * static::sign($n);
1109 10
        $t = ($r / ($a * $F)) ** (1 / $n);
1110 10
        if ($n >= 0) {
1111 10
            $theta = atan2($easting, $rF - $northing);
1112
        } else {
1113
            $theta = atan2(-$easting, -($rF - $northing));
1114
        }
1115
1116 10
        $latitude = M_PI / 2 - 2 * atan($t);
1117
        do {
1118 10
            $latitudeN = $latitude;
1119 10
            $latitude = M_PI / 2 - 2 * atan($t * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1120 10
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1121
1122 10
        $longitude = $theta / $n + $lambdaOrigin;
1123
1124 10
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1125
    }
1126
1127
    /**
1128
     * Lambert Conic Conformal (2SP Michigan).
1129
     */
1130 9
    public function lambertConicConformal2SPMichigan(
1131
        Geographic2D|Geographic3D $to,
1132
        Angle $latitudeOfFalseOrigin,
1133
        Angle $longitudeOfFalseOrigin,
1134
        Angle $latitudeOf1stStandardParallel,
1135
        Angle $latitudeOf2ndStandardParallel,
1136
        Length $eastingAtFalseOrigin,
1137
        Length $northingAtFalseOrigin,
1138
        Scale $ellipsoidScalingFactor
1139
    ): GeographicPoint {
1140 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1141 9
        $easting = $this->easting->asMetres()->getValue() - $eastingAtFalseOrigin->asMetres()->getValue();
1142 9
        $northing = $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue();
1143 9
        $lambdaOrigin = $longitudeOfFalseOrigin->asRadians()->getValue();
1144 9
        $phiF = $latitudeOfFalseOrigin->asRadians()->getValue();
1145 9
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
1146 9
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
1147 9
        $K = $ellipsoidScalingFactor->asUnity()->getValue();
1148 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1149 9
        $e = $ellipsoid->getEccentricity();
1150 9
        $e2 = $ellipsoid->getEccentricitySquared();
1151
1152 9
        $m1 = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
1153 9
        $m2 = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
1154 9
        $t1 = tan(M_PI / 4 - $phi1 / 2) / ((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))) ** ($e / 2);
1155 9
        $t2 = tan(M_PI / 4 - $phi2 / 2) / ((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))) ** ($e / 2);
1156 9
        $tF = tan(M_PI / 4 - $phiF / 2) / ((1 - $e * sin($phiF)) / (1 + $e * sin($phiF))) ** ($e / 2);
1157 9
        $n = (log($m1) - log($m2)) / (log($t1) - log($t2));
1158 9
        $F = $m1 / ($n * $t1 ** $n);
1159 9
        $rF = $a * $K * $F * $tF ** $n;
1160 9
        $r = sqrt($easting ** 2 + ($rF - $northing) ** 2) * static::sign($n);
1161 9
        $t = ($r / ($a * $K * $F)) ** (1 / $n);
1162 9
        if ($n >= 0) {
1163 9
            $theta = atan2($easting, $rF - $northing);
1164
        } else {
1165
            $theta = atan2(-$easting, -($rF - $northing));
1166
        }
1167
1168 9
        $latitude = M_PI / 2 - 2 * atan($t);
1169
        do {
1170 9
            $latitudeN = $latitude;
1171 9
            $latitude = M_PI / 2 - 2 * atan($t * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1172 9
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1173
1174 9
        $longitude = $theta / $n + $lambdaOrigin;
1175
1176 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1177
    }
1178
1179
    /**
1180
     * Lambert Conic Conformal (2SP Belgium)
1181
     * In 2000 this modification was replaced through use of the regular Lambert Conic Conformal (2SP) method [9802]
1182
     * with appropriately modified parameter values.
1183
     */
1184 9
    public function lambertConicConformal2SPBelgium(
1185
        Geographic2D|Geographic3D $to,
1186
        Angle $latitudeOfFalseOrigin,
1187
        Angle $longitudeOfFalseOrigin,
1188
        Angle $latitudeOf1stStandardParallel,
1189
        Angle $latitudeOf2ndStandardParallel,
1190
        Length $eastingAtFalseOrigin,
1191
        Length $northingAtFalseOrigin
1192
    ): GeographicPoint {
1193 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1194 9
        $easting = $this->easting->asMetres()->getValue() - $eastingAtFalseOrigin->asMetres()->getValue();
1195 9
        $northing = $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue();
1196 9
        $lambdaOrigin = $longitudeOfFalseOrigin->asRadians()->getValue();
1197 9
        $phiF = $latitudeOfFalseOrigin->asRadians()->getValue();
1198 9
        $phi1 = $latitudeOf1stStandardParallel->asRadians()->getValue();
1199 9
        $phi2 = $latitudeOf2ndStandardParallel->asRadians()->getValue();
1200 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1201 9
        $e = $ellipsoid->getEccentricity();
1202 9
        $e2 = $ellipsoid->getEccentricitySquared();
1203
1204 9
        $m1 = cos($phi1) / sqrt(1 - $e2 * sin($phi1) ** 2);
1205 9
        $m2 = cos($phi2) / sqrt(1 - $e2 * sin($phi2) ** 2);
1206 9
        $t1 = tan(M_PI / 4 - $phi1 / 2) / ((1 - $e * sin($phi1)) / (1 + $e * sin($phi1))) ** ($e / 2);
1207 9
        $t2 = tan(M_PI / 4 - $phi2 / 2) / ((1 - $e * sin($phi2)) / (1 + $e * sin($phi2))) ** ($e / 2);
1208 9
        $tF = tan(M_PI / 4 - $phiF / 2) / ((1 - $e * sin($phiF)) / (1 + $e * sin($phiF))) ** ($e / 2);
1209 9
        $n = (log($m1) - log($m2)) / (log($t1) - log($t2));
1210 9
        $F = $m1 / ($n * $t1 ** $n);
1211 9
        $rF = $a * $F * $tF ** $n;
1212 9
        if (is_nan($rF)) {
1213 9
            $rF = 0;
1214
        }
1215 9
        $r = hypot($easting, $rF - $northing) * static::sign($n);
1216 9
        $t = ($r / ($a * $F)) ** (1 / $n);
1217 9
        if ($n >= 0) {
1218 9
            $theta = atan2($easting, $rF - $northing);
1219
        } else {
1220
            $theta = atan2(-$easting, -($rF - $northing));
1221
        }
1222
1223 9
        $latitude = M_PI / 2 - 2 * atan($t);
1224
        do {
1225 9
            $latitudeN = $latitude;
1226 9
            $latitude = M_PI / 2 - 2 * atan($t * ((1 - $e * sin($latitude)) / (1 + $e * sin($latitude))) ** ($e / 2));
1227 9
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1228
1229 9
        $longitude = ($theta + (new ArcSecond(29.2985))->asRadians()->getValue()) / $n + $lambdaOrigin;
1230
1231 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1232
    }
1233
1234
    /**
1235
     * Lambert Conic Near-Conformal
1236
     * The Lambert Near-Conformal projection is derived from the Lambert Conformal Conic projection by truncating the
1237
     * series expansion of the projection formulae.
1238
     */
1239 9
    public function lambertConicNearConformal(
1240
        Geographic2D|Geographic3D $to,
1241
        Angle $latitudeOfNaturalOrigin,
1242
        Angle $longitudeOfNaturalOrigin,
1243
        Scale $scaleFactorAtNaturalOrigin,
1244
        Length $falseEasting,
1245
        Length $falseNorthing
1246
    ): GeographicPoint {
1247 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1248 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1249 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1250 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1251 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1252 9
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1253 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1254 9
        $e2 = $ellipsoid->getEccentricitySquared();
1255 9
        $f = $ellipsoid->getFlattening();
1256
1257 9
        $n = $f / (2 - $f);
1258 9
        $rhoO = $a * (1 - $e2) / (1 - $e2 * sin($latitudeOrigin) ** 2) ** (3 / 2);
1259 9
        $nuO = $a / sqrt(1 - $e2 * (sin($latitudeOrigin) ** 2));
1260 9
        $A = 1 / (6 * $rhoO * $nuO);
1261 9
        $APrime = $a * (1 - $n + 5 * ($n ** 2 - $n ** 3) / 4 + 81 * ($n ** 4 - $n ** 5) / 64);
1262 9
        $BPrime = 3 * $a * ($n - $n ** 2 + 7 * ($n ** 3 - $n ** 4) / 8 + 55 * $n ** 5 / 64) / 2;
1263 9
        $CPrime = 15 * $a * ($n ** 2 - $n ** 3 + 3 * ($n ** 4 - $n ** 5) / 4) / 16;
1264 9
        $DPrime = 35 * $a * ($n ** 3 - $n ** 4 + 11 * $n ** 5 / 16) / 48;
1265 9
        $EPrime = 315 * $a * ($n ** 4 - $n ** 5) / 512;
1266 9
        $rO = $scaleFactorOrigin * $nuO / tan($latitudeOrigin);
1267 9
        $sO = $APrime * $latitudeOrigin - $BPrime * sin(2 * $latitudeOrigin) + $CPrime * sin(4 * $latitudeOrigin) - $DPrime * sin(6 * $latitudeOrigin) + $EPrime * sin(8 * $latitudeOrigin);
1268
1269 9
        $theta = atan2($easting, $rO - $northing);
1270 9
        $r = hypot($easting, $rO - $northing) * static::sign($latitudeOrigin);
1271 9
        $M = $rO - $r;
1272
1273 9
        $m = $M;
1274
        do {
1275 9
            $mN = $m;
1276 9
            $m = $m - ($M - $scaleFactorOrigin * $m - $scaleFactorOrigin * $A * $m ** 3) / (-$scaleFactorOrigin - 3 * $scaleFactorOrigin * $A * $m ** 2);
1277 9
        } while (abs($m - $mN) >= static::ITERATION_CONVERGENCE_FORMULA);
1278
1279 9
        $latitude = $latitudeOrigin + $m / $A;
1280
        do {
1281 9
            $latitudeN = $latitude;
1282 9
            $latitude = $latitude + ($m + $sO - ($APrime * $latitude - $BPrime * sin(2 * $latitude) + $CPrime * sin(4 * $latitude) - $DPrime * sin(6 * $latitude) + $EPrime * sin(8 * $latitude))) / $APrime;
1283 9
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1284
1285 9
        $longitude = $longitudeOrigin + $theta / sin($latitudeOrigin);
1286
1287 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1288
    }
1289
1290
    /**
1291
     * Lambert Cylindrical Equal Area
1292
     * This is the ellipsoidal form of the projection.
1293
     */
1294 9
    public function lambertCylindricalEqualArea(
1295
        Geographic2D|Geographic3D $to,
1296
        Angle $latitudeOf1stStandardParallel,
1297
        Angle $longitudeOfNaturalOrigin,
1298
        Length $falseEasting,
1299
        Length $falseNorthing
1300
    ): GeographicPoint {
1301 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1302 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1303 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1304 9
        $latitudeFirstParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1305 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1306 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1307 9
        $e = $ellipsoid->getEccentricity();
1308 9
        $e2 = $ellipsoid->getEccentricitySquared();
1309 9
        $e4 = $e ** 4;
1310 9
        $e6 = $e ** 6;
1311
1312 9
        $k = cos($latitudeFirstParallel) / sqrt(1 - $e2 * sin($latitudeFirstParallel) ** 2);
1313 9
        $qP = (1 - $e2) * ((sin(M_PI_2) / (1 - $e2 * sin(M_PI_2) ** 2)) - (1 / (2 * $e)) * log((1 - $e * sin(M_PI_2)) / (1 + $e * sin(M_PI_2))));
1314 9
        $beta = self::asin(2 * $northing * $k / ($a * $qP));
1315
1316 9
        $latitude = $beta + (($e2 / 3 + 31 * $e4 / 180 + 517 * $e6 / 5040) * sin(2 * $beta)) + ((23 * $e4 / 360 + 251 * $e6 / 3780) * sin(4 * $beta)) + ((761 * $e6 / 45360) * sin(6 * $beta));
1317 9
        $longitude = $longitudeOrigin + $easting / ($a * $k);
1318
1319 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1320
    }
1321
1322
    /**
1323
     * Modified Azimuthal Equidistant
1324
     * Modified form of Oblique Azimuthal Equidistant projection method developed for Polynesian islands. For the
1325
     * distances over which these projections are used (under 800km) this modification introduces no significant error.
1326
     */
1327 9
    public function modifiedAzimuthalEquidistant(
1328
        Geographic2D|Geographic3D $to,
1329
        Angle $latitudeOfNaturalOrigin,
1330
        Angle $longitudeOfNaturalOrigin,
1331
        Length $falseEasting,
1332
        Length $falseNorthing
1333
    ): GeographicPoint {
1334 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1335 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1336 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1337 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1338 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1339 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1340 9
        $e2 = $ellipsoid->getEccentricitySquared();
1341
1342 9
        $nuO = $a / sqrt(1 - $e2 * sin($latitudeOrigin) ** 2);
1343 9
        $c = hypot($easting, $northing);
1344 9
        $alpha = atan2($easting, $northing);
1345 9
        $A = -$e2 * cos($latitudeOrigin) ** 2 * cos($alpha) ** 2 / (1 - $e2);
1346 9
        $B = 3 * $e2 * (1 - $A) * sin($latitudeOrigin) * cos($latitudeOrigin) * cos($alpha) / (1 - $e2);
1347 9
        $D = $c / $nuO;
1348 9
        $J = $D - ($A * (1 + $A) * $D ** 3 / 6) - ($B * (1 + 3 * $A) * $D ** 4 / 24);
1349 9
        $K = 1 - ($A * $J ** 2 / 2) - ($B * $J ** 3 / 6);
1350 9
        $psi = self::asin(sin($latitudeOrigin) * cos($J) + cos($latitudeOrigin) * sin($J) * cos($alpha));
1351
1352 9
        $latitude = atan((1 - $e2 * $K * sin($latitudeOrigin) / sin($psi)) * tan($psi) / (1 - $e2));
1353 9
        $longitude = $longitudeOrigin + self::asin(sin($alpha) * sin($J) / cos($psi));
1354
1355 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1356
    }
1357
1358
    /**
1359
     * Oblique Stereographic
1360
     * This is not the same as the projection method of the same name in USGS Professional Paper no. 1395, "Map
1361
     * Projections - A Working Manual" by John P. Snyder.
1362
     */
1363 27
    public function obliqueStereographic(
1364
        Geographic2D|Geographic3D $to,
1365
        Angle $latitudeOfNaturalOrigin,
1366
        Angle $longitudeOfNaturalOrigin,
1367
        Scale $scaleFactorAtNaturalOrigin,
1368
        Length $falseEasting,
1369
        Length $falseNorthing
1370
    ): GeographicPoint {
1371 27
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1372 27
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1373 27
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1374 27
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1375 27
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1376 27
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1377 27
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1378 27
        $e = $ellipsoid->getEccentricity();
1379 27
        $e2 = $ellipsoid->getEccentricitySquared();
1380
1381 27
        $rhoOrigin = $a * (1 - $e2) / (1 - $e2 * sin($latitudeOrigin) ** 2) ** (3 / 2);
1382 27
        $nuOrigin = $a / sqrt(1 - $e2 * (sin($latitudeOrigin) ** 2));
1383 27
        $R = sqrt($rhoOrigin * $nuOrigin);
1384
1385 27
        $n = sqrt(1 + ($e2 * cos($latitudeOrigin) ** 4 / (1 - $e2)));
1386 27
        $S1 = (1 + sin($latitudeOrigin)) / (1 - sin($latitudeOrigin));
1387 27
        $S2 = (1 - $e * sin($latitudeOrigin)) / (1 + $e * sin($latitudeOrigin));
1388 27
        $w1 = ($S1 * ($S2 ** $e)) ** $n;
1389 27
        $c = (($n + sin($latitudeOrigin)) * (1 - ($w1 - 1) / ($w1 + 1))) / (($n - sin($latitudeOrigin)) * (1 + ($w1 - 1) / ($w1 + 1)));
1390 27
        $w2 = $c * $w1;
1391 27
        $chiOrigin = self::asin(($w2 - 1) / ($w2 + 1));
1392
1393 27
        $g = 2 * $R * $scaleFactorOrigin * tan(M_PI / 4 - $chiOrigin / 2);
1394 27
        $h = 4 * $R * $scaleFactorOrigin * tan($chiOrigin) + $g;
1395 27
        $i = atan2($easting, ($h + $northing));
1396 27
        $j = atan2($easting, ($g - $northing)) - $i;
1397 27
        $chi = $chiOrigin + 2 * atan(($northing - $easting * tan($j / 2)) / (2 * $R * $scaleFactorOrigin));
1398 27
        $lambda = $j + 2 * $i + $longitudeOrigin;
1399
1400 27
        $longitude = ($lambda - $longitudeOrigin) / $n + $longitudeOrigin;
1401
1402 27
        $psi = 0.5 * log((1 + sin($chi)) / ($c * (1 - sin($chi)))) / $n;
1403
1404 27
        $latitude = 2 * atan(M_E ** $psi) - M_PI / 2;
1405
        do {
1406 27
            $latitudeN = $latitude;
1407 27
            $psiN = log((tan($latitudeN / 2 + M_PI / 4)) * ((1 - $e * sin($latitudeN)) / (1 + $e * sin($latitudeN))) ** ($e / 2));
1408 27
            $latitude = $latitudeN - ($psiN - $psi) * cos($latitudeN) * (1 - $e2 * sin($latitudeN) ** 2) / (1 - $e2);
1409 27
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1410
1411 27
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1412
    }
1413
1414
    /**
1415
     * Polar Stereographic (variant A)
1416
     * Latitude of natural origin must be either 90 degrees or -90 degrees (or equivalent in alternative angle unit).
1417
     */
1418 9
    public function polarStereographicVariantA(
1419
        Geographic2D|Geographic3D $to,
1420
        Angle $latitudeOfNaturalOrigin,
1421
        Angle $longitudeOfNaturalOrigin,
1422
        Scale $scaleFactorAtNaturalOrigin,
1423
        Length $falseEasting,
1424
        Length $falseNorthing
1425
    ): GeographicPoint {
1426 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1427 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1428 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1429 9
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1430 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1431 9
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1432 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1433 9
        $e = $ellipsoid->getEccentricity();
1434 9
        $e2 = $ellipsoid->getEccentricitySquared();
1435 9
        $e4 = $e ** 4;
1436 9
        $e6 = $e ** 6;
1437 9
        $e8 = $e ** 8;
1438
1439 9
        $rho = hypot($easting, $northing);
1440 9
        $t = $rho * sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e)) / (2 * $a * $scaleFactorOrigin);
1441
1442 9
        if ($latitudeOrigin < 0) {
1443
            $chi = 2 * atan($t) - M_PI / 2;
1444
        } else {
1445 9
            $chi = M_PI / 2 - 2 * atan($t);
1446
        }
1447
1448 9
        $latitude = $chi + ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) * sin(2 * $chi) + (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) * sin(4 * $chi) + (7 * $e6 / 120 + 81 * $e8 / 1120) * sin(6 * $chi) + (4279 * $e8 / 161280) * sin(8 * $chi);
1449
1450 9
        if ($easting === 0.0) {
0 ignored issues
show
introduced by
The condition $easting === 0.0 is always false.
Loading history...
1451
            $longitude = $longitudeOrigin;
1452 9
        } elseif ($latitudeOrigin < 0) {
1453
            $longitude = $longitudeOrigin + atan2($easting, $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue());
1454
        } else {
1455 9
            $longitude = $longitudeOrigin + atan2($easting, $falseNorthing->asMetres()->getValue() - $this->northing->asMetres()->getValue());
1456
        }
1457
1458 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1459
    }
1460
1461
    /**
1462
     * Polar Stereographic (variant B).
1463
     */
1464 9
    public function polarStereographicVariantB(
1465
        Geographic2D|Geographic3D $to,
1466
        Angle $latitudeOfStandardParallel,
1467
        Angle $longitudeOfOrigin,
1468
        Length $falseEasting,
1469
        Length $falseNorthing
1470
    ): GeographicPoint {
1471 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1472 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1473 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1474 9
        $standardParallel = $latitudeOfStandardParallel->asRadians()->getValue();
1475 9
        $longitudeOrigin = $longitudeOfOrigin->asRadians()->getValue();
1476 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1477 9
        $e = $ellipsoid->getEccentricity();
1478 9
        $e2 = $ellipsoid->getEccentricitySquared();
1479 9
        $e4 = $e ** 4;
1480 9
        $e6 = $e ** 6;
1481 9
        $e8 = $e ** 8;
1482
1483 9
        $rho = hypot($easting, $northing);
1484 9
        if ($standardParallel < 0) {
1485 9
            $tF = tan(M_PI / 4 + $standardParallel / 2) / (((1 + $e * sin($standardParallel)) / (1 - $e * sin($standardParallel))) ** ($e / 2));
1486
        } else {
1487
            $tF = tan(M_PI / 4 - $standardParallel / 2) * (((1 + $e * sin($standardParallel)) / (1 - $e * sin($standardParallel))) ** ($e / 2));
1488
        }
1489 9
        $mF = cos($standardParallel) / sqrt(1 - $e2 * sin($standardParallel) ** 2);
1490 9
        $kO = $mF * sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e)) / (2 * $tF);
1491 9
        $t = $rho * sqrt((1 + $e) ** (1 + $e) * (1 - $e) ** (1 - $e)) / (2 * $a * $kO);
1492
1493 9
        if ($standardParallel < 0) {
1494 9
            $chi = 2 * atan($t) - M_PI / 2;
1495
        } else {
1496
            $chi = M_PI / 2 - 2 * atan($t);
1497
        }
1498
1499 9
        $latitude = $chi + ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) * sin(2 * $chi) + (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) * sin(4 * $chi) + (7 * $e6 / 120 + 81 * $e8 / 1120) * sin(6 * $chi) + (4279 * $e8 / 161280) * sin(8 * $chi);
1500
1501 9
        if ($easting === 0.0) {
0 ignored issues
show
introduced by
The condition $easting === 0.0 is always false.
Loading history...
1502
            $longitude = $longitudeOrigin;
1503 9
        } elseif ($standardParallel < 0) {
1504 9
            $longitude = $longitudeOrigin + atan2($easting, $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue());
1505
        } else {
1506
            $longitude = $longitudeOrigin + atan2($easting, $falseNorthing->asMetres()->getValue() - $this->northing->asMetres()->getValue());
1507
        }
1508
1509 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1510
    }
1511
1512
    /**
1513
     * Polar Stereographic (variant C).
1514
     */
1515 9
    public function polarStereographicVariantC(
1516
        Geographic2D|Geographic3D $to,
1517
        Angle $latitudeOfStandardParallel,
1518
        Angle $longitudeOfOrigin,
1519
        Length $eastingAtFalseOrigin,
1520
        Length $northingAtFalseOrigin
1521
    ): GeographicPoint {
1522 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1523 9
        $easting = $this->easting->asMetres()->getValue() - $eastingAtFalseOrigin->asMetres()->getValue();
1524 9
        $northing = $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue();
1525 9
        $standardParallel = $latitudeOfStandardParallel->asRadians()->getValue();
1526 9
        $longitudeOrigin = $longitudeOfOrigin->asRadians()->getValue();
1527 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1528 9
        $e = $ellipsoid->getEccentricity();
1529 9
        $e2 = $ellipsoid->getEccentricitySquared();
1530 9
        $e4 = $e ** 4;
1531 9
        $e6 = $e ** 6;
1532 9
        $e8 = $e ** 8;
1533
1534 9
        if ($standardParallel < 0) {
1535 9
            $tF = tan(M_PI / 4 + $standardParallel / 2) / (((1 + $e * sin($standardParallel)) / (1 - $e * sin($standardParallel))) ** ($e / 2));
1536
        } else {
1537
            $tF = tan(M_PI / 4 - $standardParallel / 2) * (((1 + $e * sin($standardParallel)) / (1 - $e * sin($standardParallel))) ** ($e / 2));
1538
        }
1539 9
        $mF = cos($standardParallel) / sqrt(1 - $e2 * sin($standardParallel) ** 2);
1540 9
        $rhoF = $a * $mF;
1541 9
        if ($standardParallel < 0) {
1542 9
            $rho = hypot($easting, $northing + $rhoF);
1543 9
            $t = $rho * $tF / $rhoF;
1544 9
            $chi = 2 * atan($t) - M_PI / 2;
1545
        } else {
1546
            $rho = hypot($easting, $northing - $rhoF);
1547
            $t = $rho * $tF / $rhoF;
1548
            $chi = M_PI / 2 - 2 * atan($t);
1549
        }
1550
1551 9
        $latitude = $chi + ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) * sin(2 * $chi) + (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) * sin(4 * $chi) + (7 * $e6 / 120 + 81 * $e8 / 1120) * sin(6 * $chi) + (4279 * $e8 / 161280) * sin(8 * $chi);
1552
1553 9
        if ($easting === 0.0) {
0 ignored issues
show
introduced by
The condition $easting === 0.0 is always false.
Loading history...
1554
            $longitude = $longitudeOrigin;
1555 9
        } elseif ($standardParallel < 0) {
1556 9
            $longitude = $longitudeOrigin + atan2($easting, $this->northing->asMetres()->getValue() - $northingAtFalseOrigin->asMetres()->getValue() + $rhoF);
1557
        } else {
1558
            $longitude = $longitudeOrigin + atan2($easting, $northingAtFalseOrigin->asMetres()->getValue() - $this->northing->asMetres()->getValue() + $rhoF);
1559
        }
1560
1561 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1562
    }
1563
1564
    /**
1565
     * Popular Visualisation Pseudo Mercator
1566
     * Applies spherical formulas to the ellipsoid. As such does not have the properties of a true Mercator projection.
1567
     */
1568 18
    public function popularVisualisationPseudoMercator(
1569
        Geographic2D|Geographic3D $to,
1570
        Angle $latitudeOfNaturalOrigin,
1571
        Angle $longitudeOfNaturalOrigin,
1572
        Length $falseEasting,
1573
        Length $falseNorthing
1574
    ): GeographicPoint {
1575 18
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1576 18
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1577 18
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1578 18
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
0 ignored issues
show
Unused Code introduced by
The assignment to $latitudeOrigin is dead and can be removed.
Loading history...
1579 18
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1580 18
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1581
1582 18
        $D = -$northing / $a;
1583 18
        $latitude = M_PI / 2 - 2 * atan(M_E ** $D);
1584 18
        $longitude = $easting / $a + $longitudeOrigin;
1585
1586 18
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1587
    }
1588
1589
    /**
1590
     * Similarity transformation
1591
     * Defined for two-dimensional coordinate systems.
1592
     */
1593 9
    public function similarityTransformation(
1594
        Projected $to,
1595
        Length $ordinate1OfEvaluationPointInTargetCRS,
1596
        Length $ordinate2OfEvaluationPointInTargetCRS,
1597
        Scale $scaleFactorForSourceCRSAxes,
1598
        Angle $rotationAngleOfSourceCRSAxes,
1599
        bool $inReverse
1600
    ): self {
1601 9
        $xs = $this->easting->asMetres()->getValue();
1602 9
        $ys = $this->northing->asMetres()->getValue();
1603 9
        $xo = $ordinate1OfEvaluationPointInTargetCRS->asMetres()->getValue();
1604 9
        $yo = $ordinate2OfEvaluationPointInTargetCRS->asMetres()->getValue();
1605 9
        $M = $scaleFactorForSourceCRSAxes->asUnity()->getValue();
1606 9
        $theta = $rotationAngleOfSourceCRSAxes->asRadians()->getValue();
1607
1608 9
        if ($inReverse) {
1609
            $easting = (($xs - $xo) * cos($theta) - ($ys - $yo) * sin($theta)) / $M;
1610
            $northing = (($xs - $xo) * sin($theta) + ($ys - $yo) * cos($theta)) / $M;
1611
        } else {
1612 9
            $easting = $xo + $xs * $M * cos($theta) + $ys * $M * sin($theta);
1613 9
            $northing = $yo - $xs * $M * sin($theta) + $ys * $M * cos($theta);
1614
        }
1615
1616 9
        return self::create($to, new Metre($easting), new Metre($northing), new Metre(-$easting), new Metre(-$northing), $this->epoch);
1617
    }
1618
1619
    /**
1620
     * Mercator (variant A)
1621
     * Note that in these formulas the parameter latitude of natural origin (latO) is not used. However for this
1622
     * Mercator (variant A) method the EPSG dataset includes this parameter, which must have a value of zero, for
1623
     * completeness in CRS labelling.
1624
     */
1625 18
    public function mercatorVariantA(
1626
        Geographic2D|Geographic3D $to,
1627
        Angle $latitudeOfNaturalOrigin,
1628
        Angle $longitudeOfNaturalOrigin,
1629
        Scale $scaleFactorAtNaturalOrigin,
1630
        Length $falseEasting,
1631
        Length $falseNorthing
1632
    ): GeographicPoint {
1633 18
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1634 18
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1635 18
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
0 ignored issues
show
Unused Code introduced by
The assignment to $northing is dead and can be removed.
Loading history...
1636 18
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
0 ignored issues
show
Unused Code introduced by
The assignment to $latitudeOrigin is dead and can be removed.
Loading history...
1637 18
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1638 18
        $scaleFactorOrigin = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1639 18
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1640 18
        $e = $ellipsoid->getEccentricity();
1641 18
        $e2 = $ellipsoid->getEccentricitySquared();
1642 18
        $e4 = $e ** 4;
1643 18
        $e6 = $e ** 6;
1644 18
        $e8 = $e ** 8;
1645
1646 18
        $t = M_E ** (($falseNorthing->asMetres()->getValue() - $this->northing->asMetres()->getValue()) / ($a * $scaleFactorOrigin));
1647 18
        $chi = M_PI / 2 - 2 * atan($t);
1648
1649 18
        $latitude = $chi + ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) * sin(2 * $chi) + (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) * sin(4 * $chi) + (7 * $e6 / 120 + 81 * $e8 / 1120) * sin(6 * $chi) + (4279 * $e8 / 161280) * sin(8 * $chi);
1650 18
        $longitude = $easting / ($a * $scaleFactorOrigin) + $longitudeOrigin;
1651
1652 18
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1653
    }
1654
1655
    /**
1656
     * Mercator (variant B)
1657
     * Used for most nautical charts.
1658
     */
1659 9
    public function mercatorVariantB(
1660
        Geographic2D|Geographic3D $to,
1661
        Angle $latitudeOf1stStandardParallel,
1662
        Angle $longitudeOfNaturalOrigin,
1663
        Length $falseEasting,
1664
        Length $falseNorthing
1665
    ): GeographicPoint {
1666 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1667 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1668 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
0 ignored issues
show
Unused Code introduced by
The assignment to $northing is dead and can be removed.
Loading history...
1669 9
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1670 9
        $firstStandardParallel = $latitudeOf1stStandardParallel->asRadians()->getValue();
1671 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1672 9
        $e = $ellipsoid->getEccentricity();
1673 9
        $e2 = $ellipsoid->getEccentricitySquared();
1674 9
        $e4 = $e ** 4;
1675 9
        $e6 = $e ** 6;
1676 9
        $e8 = $e ** 8;
1677
1678 9
        $scaleFactorOrigin = cos($firstStandardParallel) / sqrt(1 - $e2 * sin($firstStandardParallel) ** 2);
1679
1680 9
        $t = M_E ** (($falseNorthing->asMetres()->getValue() - $this->northing->asMetres()->getValue()) / ($a * $scaleFactorOrigin));
1681 9
        $chi = M_PI / 2 - 2 * atan($t);
1682
1683 9
        $latitude = $chi + ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) * sin(2 * $chi) + (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) * sin(4 * $chi) + (7 * $e6 / 120 + 81 * $e8 / 1120) * sin(6 * $chi) + (4279 * $e8 / 161280) * sin(8 * $chi);
1684 9
        $longitude = $easting / ($a * $scaleFactorOrigin) + $longitudeOrigin;
1685
1686 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1687
    }
1688
1689
    /**
1690
     * Hotine Oblique Mercator (variant A).
1691
     */
1692 9
    public function obliqueMercatorHotineVariantA(
1693
        Geographic2D|Geographic3D $to,
1694
        Angle $latitudeOfProjectionCentre,
1695
        Angle $longitudeOfProjectionCentre,
1696
        Angle $azimuthOfInitialLine,
1697
        Angle $angleFromRectifiedToSkewGrid,
1698
        Scale $scaleFactorOnInitialLine,
1699
        Length $falseEasting,
1700
        Length $falseNorthing
1701
    ): GeographicPoint {
1702 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1703 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1704 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1705 9
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1706 9
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1707 9
        $alphaC = $azimuthOfInitialLine->asRadians()->getValue();
1708 9
        $kC = $scaleFactorOnInitialLine->asUnity()->getValue();
1709 9
        $gammaC = $angleFromRectifiedToSkewGrid->asRadians()->getValue();
1710 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1711 9
        $e = $ellipsoid->getEccentricity();
1712 9
        $e2 = $ellipsoid->getEccentricitySquared();
1713 9
        $e4 = $e ** 4;
1714 9
        $e6 = $e ** 6;
1715 9
        $e8 = $e ** 8;
1716
1717 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1718 9
        $A = $a * $B * $kC * sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2);
1719 9
        $tO = tan(M_PI / 4 - $latC / 2) / ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2);
1720 9
        $D = $B * sqrt((1 - $e2)) / (cos($latC) * sqrt(1 - $e2 * sin($latC) ** 2));
1721 9
        $DD = max(1, $D ** 2);
1722 9
        $F = $D + sqrt($DD - 1) * static::sign($latC);
1723 9
        $H = $F * ($tO) ** $B;
1724 9
        $G = ($F - 1 / $F) / 2;
1725 9
        $gammaO = self::asin(sin($alphaC) / $D);
1726 9
        $lonO = $lonC - (self::asin($G * tan($gammaO))) / $B;
1727
1728 9
        $v = $easting * cos($gammaC) - $northing * sin($gammaC);
1729 9
        $u = $northing * cos($gammaC) + $easting * sin($gammaC);
1730
1731 9
        $Q = M_E ** -($B * $v / $A);
1732 9
        $S = ($Q - 1 / $Q) / 2;
1733 9
        $T = ($Q + 1 / $Q) / 2;
1734 9
        $V = sin($B * $u / $A);
1735 9
        $U = ($V * cos($gammaO) + $S * sin($gammaO)) / $T;
1736 9
        $t = ($H / sqrt((1 + $U) / (1 - $U))) ** (1 / $B);
1737
1738 9
        $chi = M_PI / 2 - 2 * atan($t);
1739
1740 9
        $latitude = $chi + sin(2 * $chi) * ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) + sin(4 * $chi) * (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) + sin(6 * $chi) * (7 * $e6 / 120 + 81 * $e8 / 1120) + sin(8 * $chi) * (4279 * $e8 / 161280);
1741 9
        $longitude = $lonO - atan2(($S * cos($gammaO) - $V * sin($gammaO)), cos($B * $u / $A)) / $B;
1742
1743 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1744
    }
1745
1746
    /**
1747
     * Hotine Oblique Mercator (variant B).
1748
     */
1749 9
    public function obliqueMercatorHotineVariantB(
1750
        Geographic2D|Geographic3D $to,
1751
        Angle $latitudeOfProjectionCentre,
1752
        Angle $longitudeOfProjectionCentre,
1753
        Angle $azimuthOfInitialLine,
1754
        Angle $angleFromRectifiedToSkewGrid,
1755
        Scale $scaleFactorOnInitialLine,
1756
        Length $eastingAtProjectionCentre,
1757
        Length $northingAtProjectionCentre
1758
    ): GeographicPoint {
1759 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1760 9
        $easting = $this->easting->asMetres()->getValue() - $eastingAtProjectionCentre->asMetres()->getValue();
1761 9
        $northing = $this->northing->asMetres()->getValue() - $northingAtProjectionCentre->asMetres()->getValue();
1762 9
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1763 9
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1764 9
        $alphaC = $azimuthOfInitialLine->asRadians()->getValue();
1765 9
        $kC = $scaleFactorOnInitialLine->asUnity()->getValue();
1766 9
        $gammaC = $angleFromRectifiedToSkewGrid->asRadians()->getValue();
1767 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1768 9
        $e = $ellipsoid->getEccentricity();
1769 9
        $e2 = $ellipsoid->getEccentricitySquared();
1770 9
        $e4 = $e ** 4;
1771 9
        $e6 = $e ** 6;
1772 9
        $e8 = $e ** 8;
1773
1774 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1775 9
        $A = $a * $B * $kC * sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2);
1776 9
        $tO = tan(M_PI / 4 - $latC / 2) / ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2);
1777 9
        $D = $B * sqrt((1 - $e2)) / (cos($latC) * sqrt(1 - $e2 * sin($latC) ** 2));
1778 9
        $DD = max(1, $D ** 2);
1779 9
        $F = $D + sqrt($DD - 1) * static::sign($latC);
1780 9
        $H = $F * ($tO) ** $B;
1781 9
        $G = ($F - 1 / $F) / 2;
1782 9
        $gammaO = self::asin(sin($alphaC) / $D);
1783 9
        $lonO = $lonC - (self::asin($G * tan($gammaO))) / $B;
1784 9
        $vC = 0;
0 ignored issues
show
Unused Code introduced by
The assignment to $vC is dead and can be removed.
Loading history...
1785 9
        if ($alphaC === M_PI / 2) {
1786
            $uC = $A * ($lonC - $lonO);
1787
        } else {
1788 9
            $uC = ($A / $B) * atan2(sqrt($DD - 1), cos($alphaC)) * static::sign($latC);
1789
        }
1790
1791 9
        $v = $easting * cos($gammaC) - $northing * sin($gammaC);
1792 9
        $u = $northing * cos($gammaC) + $easting * sin($gammaC) + (abs($uC) * static::sign($latC));
1793
1794 9
        $Q = M_E ** -($B * $v / $A);
1795 9
        $S = ($Q - 1 / $Q) / 2;
1796 9
        $T = ($Q + 1 / $Q) / 2;
1797 9
        $V = sin($B * $u / $A);
1798 9
        $U = ($V * cos($gammaO) + $S * sin($gammaO)) / $T;
1799 9
        $t = ($H / sqrt((1 + $U) / (1 - $U))) ** (1 / $B);
1800
1801 9
        $chi = M_PI / 2 - 2 * atan($t);
1802
1803 9
        $latitude = $chi + sin(2 * $chi) * ($e2 / 2 + 5 * $e4 / 24 + $e6 / 12 + 13 * $e8 / 360) + sin(4 * $chi) * (7 * $e4 / 48 + 29 * $e6 / 240 + 811 * $e8 / 11520) + sin(6 * $chi) * (7 * $e6 / 120 + 81 * $e8 / 1120) + sin(8 * $chi) * (4279 * $e8 / 161280);
1804 9
        $longitude = $lonO - atan2(($S * cos($gammaO) - $V * sin($gammaO)), cos($B * $u / $A)) / $B;
1805
1806 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1807
    }
1808
1809
    /**
1810
     * Laborde Oblique Mercator.
1811
     */
1812 9
    public function obliqueMercatorLaborde(
1813
        Geographic2D|Geographic3D $to,
1814
        Angle $latitudeOfProjectionCentre,
1815
        Angle $longitudeOfProjectionCentre,
1816
        Angle $azimuthOfInitialLine,
1817
        Scale $scaleFactorOnInitialLine,
1818
        Length $falseEasting,
1819
        Length $falseNorthing
1820
    ): GeographicPoint {
1821 9
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1822 9
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1823 9
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1824 9
        $latC = $latitudeOfProjectionCentre->asRadians()->getValue();
1825 9
        $lonC = $longitudeOfProjectionCentre->asRadians()->getValue();
1826 9
        $alphaC = $azimuthOfInitialLine->asRadians()->getValue();
1827 9
        $kC = $scaleFactorOnInitialLine->asUnity()->getValue();
1828 9
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1829 9
        $e = $ellipsoid->getEccentricity();
1830 9
        $e2 = $ellipsoid->getEccentricitySquared();
1831
1832 9
        $B = sqrt(1 + ($e2 * cos($latC) ** 4 / (1 - $e2)));
1833 9
        $latS = self::asin(sin($latC) / $B);
1834 9
        $R = $a * $kC * (sqrt(1 - $e2) / (1 - $e2 * sin($latC) ** 2));
1835 9
        $C = log(tan(M_PI / 4 + $latS / 2)) - $B * log(tan(M_PI / 4 + $latC / 2) * ((1 - $e * sin($latC)) / (1 + $e * sin($latC))) ** ($e / 2));
1836
1837 9
        $G = (new ComplexNumber(1 - cos(2 * $alphaC), sin(2 * $alphaC)))->divide(new ComplexNumber(12, 0));
1838
1839 9
        $H0 = new ComplexNumber($northing / $R, $easting / $R);
1840 9
        $H = $H0->divide($H0->pow(3)->multiply($G)->add($H0));
1841
        do {
1842 9
            $HN = $H;
1843 9
            $H = ($HN->pow(3)->multiply($G)->multiply(new ComplexNumber(2, 0))->add($H0))->divide($HN->pow(2)->multiply($G)->multiply(new ComplexNumber(3, 0))->add(new ComplexNumber(1, 0)));
1844 9
        } while (abs($H0->subtract($H)->subtract($H->pow(3)->multiply($G))->getReal()) >= static::ITERATION_CONVERGENCE_FORMULA);
1845
1846 9
        $LPrime = -1 * $H->getReal();
1847 9
        $PPrime = 2 * atan(M_E ** $H->getImaginary()) - M_PI / 2;
1848 9
        $U = cos($PPrime) * cos($LPrime) * cos($latS) + cos($PPrime) * sin($LPrime) * sin($latS);
1849 9
        $V = sin($PPrime);
1850 9
        $W = cos($PPrime) * cos($LPrime) * sin($latS) - cos($PPrime) * sin($LPrime) * cos($latS);
1851
1852 9
        $d = hypot($U, $V);
1853 9
        if ($d === 0) {
1854
            $L = 0;
1855
            $P = static::sign($W) * M_PI / 2;
1856
        } else {
1857 9
            $L = 2 * atan($V / ($U + $d));
1858 9
            $P = atan($W / $d);
1859
        }
1860
1861 9
        $longitude = $lonC + ($L / $B);
1862
1863 9
        $q = (log(tan(M_PI / 4 + $P / 2)) - $C) / $B;
1864
1865 9
        $latitude = 2 * atan(M_E ** $q) - M_PI / 2;
1866
        do {
1867 9
            $latitudeN = $latitude;
1868 9
            $latitude = 2 * atan(((1 + $e * sin($latitude)) / (1 - $e * sin($latitude))) ** ($e / 2) * M_E ** $q) - M_PI / 2;
1869 9
        } while (abs($latitude - $latitudeN) >= static::ITERATION_CONVERGENCE_FORMULA);
1870
1871 9
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), null, $this->epoch);
1872
    }
1873
1874
    /**
1875
     * Transverse Mercator.
1876
     */
1877 119
    public function transverseMercator(
1878
        Geographic2D|Geographic3D $to,
1879
        Angle $latitudeOfNaturalOrigin,
1880
        Angle $longitudeOfNaturalOrigin,
1881
        Scale $scaleFactorAtNaturalOrigin,
1882
        Length $falseEasting,
1883
        Length $falseNorthing
1884
    ): GeographicPoint {
1885 119
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
1886 119
        $easting = $this->easting->asMetres()->getValue() - $falseEasting->asMetres()->getValue();
1887 119
        $northing = $this->northing->asMetres()->getValue() - $falseNorthing->asMetres()->getValue();
1888 119
        $latitudeOrigin = $latitudeOfNaturalOrigin->asRadians()->getValue();
1889 119
        $longitudeOrigin = $longitudeOfNaturalOrigin->asRadians()->getValue();
1890 119
        $kO = $scaleFactorAtNaturalOrigin->asUnity()->getValue();
1891 119
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
1892 119
        $e = $ellipsoid->getEccentricity();
1893 119
        $f = $ellipsoid->getFlattening();
1894
1895 119
        $n = $f / (2 - $f);
1896 119
        $B = ($a / (1 + $n)) * (1 + $n ** 2 / 4 + $n ** 4 / 64 + $n ** 6 / 256 + (25 / 16384) * $n ** 8);
1897
1898 119
        $h1 = $n / 2 - (2 / 3) * $n ** 2 + (5 / 16) * $n ** 3 + (41 / 180) * $n ** 4 - (127 / 288) * $n ** 5 + (7891 / 37800) * $n ** 6 + (72161 / 387072) * $n ** 7 - (18975107 / 50803200) * $n ** 8;
1899 119
        $h2 = (13 / 48) * $n ** 2 - (3 / 5) * $n ** 3 + (557 / 1440) * $n ** 4 + (281 / 630) * $n ** 5 - (1983433 / 1935360) * $n ** 6 + (13769 / 28800) * $n ** 7 + (148003883 / 174182400) * $n ** 8;
1900 119
        $h3 = (61 / 240) * $n ** 3 - (103 / 140) * $n ** 4 + (15061 / 26880) * $n ** 5 + (167603 / 181440) * $n ** 6 - (67102379 / 29030400) * $n ** 7 + (79682431 / 79833600) * $n ** 8;
1901 119
        $h4 = (49561 / 161280) * $n ** 4 - (179 / 168) * $n ** 5 + (6601661 / 7257600) * $n ** 6 + (97445 / 49896) * $n ** 7 - (40176129013 / 7664025600) * $n ** 8;
1902 119
        $h5 = (34729 / 80640) * $n ** 5 - (3418889 / 1995840) * $n ** 6 + (14644087 / 9123840) * $n ** 7 + (2605413599 / 622702080) * $n ** 8;
1903 119
        $h6 = (212378941 / 319334400) * $n ** 6 - (30705481 / 10378368) * $n ** 7 + (175214326799 / 58118860800) * $n ** 8;
1904 119
        $h7 = (1522256789 / 1383782400) * $n ** 7 - (16759934899 / 3113510400) * $n ** 8;
1905 119
        $h8 = (1424729850961 / 743921418240) * $n ** 8;
1906
1907 119
        if ($latitudeOrigin === 0.0) {
0 ignored issues
show
introduced by
The condition $latitudeOrigin === 0.0 is always false.
Loading history...
1908 81
            $mO = 0;
1909 38
        } elseif ($latitudeOrigin === M_PI / 2) {
1910
            $mO = $B * M_PI / 2;
1911 38
        } elseif ($latitudeOrigin === -M_PI / 2) {
1912
            $mO = $B * -M_PI / 2;
1913
        } else {
1914 38
            $qO = asinh(tan($latitudeOrigin)) - ($e * atanh($e * sin($latitudeOrigin)));
1915 38
            $betaO = atan(sinh($qO));
1916 38
            $xiO0 = self::asin(sin($betaO));
1917 38
            $xiO1 = $h1 * sin(2 * $xiO0);
1918 38
            $xiO2 = $h2 * sin(4 * $xiO0);
1919 38
            $xiO3 = $h3 * sin(6 * $xiO0);
1920 38
            $xiO4 = $h4 * sin(8 * $xiO0);
1921 38
            $xiO5 = $h5 * sin(10 * $xiO0);
1922 38
            $xiO6 = $h6 * sin(12 * $xiO0);
1923 38
            $xiO7 = $h7 * sin(14 * $xiO0);
1924 38
            $xiO8 = $h8 * sin(16 * $xiO0);
1925 38
            $xiO = $xiO0 + $xiO1 + $xiO2 + $xiO3 + $xiO4 + $xiO5 + $xiO6 + $xiO7 + $xiO8;
1926 38
            $mO = $B * $xiO;
1927
        }
1928
1929 119
        $h1 = $n / 2 - (2 / 3) * $n ** 2 + (37 / 96) * $n ** 3 - (1 / 360) * $n ** 4 - (81 / 512) * $n ** 5 + (96199 / 604800) * $n ** 6 - (5406467 / 38707200) * $n ** 7 + (7944359 / 67737600) * $n ** 8;
1930 119
        $h2 = (1 / 48) * $n ** 2 + (1 / 15) * $n ** 3 - (437 / 1440) * $n ** 4 + (46 / 105) * $n ** 5 - (1118711 / 3870720) * $n ** 6 + (51841 / 1209600) * $n ** 7 + (24749483 / 348364800) * $n ** 8;
1931 119
        $h3 = (17 / 480) * $n ** 3 - (37 / 840) * $n ** 4 - (209 / 4480) * $n ** 5 + (5569 / 90720) * $n ** 6 + (9261899 / 58060800) * $n ** 7 - (6457463 / 17740800) * $n ** 8;
1932 119
        $h4 = (4397 / 161280) * $n ** 4 - (11 / 504) * $n ** 5 - (830251 / 7257600) * $n ** 6 + (466511 / 2494800) * $n ** 7 + (324154477 / 7664025600) * $n ** 8;
1933 119
        $h5 = (4583 / 161280) * $n ** 5 - (108847 / 3991680) * $n ** 6 - (8005831 / 63866880) * $n ** 7 + (22894433 / 124540416) * $n ** 8;
1934 119
        $h6 = (20648693 / 638668800) * $n ** 6 - (16363163 / 518918400) * $n ** 7 - (2204645983 / 12915302400) * $n ** 8;
1935 119
        $h7 = (219941297 / 5535129600) * $n ** 7 - (497323811 / 12454041600) * $n ** 8;
1936 119
        $h8 = (191773887257 / 3719607091200) * $n ** 8;
1937
1938 119
        $eta = $easting / ($B * $kO);
1939 119
        $xi = ($northing + $kO * $mO) / ($B * $kO);
1940 119
        $xi1 = $h1 * sin(2 * $xi) * cosh(2 * $eta);
1941 119
        $eta1 = $h1 * cos(2 * $xi) * sinh(2 * $eta);
1942 119
        $xi2 = $h2 * sin(4 * $xi) * cosh(4 * $eta);
1943 119
        $eta2 = $h2 * cos(4 * $xi) * sinh(4 * $eta);
1944 119
        $xi3 = $h3 * sin(6 * $xi) * cosh(6 * $eta);
1945 119
        $eta3 = $h3 * cos(6 * $xi) * sinh(6 * $eta);
1946 119
        $xi4 = $h4 * sin(8 * $xi) * cosh(8 * $eta);
1947 119
        $eta4 = $h4 * cos(8 * $xi) * sinh(8 * $eta);
1948 119
        $xi5 = $h5 * sin(10 * $xi) * cosh(10 * $eta);
1949 119
        $eta5 = $h5 * cos(10 * $xi) * sinh(10 * $eta);
1950 119
        $xi6 = $h6 * sin(12 * $xi) * cosh(12 * $eta);
1951 119
        $eta6 = $h6 * cos(12 * $xi) * sinh(12 * $eta);
1952 119
        $xi7 = $h7 * sin(14 * $xi) * cosh(14 * $eta);
1953 119
        $eta7 = $h7 * cos(14 * $xi) * sinh(14 * $eta);
1954 119
        $xi8 = $h8 * sin(16 * $xi) * cosh(16 * $eta);
1955 119
        $eta8 = $h8 * cos(16 * $xi) * sinh(16 * $eta);
1956 119
        $xi0 = $xi - $xi1 - $xi2 - $xi3 - $xi4 - $xi5 - $xi6 - $xi7 - $xi8;
1957 119
        $eta0 = $eta - $eta1 - $eta2 - $eta3 - $eta4 - $eta5 - $eta6 - $eta7 - $eta8;
1958
1959 119
        $beta = self::asin(sin($xi0) / cosh($eta0));
1960
1961 119
        $QPrime = asinh(tan($beta));
1962 119
        $Q = asinh(tan($beta));
1963
        do {
1964 119
            $QN = $Q;
1965 119
            $Q = $QPrime + ($e * atanh($e * tanh($Q)));
1966 119
        } while (abs($Q - $QN) >= static::ITERATION_CONVERGENCE_FORMULA);
1967
1968 119
        $latitude = atan(sinh($Q));
1969 119
        $longitude = $longitudeOrigin + self::asin(tanh($eta0) / cos($beta));
1970
1971 119
        $height = $this->height && $to instanceof Geographic3D ? $this->height : null;
1972
1973 119
        return GeographicPoint::create($to, new Radian($latitude), new Radian($longitude), $height, $this->epoch);
1974
    }
1975
1976
    /**
1977
     * Transverse Mercator Zoned Grid System
1978
     * If locations fall outwith the fixed zones the general Transverse Mercator method (code 9807) must be used for
1979
     * each zone.
1980
     */
1981 18
    public function transverseMercatorZonedGrid(
1982
        Geographic2D|Geographic3D $to,
1983
        Angle $latitudeOfNaturalOrigin,
1984
        Angle $initialLongitude,
1985
        Angle $zoneWidth,
1986
        Scale $scaleFactorAtNaturalOrigin,
1987
        Length $falseEasting,
1988
        Length $falseNorthing
1989
    ): GeographicPoint {
1990 18
        $Z = substr((string) $this->easting->asMetres()->getValue(), 0, 2);
1991 18
        $falseEasting = $falseEasting->add(new Metre($Z * 1000000));
1992
1993 18
        $W = $zoneWidth->asDegrees()->getValue();
1994 18
        $longitudeOrigin = $initialLongitude->add(new Degree($Z * $W - $W / 2));
1995
1996 18
        return $this->transverseMercator($to, $latitudeOfNaturalOrigin, $longitudeOrigin, $scaleFactorAtNaturalOrigin, $falseEasting, $falseNorthing);
1997
    }
1998
1999
    /**
2000
     * General polynomial.
2001
     * @param Coefficient[] $powerCoefficients
2002
     */
2003
    public function generalPolynomial(
2004
        Projected $to,
2005
        Length $ordinate1OfEvaluationPointInSourceCRS,
2006
        Length $ordinate2OfEvaluationPointInSourceCRS,
2007
        Length $ordinate1OfEvaluationPointInTargetCRS,
2008
        Length $ordinate2OfEvaluationPointInTargetCRS,
2009
        Scale $scalingFactorForSourceCRSCoordDifferences,
2010
        Scale $scalingFactorForTargetCRSCoordDifferences,
2011
        Scale $A0,
2012
        Scale $B0,
2013
        array $powerCoefficients
2014
    ): self {
2015
        $xs = $this->easting->getValue();
2016
        $ys = $this->northing->getValue();
2017
2018
        $t = $this->generalPolynomialUnitless(
2019
            $xs,
2020
            $ys,
2021
            $ordinate1OfEvaluationPointInSourceCRS,
2022
            $ordinate2OfEvaluationPointInSourceCRS,
2023
            $ordinate1OfEvaluationPointInTargetCRS,
2024
            $ordinate2OfEvaluationPointInTargetCRS,
2025
            $scalingFactorForSourceCRSCoordDifferences,
2026
            $scalingFactorForTargetCRSCoordDifferences,
2027
            $A0,
2028
            $B0,
2029
            $powerCoefficients
2030
        );
2031
2032
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2033
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2034
2035
        return static::createFromEastingNorthing(
2036
            $to,
2037
            Length::makeUnit($t['xt'], $xtUnit),
2038
            Length::makeUnit($t['yt'], $ytUnit),
2039
            $this->epoch
2040
        );
2041
    }
2042
2043
    /**
2044
     * New Zealand Map Grid.
2045
     */
2046 27
    public function newZealandMapGrid(
2047
        Geographic2D|Geographic3D $to,
2048
        Angle $latitudeOfNaturalOrigin,
2049
        Angle $longitudeOfNaturalOrigin,
2050
        Length $falseEasting,
2051
        Length $falseNorthing
2052
    ): GeographicPoint {
2053 27
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
2054 27
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
2055
2056 27
        $z = new ComplexNumber(
2057 27
            $this->northing->subtract($falseNorthing)->divide($a)->asMetres()->getValue(),
2058 27
            $this->easting->subtract($falseEasting)->divide($a)->asMetres()->getValue(),
2059
        );
2060
2061 27
        $B1 = new ComplexNumber(0.7557853228, 0.0);
2062 27
        $B2 = new ComplexNumber(0.249204646, 0.003371507);
2063 27
        $B3 = new ComplexNumber(-0.001541739, 0.041058560);
2064 27
        $B4 = new ComplexNumber(-0.10162907, 0.01727609);
2065 27
        $B5 = new ComplexNumber(-0.26623489, -0.36249218);
2066 27
        $B6 = new ComplexNumber(-0.6870983, -1.1651967);
2067 27
        $b1 = new ComplexNumber(1.3231270439, 0.0);
2068 27
        $b2 = new ComplexNumber(-0.577245789, -0.007809598);
2069 27
        $b3 = new ComplexNumber(0.508307513, -0.112208952);
2070 27
        $b4 = new ComplexNumber(-0.15094762, 0.18200602);
2071 27
        $b5 = new ComplexNumber(1.01418179, 1.64497696);
2072 27
        $b6 = new ComplexNumber(1.9660549, 2.5127645);
2073
2074 27
        $zeta = new ComplexNumber(0, 0);
2075 27
        $zeta = $zeta->add($b1->multiply($z->pow(1)));
2076 27
        $zeta = $zeta->add($b2->multiply($z->pow(2)));
2077 27
        $zeta = $zeta->add($b3->multiply($z->pow(3)));
2078 27
        $zeta = $zeta->add($b4->multiply($z->pow(4)));
2079 27
        $zeta = $zeta->add($b5->multiply($z->pow(5)));
2080 27
        $zeta = $zeta->add($b6->multiply($z->pow(6)));
2081
2082 27
        for ($iterations = 0; $iterations < 2; ++$iterations) {
2083 27
            $numerator = $z;
2084 27
            $numerator = $numerator->add($B2->multiply($zeta->pow(2))->multiply(new ComplexNumber(1, 0)));
2085 27
            $numerator = $numerator->add($B3->multiply($zeta->pow(3))->multiply(new ComplexNumber(2, 0)));
2086 27
            $numerator = $numerator->add($B4->multiply($zeta->pow(4))->multiply(new ComplexNumber(3, 0)));
2087 27
            $numerator = $numerator->add($B5->multiply($zeta->pow(5))->multiply(new ComplexNumber(4, 0)));
2088 27
            $numerator = $numerator->add($B6->multiply($zeta->pow(6))->multiply(new ComplexNumber(5, 0)));
2089
2090 27
            $denominator = $B1;
2091 27
            $denominator = $denominator->add($B2->multiply($zeta->pow(1))->multiply(new ComplexNumber(2, 0)));
2092 27
            $denominator = $denominator->add($B3->multiply($zeta->pow(2))->multiply(new ComplexNumber(3, 0)));
2093 27
            $denominator = $denominator->add($B4->multiply($zeta->pow(3))->multiply(new ComplexNumber(4, 0)));
2094 27
            $denominator = $denominator->add($B5->multiply($zeta->pow(4))->multiply(new ComplexNumber(5, 0)));
2095 27
            $denominator = $denominator->add($B6->multiply($zeta->pow(5))->multiply(new ComplexNumber(6, 0)));
2096
2097 27
            $zeta = $numerator->divide($denominator);
2098
        }
2099
2100 27
        $deltaPsi = $zeta->getReal();
2101 27
        $deltaLatitudeToOrigin = 0;
2102 27
        $deltaLatitudeToOrigin += 1.5627014243 * $deltaPsi ** 1;
2103 27
        $deltaLatitudeToOrigin += 0.5185406398 * $deltaPsi ** 2;
2104 27
        $deltaLatitudeToOrigin += -0.03333098 * $deltaPsi ** 3;
2105 27
        $deltaLatitudeToOrigin += -0.1052906 * $deltaPsi ** 4;
2106 27
        $deltaLatitudeToOrigin += -0.0368594 * $deltaPsi ** 5;
2107 27
        $deltaLatitudeToOrigin += 0.007317 * $deltaPsi ** 6;
2108 27
        $deltaLatitudeToOrigin += 0.01220 * $deltaPsi ** 7;
2109 27
        $deltaLatitudeToOrigin += 0.00394 * $deltaPsi ** 8;
2110 27
        $deltaLatitudeToOrigin += -0.0013 * $deltaPsi ** 9;
2111
2112 27
        $latitude = $latitudeOfNaturalOrigin->add(new ArcSecond($deltaLatitudeToOrigin / 0.00001));
2113 27
        $longitude = $longitudeOfNaturalOrigin->add(new Radian($zeta->getImaginary()));
2114
2115 27
        return GeographicPoint::create($to, $latitude, $longitude, null, $this->epoch);
2116
    }
2117
2118
    /**
2119
     * Complex polynomial.
2120
     * Coordinate pairs treated as complex numbers.  This exploits the correlation between the polynomial coefficients
2121
     * and leads to a smaller number of coefficients than the general polynomials.
2122
     */
2123 9
    public function complexPolynomial(
2124
        Projected $to,
2125
        Length $ordinate1OfEvaluationPointInSourceCRS,
2126
        Length $ordinate2OfEvaluationPointInSourceCRS,
2127
        Length $ordinate1OfEvaluationPointInTargetCRS,
2128
        Length $ordinate2OfEvaluationPointInTargetCRS,
2129
        Scale $scalingFactorForSourceCRSCoordDifferences,
2130
        Scale $scalingFactorForTargetCRSCoordDifferences,
2131
        Scale $A1,
2132
        Scale $A2,
2133
        Scale $A3,
2134
        Scale $A4,
2135
        Scale $A5,
2136
        Scale $A6,
2137
        ?Scale $A7 = null,
2138
        ?Scale $A8 = null
2139
    ): self {
2140 9
        $xs = $this->easting->getValue();
2141 9
        $ys = $this->northing->getValue();
2142 9
        $xso = $ordinate1OfEvaluationPointInSourceCRS->getValue();
2143 9
        $yso = $ordinate2OfEvaluationPointInSourceCRS->getValue();
2144 9
        $xto = $ordinate1OfEvaluationPointInTargetCRS->getValue();
2145 9
        $yto = $ordinate2OfEvaluationPointInTargetCRS->getValue();
2146
2147 9
        $U = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($xs - $xso);
2148 9
        $V = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($ys - $yso);
2149
2150 9
        $mTdXdY = new ComplexNumber(0, 0);
2151 9
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A1->getValue(), $A2->getValue()))->multiply(new ComplexNumber($U, $V))->pow(1));
2152 9
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A3->getValue(), $A4->getValue()))->multiply((new ComplexNumber($U, $V))->pow(2)));
2153 9
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A5->getValue(), $A6->getValue()))->multiply((new ComplexNumber($U, $V))->pow(3)));
2154 9
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A7 ? $A7->getValue() : 0, $A8 ? $A8->getValue() : 0))->multiply((new ComplexNumber($U, $V))->pow(4)));
2155
2156 9
        $xt = $xs - $xso + $xto + $mTdXdY->getReal() / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
2157 9
        $yt = $ys - $yso + $yto + $mTdXdY->getImaginary() / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
2158
2159 9
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2160 9
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2161
2162 9
        return static::createFromEastingNorthing(
2163
            $to,
2164 9
            Length::makeUnit($xt, $xtUnit),
2165 9
            Length::makeUnit($yt, $ytUnit),
2166 9
            $this->epoch
2167
        );
2168
    }
2169
2170
    /**
2171
     * Ordnance Survey National Transformation
2172
     * Geodetic transformation between ETRS89 (or WGS 84) and OSGB36 / National Grid.  Uses ETRS89 / National Grid as
2173
     * an intermediate coordinate system for bi-linear interpolation of gridded grid coordinate differences.
2174
     */
2175 2
    public function OSTN15(
2176
        Geographic2D $to,
2177
        OSTNOSGM15Grid $eastingAndNorthingDifferenceFile
2178
    ): GeographicPoint {
2179 2
        $asETRS89 = $eastingAndNorthingDifferenceFile->applyReverseHorizontalAdjustment($this);
2180
2181 2
        return $asETRS89->transverseMercator($to, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2182
    }
2183
}
2184