Passed
Push — master ( aca036...b09582 )
by Doug
83:35 queued 68:07
created

ProjectedPoint::obliqueMercatorLaborde()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 59
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 4.0023

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 40
c 1
b 0
f 0
nc 2
nop 7
dl 0
loc 59
ccs 36
cts 38
cp 0.9474
crap 4.0023
rs 9.28

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

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

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
36
use PHPCoord\CoordinateSystem\Axis;
37
use PHPCoord\CoordinateSystem\Cartesian;
38
use PHPCoord\Exception\InvalidAxesException;
39
use PHPCoord\Exception\InvalidCoordinateReferenceSystemException;
40
use PHPCoord\Exception\UnknownAxisException;
41
use PHPCoord\UnitOfMeasure\Angle\Angle;
42
use PHPCoord\UnitOfMeasure\Angle\ArcSecond;
43
use PHPCoord\UnitOfMeasure\Angle\Degree;
44
use PHPCoord\UnitOfMeasure\Angle\Radian;
45
use PHPCoord\UnitOfMeasure\Length\Length;
46
use PHPCoord\UnitOfMeasure\Length\Metre;
47
use PHPCoord\UnitOfMeasure\Scale\Coefficient;
48
use PHPCoord\UnitOfMeasure\Scale\Scale;
49
use PHPCoord\UnitOfMeasure\Scale\Unity;
50
use function sin;
51
use function sinh;
52
use function sqrt;
53
use function substr;
54
use function tan;
55
use function tanh;
56
57
/**
58
 * Coordinate representing a point on a map projection.
59
 */
60
class ProjectedPoint extends Point implements ConvertiblePoint
61
{
62
    use AutoConversion;
63
64
    /**
65
     * Easting.
66
     */
67
    protected Length $easting;
68
69
    /**
70
     * Northing.
71
     */
72
    protected Length $northing;
73
74
    /**
75
     * Westing.
76
     */
77
    protected Length $westing;
78
79
    /**
80
     * Southing.
81
     */
82
    protected Length $southing;
83
84
    /**
85
     * Coordinate reference system.
86
     */
87
    protected Projected $crs;
88
89
    /**
90
     * Coordinate epoch (date for which the specified coordinates represented this point).
91
     */
92
    protected ?DateTimeImmutable $epoch;
93
94 1314
    protected function __construct(?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, Projected $crs, ?DateTimeInterface $epoch = null)
95
    {
96 1314
        $this->crs = $crs;
97
98 1314
        $eastingAxis = $this->getAxisByName(Axis::EASTING);
99 1314
        $westingAxis = $this->getAxisByName(Axis::WESTING);
100 1314
        $northingAxis = $this->getAxisByName(Axis::NORTHING);
101 1314
        $southingAxis = $this->getAxisByName(Axis::SOUTHING);
102
103 1314
        if ($easting && $eastingAxis) {
104 1188
            $this->easting = Length::convert($easting, $eastingAxis->getUnitOfMeasureId());
105 1188
            $this->westing = $this->easting->multiply(-1);
106 144
        } elseif ($westing && $westingAxis) {
107 135
            $this->westing = Length::convert($westing, $westingAxis->getUnitOfMeasureId());
108 135
            $this->easting = $this->westing->multiply(-1);
109
        } else {
110 9
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
111
        }
112
113 1305
        if ($northing && $northingAxis) {
114 1215
            $this->northing = Length::convert($northing, $northingAxis->getUnitOfMeasureId());
115 1215
            $this->southing = $this->northing->multiply(-1);
116 99
        } elseif ($southing && $southingAxis) {
117 90
            $this->southing = Length::convert($southing, $southingAxis->getUnitOfMeasureId());
118 90
            $this->northing = $this->southing->multiply(-1);
119
        } else {
120 9
            throw new InvalidAxesException($crs->getCoordinateSystem()->getAxes());
121
        }
122
123 1296
        if ($epoch instanceof DateTime) {
124 9
            $epoch = DateTimeImmutable::createFromMutable($epoch);
125
        }
126 1296
        $this->epoch = $epoch;
127 1296
    }
128
129 1089
    public static function create(?Length $easting, ?Length $northing, ?Length $westing, ?Length $southing, Projected $crs, ?DateTimeInterface $epoch = null): self
130
    {
131 1089
        if ($crs->getSRID() === Projected::EPSG_OSGB36_BRITISH_NATIONAL_GRID) {
132 90
            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

132
            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

132
            return new BritishNationalGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch);
Loading history...
133
        }
134
135 1035
        if ($crs->getSRID() === Projected::EPSG_TM75_IRISH_GRID) {
136 9
            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

136
            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

136
            return new IrishGridPoint(/** @scrutinizer ignore-type */ $easting, $northing, $epoch);
Loading history...
137
        }
138
139 1026
        if ($crs->getSRID() === Projected::EPSG_IRENET95_IRISH_TRANSVERSE_MERCATOR) {
140 18
            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

140
            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

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