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

ProjectedPoint::affineParametricTransform()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

157
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\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

157
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
158
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($easting, $northing, $epoch),
0 ignored issues
show
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\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

158
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
Bug introduced by
It seems like $easting can also be of type null; however, parameter $easting of PHPCoord\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

158
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
159
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint($easting, $northing, $epoch),
0 ignored issues
show
Bug introduced by
It seems like $easting can also be of type null; however, parameter $easting of PHPCoord\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

159
            Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR => new IrishTransverseMercatorPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\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

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