Passed
Push — 4.x ( e9b635...bc20e1 )
by Doug
07:44
created

ProjectedPoint::lambertConicConformal2SPMichigan()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 46
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 3.0003

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 31
c 1
b 0
f 0
dl 0
loc 46
ccs 29
cts 30
cp 0.9667
rs 9.424
cc 3
nc 2
nop 8
crap 3.0003

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * PHPCoord.
4
 *
5
 * @author Doug Wright
6
 */
7
declare(strict_types=1);
8
9
namespace PHPCoord;
10
11
use function abs;
12
use function asin;
13
use function atan2;
14
use function cos;
15
use DateTime;
16
use DateTimeImmutable;
17
use DateTimeInterface;
18
use function implode;
19
use function log;
20
use PHPCoord\CoordinateOperation\ComplexNumber;
21
use PHPCoord\CoordinateReferenceSystem\Geographic;
22
use PHPCoord\CoordinateReferenceSystem\Projected;
23
use PHPCoord\CoordinateSystem\Axis;
24
use PHPCoord\Exception\InvalidAxesException;
25
use PHPCoord\Exception\InvalidCoordinateReferenceSystemException;
26
use PHPCoord\Exception\UnknownAxisException;
27
use PHPCoord\UnitOfMeasure\Angle\Angle;
28
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
29
use PHPCoord\UnitOfMeasure\Angle\Degree;
30
use PHPCoord\UnitOfMeasure\Angle\Radian;
31
use PHPCoord\UnitOfMeasure\Length\Length;
32
use PHPCoord\UnitOfMeasure\Length\Metre;
33
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
34
use PHPCoord\UnitOfMeasure\Scale\Scale;
35
use function sin;
36
use function sqrt;
37
use function tan;
38
39
/**
40
 * Coordinate representing a point on a map projection.
41
 */
42
class ProjectedPoint extends Point
43
{
44
    /**
45
     * Easting.
46
     */
47
    protected Length $easting;
48
49
    /**
50
     * Northing.
51
     */
52
    protected Length $northing;
53
54
    /**
55
     * Westing.
56
     */
57
    protected Length $westing;
58
59
    /**
60
     * Southing.
61
     */
62
    protected Length $southing;
63
64
    /**
65
     * Coordinate reference system.
66
     */
67
    protected Projected $crs;
68
69
    /**
70
     * Coordinate epoch (date for which the specified coordinates represented this point).
71
     */
72
    protected ?DateTimeImmutable $epoch;
73
74 130
    protected function __construct(?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, Projected $crs, ?DateTimeInterface $epoch = null)
75
    {
76 130
        $this->crs = $crs;
77
78 130
        $eastingAxis = $this->getAxisByName(Axis::EASTING);
79 130
        $westingAxis = $this->getAxisByName(Axis::WESTING);
80 130
        $northingAxis = $this->getAxisByName(Axis::NORTHING);
81 130
        $southingAxis = $this->getAxisByName(Axis::SOUTHING);
82
83 130
        if ($easting && $eastingAxis) {
84 116
            $this->easting = Length::convert($easting, $eastingAxis->getUnitOfMeasureId());
85 116
            $this->westing = $this->easting->multiply(-1);
86 16
        } elseif ($westing && $westingAxis) {
87 15
            $this->westing = Length::convert($westing, $westingAxis->getUnitOfMeasureId());
88 15
            $this->easting = $this->westing->multiply(-1);
89
        } else {
90 1
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
91
        }
92
93 129
        if ($northing && $northingAxis) {
94 119
            $this->northing = Length::convert($northing, $northingAxis->getUnitOfMeasureId());
95 119
            $this->southing = $this->northing->multiply(-1);
96 11
        } elseif ($southing && $southingAxis) {
97 10
            $this->southing = Length::convert($southing, $southingAxis->getUnitOfMeasureId());
98 10
            $this->northing = $this->southing->multiply(-1);
99
        } else {
100 1
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
101
        }
102
103 128
        if ($epoch instanceof DateTime) {
104 1
            $epoch = DateTimeImmutable::createFromMutable($epoch);
105
        }
106 128
        $this->epoch = $epoch;
107 128
    }
108
109 110
    public static function create(?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, Projected $crs, ?DateTimeInterface $epoch = null): self
110
    {
111 110
        if ($crs->getSRID() === Projected::EPSG_OSGB_1936_BRITISH_NATIONAL_GRID) {
112 6
            return new BritishNationalGridPoint($easting, $northing, $epoch);
0 ignored issues
show
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\BritishNationalGridPoint::__construct() does only seem to accept PHPCoord\UnitOfMeasure\Length\Length, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

112
            return new BritishNationalGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch);
Loading history...
113
        }
114
115 107
        if ($crs->getSRID() === Projected::EPSG_TM75_IRISH_GRID) {
116 1
            return new IrishGridPoint($easting, $northing, $epoch);
0 ignored issues
show
Bug introduced by
It seems like $northing can also be of type null; however, parameter $northing of PHPCoord\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

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

116
            return new IrishGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch);
Loading history...
117
        }
118
119 106
        if ($crs->getSRID() === Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR) {
120 1
            return 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

120
            return 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

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