Passed
Push — 4.x ( 2e43a7...18aaa8 )
by Doug
06:37
created

ProjectedPoint::complexPolynomial()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 22
nc 1
nop 15
dl 0
loc 44
ccs 23
cts 23
cp 1
crap 3
rs 9.568
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

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