Passed
Push — master ( aff60e...d66a7a )
by Doug
120:22 queued 55:20
created

ProjectedPoint::complexPolynomial()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 3

Importance

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

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