Passed
Push — master ( 9ef2b1...e5f776 )
by Doug
61:06
created

ProjectedPoint::__construct()   D

Complexity

Conditions 18
Paths 178

Size

Total Lines 44
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 30
c 0
b 0
f 0
nc 178
nop 7
dl 0
loc 44
rs 4.2166

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord\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 $azimuthOfInitialLine,
1730
        Angle $angleFromRectifiedToSkewGrid,
1731
        Scale $scaleFactorOnInitialLine,
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 = $azimuthOfInitialLine->asRadians()->getValue();
1741
        $kC = $scaleFactorOnInitialLine->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 $azimuthOfInitialLine,
1787
        Angle $angleFromRectifiedToSkewGrid,
1788
        Scale $scaleFactorOnInitialLine,
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 = $azimuthOfInitialLine->asRadians()->getValue();
1798
        $kC = $scaleFactorOnInitialLine->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 $azimuthOfInitialLine,
1850
        Scale $scaleFactorOnInitialLine,
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 = $azimuthOfInitialLine->asRadians()->getValue();
1860
        $kC = $scaleFactorOnInitialLine->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
    ): self {
2048
        $xs = $this->easting->getValue();
2049
        $ys = $this->northing->getValue();
2050
2051
        $t = $this->generalPolynomialUnitless(
2052
            $xs,
2053
            $ys,
2054
            $ordinate1OfEvaluationPointInSourceCRS,
2055
            $ordinate2OfEvaluationPointInSourceCRS,
2056
            $ordinate1OfEvaluationPointInTargetCRS,
2057
            $ordinate2OfEvaluationPointInTargetCRS,
2058
            $scalingFactorForSourceCRSCoordDifferences,
2059
            $scalingFactorForTargetCRSCoordDifferences,
2060
            $A0,
2061
            $B0,
2062
            $powerCoefficients
2063
        );
2064
2065
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2066
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2067
2068
        return static::createFromEastingNorthing(
2069
            $to,
2070
            Length::makeUnit($t['xt'], $xtUnit),
2071
            Length::makeUnit($t['yt'], $ytUnit),
2072
            $this->epoch
2073
        );
2074
    }
2075
2076
    /**
2077
     * New Zealand Map Grid.
2078
     */
2079
    public function newZealandMapGrid(
2080
        Geographic2D|Geographic3D $to,
2081
        Angle $latitudeOfNaturalOrigin,
2082
        Angle $longitudeOfNaturalOrigin,
2083
        Length $falseEasting,
2084
        Length $falseNorthing
2085
    ): GeographicPoint {
2086
        $ellipsoid = $this->crs->getDatum()->getEllipsoid();
2087
        $a = $ellipsoid->getSemiMajorAxis()->asMetres()->getValue();
2088
2089
        $z = new ComplexNumber(
2090
            $this->northing->subtract($falseNorthing)->divide($a)->asMetres()->getValue(),
2091
            $this->easting->subtract($falseEasting)->divide($a)->asMetres()->getValue(),
2092
        );
2093
2094
        $B1 = new ComplexNumber(0.7557853228, 0.0);
2095
        $B2 = new ComplexNumber(0.249204646, 0.003371507);
2096
        $B3 = new ComplexNumber(-0.001541739, 0.041058560);
2097
        $B4 = new ComplexNumber(-0.10162907, 0.01727609);
2098
        $B5 = new ComplexNumber(-0.26623489, -0.36249218);
2099
        $B6 = new ComplexNumber(-0.6870983, -1.1651967);
2100
        $b1 = new ComplexNumber(1.3231270439, 0.0);
2101
        $b2 = new ComplexNumber(-0.577245789, -0.007809598);
2102
        $b3 = new ComplexNumber(0.508307513, -0.112208952);
2103
        $b4 = new ComplexNumber(-0.15094762, 0.18200602);
2104
        $b5 = new ComplexNumber(1.01418179, 1.64497696);
2105
        $b6 = new ComplexNumber(1.9660549, 2.5127645);
2106
2107
        $zeta = new ComplexNumber(0, 0);
2108
        $zeta = $zeta->add($b1->multiply($z->pow(1)));
2109
        $zeta = $zeta->add($b2->multiply($z->pow(2)));
2110
        $zeta = $zeta->add($b3->multiply($z->pow(3)));
2111
        $zeta = $zeta->add($b4->multiply($z->pow(4)));
2112
        $zeta = $zeta->add($b5->multiply($z->pow(5)));
2113
        $zeta = $zeta->add($b6->multiply($z->pow(6)));
2114
2115
        for ($iterations = 0; $iterations < 2; ++$iterations) {
2116
            $numerator = $z;
2117
            $numerator = $numerator->add($B2->multiply($zeta->pow(2))->multiply(new ComplexNumber(1, 0)));
2118
            $numerator = $numerator->add($B3->multiply($zeta->pow(3))->multiply(new ComplexNumber(2, 0)));
2119
            $numerator = $numerator->add($B4->multiply($zeta->pow(4))->multiply(new ComplexNumber(3, 0)));
2120
            $numerator = $numerator->add($B5->multiply($zeta->pow(5))->multiply(new ComplexNumber(4, 0)));
2121
            $numerator = $numerator->add($B6->multiply($zeta->pow(6))->multiply(new ComplexNumber(5, 0)));
2122
2123
            $denominator = $B1;
2124
            $denominator = $denominator->add($B2->multiply($zeta->pow(1))->multiply(new ComplexNumber(2, 0)));
2125
            $denominator = $denominator->add($B3->multiply($zeta->pow(2))->multiply(new ComplexNumber(3, 0)));
2126
            $denominator = $denominator->add($B4->multiply($zeta->pow(3))->multiply(new ComplexNumber(4, 0)));
2127
            $denominator = $denominator->add($B5->multiply($zeta->pow(4))->multiply(new ComplexNumber(5, 0)));
2128
            $denominator = $denominator->add($B6->multiply($zeta->pow(5))->multiply(new ComplexNumber(6, 0)));
2129
2130
            $zeta = $numerator->divide($denominator);
2131
        }
2132
2133
        $deltaPsi = $zeta->getReal();
2134
        $deltaLatitudeToOrigin = 0;
2135
        $deltaLatitudeToOrigin += 1.5627014243 * $deltaPsi ** 1;
2136
        $deltaLatitudeToOrigin += 0.5185406398 * $deltaPsi ** 2;
2137
        $deltaLatitudeToOrigin += -0.03333098 * $deltaPsi ** 3;
2138
        $deltaLatitudeToOrigin += -0.1052906 * $deltaPsi ** 4;
2139
        $deltaLatitudeToOrigin += -0.0368594 * $deltaPsi ** 5;
2140
        $deltaLatitudeToOrigin += 0.007317 * $deltaPsi ** 6;
2141
        $deltaLatitudeToOrigin += 0.01220 * $deltaPsi ** 7;
2142
        $deltaLatitudeToOrigin += 0.00394 * $deltaPsi ** 8;
2143
        $deltaLatitudeToOrigin += -0.0013 * $deltaPsi ** 9;
2144
2145
        $latitude = $latitudeOfNaturalOrigin->add(new ArcSecond($deltaLatitudeToOrigin / 0.00001));
2146
        $longitude = $longitudeOfNaturalOrigin->add(new Radian($zeta->getImaginary()));
2147
2148
        return GeographicPoint::create($to, $latitude, $longitude, null, $this->epoch);
2149
    }
