Passed
Push — master ( bbeaaa...9ed6c0 )
by Doug
25:29
created

ProjectedPoint::lambertConicConformal2SPMichigan()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 47
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 30
CRAP Score 3.0003

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 32
c 1
b 0
f 0
nc 2
nop 8
dl 0
loc 47
ccs 30
cts 31
cp 0.9677
crap 3.0003
rs 9.408

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

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

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

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

154
            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

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

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

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