Passed
Push — master ( 48e916...a64be8 )
by Doug
13:11
created

ProjectedPoint::obliqueMercatorHotineVariantA()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 52
Code Lines 36

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 36
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 52
ccs 37
cts 37
cp 1
crap 1
rs 9.344

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

131
            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

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

132
            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

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

133
            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

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