2150
2151
    /**
2152
     * Complex polynomial.
2153
     * Coordinate pairs treated as complex numbers.  This exploits the correlation between the polynomial coefficients
2154
     * and leads to a smaller number of coefficients than the general polynomials.
2155
     */
2156
    public function complexPolynomial(
2157
        Projected $to,
2158
        Length $ordinate1OfEvaluationPointInSourceCRS,
2159
        Length $ordinate2OfEvaluationPointInSourceCRS,
2160
        Length $ordinate1OfEvaluationPointInTargetCRS,
2161
        Length $ordinate2OfEvaluationPointInTargetCRS,
2162
        Scale $scalingFactorForSourceCRSCoordDifferences,
2163
        Scale $scalingFactorForTargetCRSCoordDifferences,
2164
        Scale $A1,
2165
        Scale $A2,
2166
        Scale $A3,
2167
        Scale $A4,
2168
        Scale $A5,
2169
        Scale $A6,
2170
        ?Scale $A7 = null,
2171
        ?Scale $A8 = null
2172
    ): self {
2173
        $xs = $this->easting->getValue();
2174
        $ys = $this->northing->getValue();
2175
        $xso = $ordinate1OfEvaluationPointInSourceCRS->getValue();
2176
        $yso = $ordinate2OfEvaluationPointInSourceCRS->getValue();
2177
        $xto = $ordinate1OfEvaluationPointInTargetCRS->getValue();
2178
        $yto = $ordinate2OfEvaluationPointInTargetCRS->getValue();
2179
2180
        $U = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($xs - $xso);
2181
        $V = $scalingFactorForSourceCRSCoordDifferences->asUnity()->getValue() * ($ys - $yso);
2182
2183
        $mTdXdY = new ComplexNumber(0, 0);
2184
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A1->getValue(), $A2->getValue()))->multiply(new ComplexNumber($U, $V))->pow(1));
2185
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A3->getValue(), $A4->getValue()))->multiply((new ComplexNumber($U, $V))->pow(2)));
2186
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A5->getValue(), $A6->getValue()))->multiply((new ComplexNumber($U, $V))->pow(3)));
2187
        $mTdXdY = $mTdXdY->add((new ComplexNumber($A7 ? $A7->getValue() : 0, $A8 ? $A8->getValue() : 0))->multiply((new ComplexNumber($U, $V))->pow(4)));
2188
2189
        $xt = $xs - $xso + $xto + $mTdXdY->getReal() / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
2190
        $yt = $ys - $yso + $yto + $mTdXdY->getImaginary() / $scalingFactorForTargetCRSCoordDifferences->asUnity()->getValue();
2191
2192
        $xtUnit = $to->getCoordinateSystem()->getAxes()[0]->getUnitOfMeasureId();
2193
        $ytUnit = $to->getCoordinateSystem()->getAxes()[1]->getUnitOfMeasureId();
2194
2195
        return static::createFromEastingNorthing(
2196
            $to,
2197
            Length::makeUnit($xt, $xtUnit),
2198
            Length::makeUnit($yt, $ytUnit),
2199
            $this->epoch
2200
        );
2201
    }
2202
2203
    /**
2204
     * Ordnance Survey National Transformation
2205
     * Geodetic transformation between ETRS89 (or WGS 84) and OSGB36 / National Grid.  Uses ETRS89 / National Grid as
2206
     * an intermediate coordinate system for bi-linear interpolation of gridded grid coordinate differences.
2207
     */
2208
    public function OSTN15(
2209
        Geographic2D $to,
2210
        OSTNOSGM15Grid $eastingAndNorthingDifferenceFile
2211
    ): GeographicPoint {
2212
        $asETRS89 = $eastingAndNorthingDifferenceFile->applyReverseHorizontalAdjustment($this);
2213
2214
        return $asETRS89->transverseMercator($to, new Degree(49), new Degree(-2), new Unity(0.9996012717), new Metre(400000), new Metre(-100000));
2215
    }
2216
}
2217