ProjectedPoint::obliqueMercatorHotineVariantB()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 58
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 41
c 0
b 0
f 0
nc 2
nop 8
dl 0
loc 58
ccs 39
cts 40
cp 0.975
crap 2
rs 9.264

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

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

158
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint($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\Point\BritishNa...ridPoint::__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

158
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
159 1108
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($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\Point\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

159
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint(/** @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\Point\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

159
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
160 1198
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint($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\Point\IrishTran...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

160
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint($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\Point\IrishTran...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

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