Passed
Push — master ( 0a16ff...1038d1 )
by Doug
28:57
created

ProjectedPoint::obliqueMercatorHotineVariantB()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 58
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 41
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 41
nc 2
nop 8
dl 0
loc 58
ccs 41
cts 41
cp 1
crap 2
rs 9.264
c 1
b 0
f 0

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord;
10
11
use function abs;
12
use function asinh;
13
use function atan;
14
use function atan2;
15
use function atanh;
16
use function cos;
17
use function cosh;
18
use function count;
19
20
use DateTime;
21
use DateTimeImmutable;
22
use DateTimeInterface;
23
24
use function hypot;
25
use function implode;
26
use function is_nan;
27
use function log;
28
29
use const M_E;
30
use const M_PI;
31
use const M_PI_2;
32
33
use function max;
34
35
use PHPCoord\CoordinateOperation\AutoConversion;
36
use PHPCoord\CoordinateOperation\ComplexNumber;
37
use PHPCoord\CoordinateOperation\ConvertiblePoint;
38
use PHPCoord\CoordinateOperation\OSTNOSGM15Grid;
39
use PHPCoord\CoordinateReferenceSystem\CoordinateReferenceSystem;
40
use PHPCoord\CoordinateReferenceSystem\Geographic2D;
41
use PHPCoord\CoordinateReferenceSystem\Geographic3D;
42
use PHPCoord\CoordinateReferenceSystem\Projected;
0 ignored issues
show
Bug introduced by
The type PHPCoord\CoordinateReferenceSystem\Projected was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
use PHPCoord\CoordinateSystem\Axis;
44
use PHPCoord\CoordinateSystem\Cartesian;
45
use PHPCoord\Exception\InvalidAxesException;
46
use PHPCoord\Exception\InvalidCoordinateReferenceSystemException;
47
use PHPCoord\Exception\UnknownAxisException;
48
use PHPCoord\UnitOfMeasure\Angle\Angle;
49
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
50
use PHPCoord\UnitOfMeasure\Angle\Degree;
51
use PHPCoord\UnitOfMeasure\Angle\Radian;
52
use PHPCoord\UnitOfMeasure\Length\Length;
53
use PHPCoord\UnitOfMeasure\Length\Metre;
54
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
55
use PHPCoord\UnitOfMeasure\Scale\Scale;
56
use PHPCoord\UnitOfMeasure\Scale\Unity;
57
58
use function sin;
59
use function sinh;
60
use function sqrt;
61
use function substr;
62
use function tan;
63
use function tanh;
64
65
/**
66
 * Coordinate representing a point on a map projection.
67
 */
68
class ProjectedPoint extends Point implements ConvertiblePoint
69
{
70
    use AutoConversion {
71
        convert as protected autoConvert;
72
    }
73
74
    /**
75
     * Easting.
76
     */
77
    protected Length $easting;
78
79
    /**
80
     * Northing.
81
     */
82
    protected Length $northing;
83
84
    /**
85
     * Westing.
86
     */
87
    protected Length $westing;
88
89
    /**
90
     * Southing.
91
     */
92
    protected Length $southing;
93
94
    /**
95
     * Height.
96
     */
97
    protected ?Length $height;
98
99
    /**
100
     * Coordinate reference system.
101
     */
102
    protected Projected $crs;
103
104
    /**
105
     * Coordinate epoch (date for which the specified coordinates represented this point).
106
     */
107
    protected ?DateTimeImmutable $epoch;
108
109 5119
    protected function __construct(Projected $crs, ?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, ?DateTimeInterface $epoch, ?Length $height)
110
    {
111 5119
        if (count($crs->getCoordinateSystem()->getAxes()) === 2 && $height !== null) {
112
            throw new InvalidCoordinateReferenceSystemException('A 2D projected point must not include a height');
113
        }
114
115 5119
        if (count($crs->getCoordinateSystem()->getAxes()) === 3 && $height === null) {
116
            throw new InvalidCoordinateReferenceSystemException('A 3D projected point must include a height, none given');
117
        }
118
119 5119
        $this->crs = $crs;
120
121 5119
        $eastingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::EASTING);
122 5119
        $westingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::WESTING);
123 5119
        $northingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::NORTHING);
124 5119
        $southingAxis = $this->crs->getCoordinateSystem()->getAxisByName(Axis::SOUTHING);
125
126 5119
        if ($easting && $eastingAxis) {
127 4948
            $this->easting = $easting::convert($easting, $eastingAxis->getUnitOfMeasureId());
128 4948
            $this->westing = $this->easting->multiply(-1);
129 171
        } elseif ($westing && $westingAxis) {
130 162
            $this->westing = $westing::convert($westing, $westingAxis->getUnitOfMeasureId());
131 162
            $this->easting = $this->westing->multiply(-1);
132
        } else {
133 9
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
134
        }
135
136 5110
        if ($northing && $northingAxis) {
137 4975
            $this->northing = $northing::convert($northing, $northingAxis->getUnitOfMeasureId());
138 4975
            $this->southing = $this->northing->multiply(-1);
139 135
        } elseif ($southing && $southingAxis) {
140 126
            $this->southing = $southing::convert($southing, $southingAxis->getUnitOfMeasureId());
141 126
            $this->northing = $this->southing->multiply(-1);
142
        } else {
143 9
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
144
        }
145
146 5101
        if ($epoch instanceof DateTime) {
147 18
            $epoch = DateTimeImmutable::createFromMutable($epoch);
148
        }
149 5101
        $this->epoch = $epoch;
150
151 5101
        $this->height = $height;
152
    }
153
154 4894
    public static function create(Projected $crs, ?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, ?DateTimeInterface $epoch = null, ?Length $height = null): self
155
    {
156 4894
        return match ($crs->getSRID()) {
157 4894
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint($easting, $northing, $epoch),
0 ignored issues
show
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\BritishNationalGridPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

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

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

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

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

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\IrishTransverse...torPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

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