Passed
Push — master ( 344e29...cbaf3e )
by Doug
38:02
created

ProjectedPoint::krovak()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 46
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 31
c 0
b 0
f 0
nc 1
nop 8
dl 0
loc 46
ccs 31
cts 31
cp 1
crap 3
rs 9.424

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

155
            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

155
            Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID => new BritishNationalGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch),
Loading history...
156 4894
            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

156
            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

156
            Projected::EPSG_TM75_IRISH_GRID => new IrishGridPoint($easting, /** @scrutinizer ignore-type */ $northing, $epoch),
Loading history...
157 4894
            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

157
            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

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